diff --git a/src/packages/compiler/utils/format-name.js b/src/packages/compiler/utils/format-name.js index 654468e3..04793a8f 100644 --- a/src/packages/compiler/utils/format-name.js +++ b/src/packages/compiler/utils/format-name.js @@ -1,5 +1,5 @@ // @flow -import { classify } from 'inflection'; +import { camelize } from 'inflection'; import chain from '../../../utils/chain'; import underscore from '../../../utils/underscore'; @@ -7,11 +7,13 @@ import underscore from '../../../utils/underscore'; import stripExt from './strip-ext'; import normalizePath from './normalize-path'; +const DOUBLE_COLON = /::/g; + /** * @private */ function applyNamespace(source: string) { - return source.replace('::', '$'); + return source.replace(DOUBLE_COLON, '$'); } /** @@ -22,7 +24,7 @@ export default function formatName(source: string) { .pipe(normalizePath) .pipe(stripExt) .pipe(underscore) - .pipe(classify) + .pipe(camelize) .pipe(applyNamespace) .value(); } diff --git a/src/packages/loader/builder/test/sort-by-namespace.test.js b/src/packages/loader/builder/test/sort-by-namespace.test.js new file mode 100644 index 00000000..36810463 --- /dev/null +++ b/src/packages/loader/builder/test/sort-by-namespace.test.js @@ -0,0 +1,37 @@ +// @flow +import { expect } from 'chai'; +import { describe, it } from 'mocha'; + +import sortByNamespace from '../utils/sort-by-namespace'; + +describe('module "loader/builder"', () => { + describe('util sortByNamespace()', () => { + it('returns -1 if "root" is the first argument', () => { + //$FlowIgnore + const result = sortByNamespace(['root'], ['api']); + + expect(result).to.equal(-1); + }); + + it('returns 1 if "root" is the second argument', () => { + //$FlowIgnore + const result = sortByNamespace(['api'], ['root']); + + expect(result).to.equal(1); + }); + + it('returns -1 if the first argument is shorter than the second', () => { + //$FlowIgnore + const result = sortByNamespace(['api'], ['admin']); + + expect(result).to.equal(-1); + }); + + it('returns 1 if the first argument is longer than the second', () => { + //$FlowIgnore + const result = sortByNamespace(['admin'], ['api']); + + expect(result).to.equal(1); + }); + }); +}); diff --git a/src/packages/loader/builder/utils/create-parent-builder.js b/src/packages/loader/builder/utils/create-parent-builder.js index 0df925a1..e11c095e 100644 --- a/src/packages/loader/builder/utils/create-parent-builder.js +++ b/src/packages/loader/builder/utils/create-parent-builder.js @@ -2,12 +2,14 @@ import { getParentKey } from '../../resolver'; import type { Builder$Construct, Builder$ParentBuilder } from '../interfaces'; +import sortByNamespace from './sort-by-namespace'; + export default function createParentBuilder( construct: Builder$Construct ): Builder$ParentBuilder { return target => Array .from(target) - .sort(([a], [b]) => a.length - b.length) + .sort(sortByNamespace) .reduce((result, [key, value]) => { let parent = value.get('application') || null; diff --git a/src/packages/loader/builder/utils/sort-by-namespace.js b/src/packages/loader/builder/utils/sort-by-namespace.js new file mode 100644 index 00000000..fe53601a --- /dev/null +++ b/src/packages/loader/builder/utils/sort-by-namespace.js @@ -0,0 +1,18 @@ +// @flow +import type { Bundle$Namespace } from '../../index'; + +/** + * @private + */ +export default function sortByNamespace( + [a]: [string, Bundle$Namespace], + [b]: [string, Bundle$Namespace] +): number { + if (a === 'root') { + return -1; + } else if (b === 'root') { + return 1; + } + + return Math.min(Math.max(a.length - b.length, -1), 1); +} diff --git a/src/packages/loader/utils/format-key.js b/src/packages/loader/utils/format-key.js index 43429261..449c7300 100644 --- a/src/packages/loader/utils/format-key.js +++ b/src/packages/loader/utils/format-key.js @@ -4,7 +4,7 @@ import { dasherize } from 'inflection'; import chain from '../../../utils/chain'; import underscore from '../../../utils/underscore'; -const NAMESPACE_DELIMITER = /\$\-/; +const NAMESPACE_DELIMITER = /\$\-/g; /** * @private diff --git a/src/packages/router/index.js b/src/packages/router/index.js index cd085a3d..254ea237 100644 --- a/src/packages/router/index.js +++ b/src/packages/router/index.js @@ -2,9 +2,9 @@ import { FreezeableMap } from '../freezeable'; import type { Request } from '../server'; -import { ID_PATTERN } from './route'; import Namespace from './namespace'; import { build, define } from './definitions'; +import createReplacer from './utils/create-replacer'; import type { Router$opts } from './interfaces'; import type Route from './route'; // eslint-disable-line no-duplicate-imports @@ -12,9 +12,9 @@ import type Route from './route'; // eslint-disable-line no-duplicate-imports * @private */ class Router extends FreezeableMap { - constructor({ routes, controller, controllers }: Router$opts) { - super(); + replacer: RegExp; + constructor({ routes, controller, controllers }: Router$opts) { const definitions = build(routes, new Namespace({ controller, controllers, @@ -22,26 +22,34 @@ class Router extends FreezeableMap { name: 'root' })); + super(); define(this, definitions); + Reflect.defineProperty(this, 'replacer', { + value: createReplacer(controllers), + writable: false, + enumerable: false, + configurable: false + }); + this.freeze(); } - match({ method, url: { pathname } }: Request) { - const staticPath = pathname.replace(ID_PATTERN, ':dynamic'); + match({ method, url }: Request): void | Route { + const params = []; + const staticPath = url.pathname.replace(this.replacer, (str, g1, g2) => { + params.push(g2); + return `${g1}/:dynamic`; + }); + + Reflect.set(url, 'params', params); return this.get(`${method}:${staticPath}`); } } export default Router; - -export { - ID_PATTERN, - DYNAMIC_PATTERN, - RESOURCE_PATTERN, - default as Route - } from './route'; +export { DYNAMIC_PATTERN, default as Route } from './route'; export type { Router$Namespace } from './interfaces'; export type { Resource$opts } from './resource'; diff --git a/src/packages/router/route/constants.js b/src/packages/router/route/constants.js index 36e1d15c..0dd73869 100644 --- a/src/packages/router/route/constants.js +++ b/src/packages/router/route/constants.js @@ -1,4 +1,2 @@ // @flow -export const ID_PATTERN = /(?!=)(\d+)/; export const DYNAMIC_PATTERN = /(:\w+)/g; -export const RESOURCE_PATTERN = /^((?!\/)[a-z\-]+)/ig; diff --git a/src/packages/router/route/index.js b/src/packages/router/route/index.js index ad402844..ba61eb14 100644 --- a/src/packages/router/route/index.js +++ b/src/packages/router/route/index.js @@ -3,7 +3,6 @@ import { FreezeableSet, freezeProps, deepFreezeProps } from '../../freezeable'; import type Controller from '../../controller'; import type { Request, Response, Request$method } from '../../server'; -import { ID_PATTERN } from './constants'; import { createAction } from './action'; import { paramsFor, defaultParamsFor, validateResourceId } from './params'; import getStaticPath from './utils/get-static-path'; @@ -112,20 +111,18 @@ class Route extends FreezeableSet> { this.freeze(); } - parseParams(pathname: string) { - const parts = pathname.match(ID_PATTERN) || []; - - return parts.reduce((params, val, index) => { - const key = this.dynamicSegments[index]; + parseParams(params: Array): Object { + return params.reduce((result, value, idx) => { + const key = this.dynamicSegments[idx]; if (key) { return { - ...params, - [key]: parseInt(val, 10) + ...result, + [key]: Number.parseInt(value, 10) }; } - return params; + return result; }, {}); } @@ -148,7 +145,7 @@ class Route extends FreezeableSet> { defaultParams, params: this.params.validate({ ...req.params, - ...this.parseParams(req.url.pathname) + ...this.parseParams(req.url.params) }) }); @@ -161,7 +158,7 @@ class Route extends FreezeableSet> { } export default Route; -export { ID_PATTERN, DYNAMIC_PATTERN, RESOURCE_PATTERN } from './constants'; +export { DYNAMIC_PATTERN } from './constants'; export type { Action } from './action'; export type { Route$opts, Route$type } from './interfaces'; diff --git a/src/packages/router/route/test/route.test.js b/src/packages/router/route/test/route.test.js index 1702c622..4be1838d 100644 --- a/src/packages/router/route/test/route.test.js +++ b/src/packages/router/route/test/route.test.js @@ -45,15 +45,15 @@ describe('module "router/route"', () => { describe('#parseParams()', () => { it('is empty for static paths', () => { - expect(staticRoute.parseParams('/posts/1')).to.be.empty; + expect(staticRoute.parseParams(['1'])).to.be.empty; }); it('contains params matching dynamic segments', () => { - expect(dynamicRoute.parseParams('/posts/1')).to.deep.equal({ id: 1 }); + expect(dynamicRoute.parseParams(['1'])).to.deep.equal({ id: 1 }); }); it('does not contain params for unmatched dynamic segments', () => { - expect(dynamicRoute.parseParams('/posts/1/2')).to.deep.equal({ id: 1 }); + expect(dynamicRoute.parseParams(['1', '2'])).to.deep.equal({ id: 1 }); }); }); }); diff --git a/src/packages/router/utils/create-replacer.js b/src/packages/router/utils/create-replacer.js new file mode 100644 index 00000000..4065d642 --- /dev/null +++ b/src/packages/router/utils/create-replacer.js @@ -0,0 +1,19 @@ +// @flow +import type Controller from '../../controller'; + +/** + * @private + */ +export default function createReplacer( + controllers: Map +): RegExp { + const names = Array + .from(controllers) + .map(([, { model }]) => model) + .filter(Boolean) + .map(({ resourceName }) => resourceName) + .filter((str, idx, arr) => idx === arr.lastIndexOf(str)) + .join('|'); + + return new RegExp(`(${names})/(\\d+)`, 'ig'); +} diff --git a/src/packages/server/request/index.js b/src/packages/server/request/index.js index 1354ba10..47b653ab 100644 --- a/src/packages/server/request/index.js +++ b/src/packages/server/request/index.js @@ -12,7 +12,7 @@ export function createRequest(req: any, { logger, router }: Request$opts): Request { - const url = parseURL(req.url, true); + const url = { ...parseURL(req.url, true), params: [] }; const headers: Map = new Map(entries(req.headers)); Object.assign(req, { diff --git a/src/packages/server/request/interfaces.js b/src/packages/server/request/interfaces.js index aa3f84ab..ce90bca6 100644 --- a/src/packages/server/request/interfaces.js +++ b/src/packages/server/request/interfaces.js @@ -16,6 +16,7 @@ type Request$url = { hostname?: string; hash?: string; search?: string; + params: Array; query: Object; pathname: string; path: string;