feat: implement joi as validator
ADD
packages/marble-joi/.editorconfig
+root = true
+
+[*]
+indent_style = space
+indent_size = 2
+end_of_line = lf
+charset = utf-8
+trim_trailing_whitespace = true
+insert_final_newline = true
+quote_type = single

ADD
packages/marble-joi/package-lock.json
+{
+ "name": "marble-joi",
+ "version": "1.0.0",
+ "lockfileVersion": 1,
+ "requires": true,
+ "dependencies": {
+ "@marblejs/core": {
+ "version": "0.2.2",
+ "resolved": "https://registry.npmjs.org/@marblejs/core/-/core-0.2.2.tgz",
+ "integrity": "sha512-SplTT7sfF6uzkZSp7NAQ659mPCVF0wsRo3A8r4OGnD/jll3X0YkaOtRlOzzovtcVeqJx9ZzZpMrR6G0ospoOZQ==",
+ "requires": {
+ "chalk": "^2.4.1",
+ "path-to-regexp": "^2.2.1"
+ }
+ },
+ "@types/joi": {
+ "version": "13.0.8",
+ "resolved": "https://registry.npmjs.org/@types/joi/-/joi-13.0.8.tgz",
+ "integrity": "sha512-GXYdIVpwBP5ZBOlHitSYfQdH+vWXVahhkeQwalX0LkoX7Mx0D3L3tg4vXXhr6nYHkEpWlAzWuEjgWEBtcp5NZA==",
+ "dev": true
+ },
+ "ansi-styles": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
+ "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
+ "requires": {
+ "color-convert": "^1.9.0"
+ }
+ },
+ "chalk": {
+ "version": "2.4.1",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz",
+ "integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==",
+ "requires": {
+ "ansi-styles": "^3.2.1",
+ "escape-string-regexp": "^1.0.5",
+ "supports-color": "^5.3.0"
+ }
+ },
+ "color-convert": {
+ "version": "1.9.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.1.tgz",
+ "integrity": "sha512-mjGanIiwQJskCC18rPR6OmrZ6fm2Lc7PeGFYwCmy5J34wC6F1PzdGL6xeMfmgicfYcNLGuVFA3WzXtIDCQSZxQ==",
+ "requires": {
+ "color-name": "^1.1.1"
+ }
+ },
+ "color-name": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
+ "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU="
+ },
+ "escape-string-regexp": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
+ "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ="
+ },
+ "has-flag": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
+ "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0="
+ },
+ "hoek": {
+ "version": "5.0.3",
+ "resolved": "https://registry.npmjs.org/hoek/-/hoek-5.0.3.tgz",
+ "integrity": "sha512-Bmr56pxML1c9kU+NS51SMFkiVQAb+9uFfXwyqR2tn4w2FPvmPt65eZ9aCcEfRXd9G74HkZnILC6p967pED4aiw=="
+ },
+ "isemail": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/isemail/-/isemail-3.1.2.tgz",
+ "integrity": "sha512-zfRhJn9rFSGhzU5tGZqepRSAj3+g6oTOHxMGGriWNJZzyLPUK8H7VHpqKntegnW8KLyGA9zwuNaCoopl40LTpg==",
+ "requires": {
+ "punycode": "2.x.x"
+ }
+ },
+ "joi": {
+ "version": "13.3.0",
+ "resolved": "https://registry.npmjs.org/joi/-/joi-13.3.0.tgz",
+ "integrity": "sha512-iF6jEYVfBIoYXztYymia1JfuoVbxBNuOcwdbsdoGin9/jjhBLhonKmfTQOvePss8r8v4tU4JOcNmYPHZzKEFag==",
+ "requires": {
+ "hoek": "5.x.x",
+ "isemail": "3.x.x",
+ "topo": "3.x.x"
+ }
+ },
+ "path-to-regexp": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-2.2.1.tgz",
+ "integrity": "sha512-gu9bD6Ta5bwGrrU8muHzVOBFFREpp2iRkVfhBJahwJ6p6Xw20SjT0MxLnwkjOibQmGSYhiUnf2FLe7k+jcFmGQ=="
+ },
+ "punycode": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.0.tgz",
+ "integrity": "sha1-X4Y+3Im5bbCQdLrXlHvwkFbKTn0="
+ },
+ "rxjs": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.1.0.tgz",
+ "integrity": "sha512-lMZdl6xbHJCSb5lmnb6nOhsoBVCyoDC5LDJQK9WWyq+tsI7KnlDIZ0r0AZAlBpRPLbwQA9kzSBAZwNIZEZ+hcw==",
+ "requires": {
+ "tslib": "^1.9.0"
+ }
+ },
+ "supports-color": {
+ "version": "5.4.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz",
+ "integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==",
+ "requires": {
+ "has-flag": "^3.0.0"
+ }
+ },
+ "topo": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/topo/-/topo-3.0.0.tgz",
+ "integrity": "sha512-Tlu1fGlR90iCdIPURqPiufqAlCZYzLjHYVVbcFWDMcX7+tK8hdZWAfsMrD/pBul9jqHHwFjNdf1WaxA9vTRRhw==",
+ "requires": {
+ "hoek": "5.x.x"
+ }
+ },
+ "tslib": {
+ "version": "1.9.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.9.1.tgz",
+ "integrity": "sha512-avfPS28HmGLLc2o4elcc2EIq2FcH++Yo5YxpBZi9Yw93BCTGFthI4HPE4Rpep6vSYQaK8e69PelM44tPj+RaQg=="
+ },
+ "typescript": {
+ "version": "2.8.3",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-2.8.3.tgz",
+ "integrity": "sha512-K7g15Bb6Ra4lKf7Iq2l/I5/En+hLIHmxWZGq3D4DIRNFxMNV6j2SHSvDOqs2tGd4UvD/fJvrwopzQXjLrT7Itw==",
+ "dev": true
+ }
+ }
+}

ADD
packages/marble-joi/src/index.ts
+import * as Joi from 'joi';
+import { Schema, SchemaValidator } from './schema';
+import { Effect, HttpRequest, HttpError, HttpStatus } from '@marblejs/core';
+import { of, Observable, throwError, from, empty } from 'rxjs';
+import { tap, switchMap, toArray, map, catchError, mapTo, mergeMap, switchMapTo } from 'rxjs/operators';
+import { filter } from 'minimatch';
+
+const validateSource = (req: HttpRequest, rules: Map<string, any>, options) => from(rules.keys()).pipe(
+ switchMap(rule => of(req[rule]).pipe(
+ mergeMap(item => {
+ return typeof item !== 'undefined'
+ ? of(item)
+ : empty();
+ }),
+ mergeMap(item => from(Joi.validate(item, rules.get(rule), options))),
+ catchError((err: Error) => throwError(new HttpError(err.message, HttpStatus.BAD_REQUEST))),
+ )),
+ mapTo(req),
+);
+
+const validator$ = (schema: Schema, options: Joi.ValidationOptions = {}): Effect<HttpRequest> => request$ => {
+ const result = Joi.validate(schema, SchemaValidator);
+ const rules = Object.keys(schema).reduce((acc, value) => acc.set(value, Joi.compile(schema[value])), new Map());
+
+ if (result && result.error) {
+ return throwError(result.error);
+ }
+
+ return request$.pipe(
+ tap(req => req.params = req.route ? req.route.params : {}),
+ switchMap(req => validateSource(req, rules, options)),
+ );
+};
+
+export {
+ Joi,
+ validator$,
+};

ADD
packages/marble-joi/src/schema.ts
+const Joi = require('joi');
+
+export const SchemaValidator = Joi.object().keys({
+ headers: Joi.any(),
+ params: Joi.any(),
+ query: Joi.any(),
+ body: Joi.any(),
+}).min(1);
+
+export interface Schema {
+ headers?: any;
+ params?: any;
+ query?: any;
+ body?: any;
+}

ADD
packages/marble-joi/test.ts
+import { validator$, Joi } from './src';
+import { httpListener, Effect, matchPath, matchType, combineRoutes, bodyParser$, error$ } from '@marblejs/core';
+import { use } from '@marblejs/core/dist/operators/use.operator';
+import { map, tap } from 'rxjs/operators';
+
+const root$: Effect = request$ => request$
+ .pipe(
+ matchPath('/'),
+ matchType('GET'),
+ map(req => ({
+ body: `API root @ ${req.url}`,
+ }))
+ );
+
+export const api$ = combineRoutes(
+ '/api/v1',
+ [ root$ ],
+);
+
+const createServer = app => {
+ const HOSTNAME = '127.0.0.1';
+ const PORT = 1337;
+
+ const httpServer = require('http')
+ .createServer(app)
+ .listen(PORT, HOSTNAME, () => {
+ console.log(`Server running @ http://${HOSTNAME}:${PORT}/`);
+ });
+
+ httpServer.on('close', () => {
+ console.log(`Connection closed`);
+ });
+};
+
+
+const middlewares = [
+ bodyParser$,
+ validator$({ body: {
+ id: Joi.string(),
+ }})
+];
+
+const effect$1: Effect = req$ => req$.pipe(
+ matchPath('/test/:id'),
+ use(validator$({ params: {
+ id: Joi.number().min(1).max(10),
+ }})),
+ map(req => req.route.params.id),
+ map(id => ({ status: 200, body: { id } })),
+);
+
+const effects = [
+ api$,
+ effect$1
+];
+
+const listener = httpListener({ middlewares, effects, errorMiddleware: error$ });
+createServer(listener);

ADD
packages/marble-joi/tsconfig.json
+{
+ "compilerOptions": {
+ "outDir": "./dist/",
+ "emitDecoratorMetadata": true,
+ "experimentalDecorators": true,
+ "module": "commonjs",
+ "moduleResolution": "node",
+ "declaration": true,
+ "noImplicitAny": false,
+ "noEmitOnError": true,
+ "pretty": true,
+ "strict": true,
+ "strictNullChecks": false,
+ "target": "es6",
+ "lib": [
+ "dom",
+ "es7"
+ ]
+ },
+
+ "inputs": [
+ "src"
+ ],
+
+ "include": [
+ "src/**/*.ts"
+ ],
+
+ "exclude": [
+ "**/*.spec.ts"
+ ]
+}

ADD
packages/marble-joi/yarn.lock
+# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
+# yarn lockfile v1
+
+
+hoek@5.x.x:
+ version "5.0.3"
+ resolved "https://registry.yarnpkg.com/hoek/-/hoek-5.0.3.tgz#b71d40d943d0a95da01956b547f83c4a5b4a34ac"
+
+isemail@3.x.x:
+ version "3.1.2"
+ resolved "https://registry.yarnpkg.com/isemail/-/isemail-3.1.2.tgz#937cf919002077999a73ea8b1951d590e84e01dd"
+ dependencies:
+ punycode "2.x.x"
+
+joi@^13.3.0:
+ version "13.3.0"
+ resolved "https://registry.yarnpkg.com/joi/-/joi-13.3.0.tgz#4defd4333b539c5d10e444ab44f5a5583480f17c"
+ dependencies:
+ hoek "5.x.x"
+ isemail "3.x.x"
+ topo "3.x.x"
+
+punycode@2.x.x:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.0.tgz#5f863edc89b96db09074bad7947bf09056ca4e7d"
+
+topo@3.x.x:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/topo/-/topo-3.0.0.tgz#37e48c330efeac784538e0acd3e62ca5e231fe7a"
+ dependencies:
+ hoek "5.x.x"

feat: implement joi as validator