Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(rest): Implement trie-based routing and tidy up path validation #1765

Merged
merged 2 commits into from Oct 8, 2018

Conversation

raymondfeng
Copy link
Contributor

@raymondfeng raymondfeng commented Sep 26, 2018

The PR introduces the following improvements:

  • validate and normalize paths for openapi
  • add benchmark for routing
  • make the router pluggable
  • simplify app routing to plain functions
  • optimize simple route mapping and use regexp as default
  • add rest.router binding to allow customization
  • add docs for routing requests
  1. Optimize routes without variables to use a direct lookup

Checklist

  • npm test passes on your machine
  • New tests added or existing tests modified to cover all changes
  • Code conforms with the style guide
  • API Documentation in code was updated
  • Documentation in /docs/site was updated
  • Affected artifact templates in packages/cli were updated
  • Affected example projects in examples/* were updated

Copy link
Contributor

@virkt25 virkt25 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM at a high level -- I do have some questions to better understand this PR. See comments below.

@@ -127,6 +128,8 @@ export class RoutingTable {
);
}
this._routes.push(route);
// Sort routes
this._routes.sort(compareRoute);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What benefit do we get by sorting the routes?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Think about this example:

/**
 * A simple controller to bounce back http requests
 */
export class PingController {
  constructor(@inject(RestBindings.Http.REQUEST) private req: Request) {}

  @get('/ping/:me')
  pingMe(@param.path.string('me') me: string): object {
    return {
      greeting: `Hello from ${me}`,
    };
  }

  // Map to `GET /ping`
  @get('/ping')
  ping(): object {
    return {
      greeting: 'Hello from LoopBack',
      date: new Date(),
      url: this.req.url,
      headers: Object.assign({}, this.req.headers),
    };
  }

  @get('/ping/xyz')
  pingXyz(): object {
    return {
      greeting: 'Hello from XYZ',
    };
  }
}

I would expect GET /ping/xyz is routed to pingXyz() instead of pingMe(). Without this PR, it matches GET /ping/:me.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll add an integration test.

expect(path).to.eql('/{foo}');
});

it('allows /:foo/bar', () => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We're going to start supporting express style of parameters? If so, we should document it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We didn't do any check before this PR. I think it will be good to allow express style too.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We didn't do any check before this PR. I think it will be good to allow express style too.

I would rather not introduce multiple parameter styles, I am concerned about the confusion it can create and the extra work it will add for any consumer of routes.

@@ -80,7 +81,7 @@ function resolveControllerSpec(constructor: Function): ControllerSpec {

const endpoint = endpoints[op];
const verb = endpoint.verb!;
const path = endpoint.path!;
const path = toOpenApiPath(endpoint.path!);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm ... why is path not a required property? (If it was we wouldn't need the !.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If path is missing, it's default to /'.

@raymondfeng raymondfeng force-pushed the validate-openapi-path branch 2 times, most recently from 0833be5 to b595511 Compare September 27, 2018 18:25
Copy link
Contributor

@virkt25 virkt25 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM. I'd like to see one more review on this before it's merged.
cc: @b-admike @jannyHou

Copy link
Contributor

@b-admike b-admike left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice work; I've left some comments, but LGTM overall.

export function compareRoute(
route1: Pick<RouteEntry, 'verb' | 'path'>,
route2: Pick<RouteEntry, 'verb' | 'path'>,
) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we have the return type here as number?

route2: Pick<RouteEntry, 'verb' | 'path'>,
) {
// First check verb
const verb1 = HTTP_VERBS[route1.verb.toLowerCase()] || 3;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For clarification, why do we set the default verb to patch here?

}

// Now check token by token
for (let i = 0; i < tokensForPath1.length; i++) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we have test coverage for this code path?

Copy link
Member

@bajtos bajtos left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not very happy about this change at high level. For medium to long term, we should abandon Express router and use a more performant alternative (probably based on Trie). This was planned all the way back in LoopBack pre-3.0 days, and we have a loopback-next issue keeping track of this work too - see #98.

I would prefer to switch to the new router first and only then start adding new features like express-styled parameters, to ensure we don't accidentally add an Express-specific feature that is difficult to support with the new router.

Having said that, the example where /ping/{me} takes precedence over /ping/xyz is clearly a bug (a violation of OpenAPI Spec) that should be fixed.

expect(path).to.eql('/{foo}');
});

it('allows /:foo/bar', () => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We didn't do any check before this PR. I think it will be good to allow express style too.

I would rather not introduce multiple parameter styles, I am concerned about the confusion it can create and the extra work it will add for any consumer of routes.

@raymondfeng
Copy link
Contributor Author

I can update the PR to report errors if Express styles are used for the GA. Here is what led me to create this PR:

  1. I was playing with the Ping controller. As somebody familiar with Express, I used :id style. But our runtime does not complain and it causes problems in the generated OpenAPI spec.

  2. I also found out that /ping/xyz was not routing the correct method if we have /ping/{me} and /ping/xyz.

@raymondfeng
Copy link
Contributor Author

@bajtos I reverted the sorting based approach and implemented trie-based routing based on wayfarer. PTAL.

@raymondfeng raymondfeng changed the title fix(openapi-v3): validate and normalize paths for openapi feat(openapi-v3): Implement trie-based routing and tidy up path validation Oct 1, 2018
@raymondfeng raymondfeng changed the title feat(openapi-v3): Implement trie-based routing and tidy up path validation feat(rest): Implement trie-based routing and tidy up path validation Oct 1, 2018
Copy link
Member

@bajtos bajtos left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice!

let tokens = pathToRegExp.parse(path);
if (tokens.some(t => typeof t === 'object')) {
throw new Error(
`Invalid path template: '${path}'. Please use {param} instead of ':param'`,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

*/
export function toOpenApiPath(path: string = '/') {
validatePath(path);
return path.replace(/\:([A-Za-z0-9_]+)/g, '{$1}');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this replace call still needed, considering that validatePath rejects values containing express-style parameters? Maybe I am missing something?

packages/openapi-v3/test/unit/to-openapi-path.unit.ts Outdated Show resolved Hide resolved
packages/openapi-v3/test/unit/to-openapi-path.unit.ts Outdated Show resolved Hide resolved
// Add the route to the trie
const path = route.path.startsWith('/') ? route.path : `/${route.path}`;
const verb = route.verb.toLowerCase() || 'get';
this._router.on(`/${verb}${path}`, () => {});
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we store route on the function passed to this._router.on to avoid costly lookup (iteration through this._routes later on? E.g.

// store
this._router.on(`/${verb}${path}`, Object.assign(() => {}, {route}));

// pick up - I hope that's the Wayfarer API
const found = this._router.match(path);
const route = found.route;

packages/rest/src/router/routing-table.ts Outdated Show resolved Hide resolved
.expect(200, 'hello john');
await whenIMakeRequestTo(app)
.get('/greet/world')
.expect(200, 'hello WORLD');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like this test much more than the previous version 👍

@bajtos
Copy link
Member

bajtos commented Oct 1, 2018

Could you also update the benchmark to show the new performance with Wayfarer under the hood? See https://github.com/strongloop/loopback-next/tree/master/benchmark

Copy link
Contributor

@b-admike b-admike left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks awesome 👍

@raymondfeng raymondfeng force-pushed the validate-openapi-path branch 2 times, most recently from 9338db3 to 1181189 Compare October 1, 2018 18:11
@raymondfeng
Copy link
Contributor Author

@b-admike @bajtos I pushed more changes to use _router to store routes per @bajtos suggestion. PATL.

Copy link
Member

@bajtos bajtos left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have few more comments I'd like to see addressed.

I don't want to block the progress though, so feel free to address them in a follow-up pull request if you prefer.

debug(' -> found with params: %j', pathParams);

return createResolvedRoute(this, pathParams);
this.path = path.replace(/{([A-Za-z0-9_]*)}/g, ':$1');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This RegExp will convert /{} to /: - is that intentional? Could you please add a test for this case? I think we should preserve /{} as /{} and use + instead of * in the regular expression.

I see you are allowing only [A-Za-z0-9_] characters. Is this based on character list allowed by OpenAPI spec? Please add some tests to show what happens when a different character is used in the path param name, e.g. /{foo*} or /{jméno} ("jméno" is "name" in Czech)

}

greetWorld() {
return `hello WORLD`;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's use HELLO WORLD to make the distinction from greet more obvious. You can also use regular apostrophes instead of back-ticks.

}
}

const spec = anOpenApiSpec()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this test setup can be greatly simplified by using app.route API, see https://loopback.io/doc/en/lb4/Routes.html#using-partial-openapi-spec-fragments

app.route(new Route(
  'get',
  '/greet/{name}',
  anOperationSpec().withParameter({name: 'name', in: 'path', type: 'string'}),
  (name: string) => `hello ${name}`,
));

app.route(new Route(
  'get',
  '/greet/world',
  anOperationSpec(),
  () => 'HELLO WORLD',
));

In case .route() is not exposed on RestApplication class, you can call app.restServer.route() instead (or add the missing RestApplication.prototype.route() shortcut).

* Validate and normalize the path to OpenAPI path template. No parameter
* modifier, custom pattern, or unnamed parameter is allowed.
*
* @param path Express style path (https://github.com/pillarjs/path-to-regexp)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I find this confusing - the method is used in a way that OpenAPI-styled path is passed in the input.

I think we should remove this method completely and modify resolveControllerSpec implementation to call validatePath instead. Thoughts?

On the second thought, I think we should call validatePath right at the time when a new route is created, regardless of whether the route is Controller-based or handler-based (see app.route API).

Could you please add some tests to verify that invalid paths are correctly rejected for all possible sources of path strings? I am thinking about the following sources: @get decorator on a controller method, app.route/restServer.route, paths provided in the OpenAPI spec document passed to app.api().

@raymondfeng raymondfeng force-pushed the validate-openapi-path branch 5 times, most recently from b35a395 to 056062a Compare October 3, 2018 04:57
@raymondfeng
Copy link
Contributor Author

I added optimization for those routes that don't have path variables. The RoutingTable by default uses RegExpRouter now.

@raymondfeng raymondfeng force-pushed the validate-openapi-path branch 3 times, most recently from e2c61d9 to 285e128 Compare October 3, 2018 23:26
@b-admike
Copy link
Contributor

b-admike commented Oct 4, 2018

@raymondfeng I looked at your recent changes at a high level and they look great 👍. I'll look into them in detail tomorrow.

Copy link
Member

@bajtos bajtos left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not a fan of having multiple routers when it's not clear how should LB4 users decided which router to use. Personally, I prefer the framework to make to he hard decisions and let users to focus on delivering business value.

I am also concerned about adding ~1.2k lines of new code that we will have to maintain and fix bug inside.

Having said that, if others thinks this is a good idea, then I am not going to block progress.

At minimum, please add documentation explaining the users how to configure the app to use a different router, explain the trade-offs between our two provided routers so that users can better decide which one to use.

Nice to have: document how to implement a custom router. Most importantly, we need a clear documentation on the assumption made by RestServer about the behavior of a custom router. A test suite that people implementing a custom router can execute to validate their implementation would be ideal.

Extensibility comes with costs if we want to make it user friendly.

this._httpHandler = new HttpHandler(this);
const router = this.getSync(RestBindings.ROUTER, {optional: true});
let routingTable: RoutingTable;
if (router) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we move this if to RoutingTable constructor so that RestServer can be simplified as follows:

const routingTable = new RoutingTable(router);

// License text available at https://opensource.org/licenses/MIT

export {
RouteEntry,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Considering how many items we are exporting, I think it may be best to simply export everything.

export * from `./routing-table';

Thoughts?

private readonly _routes: RouteEntry[] = [];
private readonly _router: RestRouter = new RegExpRouter();

constructor(router?: RestRouter) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about:

constructor(private readonly router: RestRouter = new RegExpRouter()) {
  // ...
}

@raymondfeng raymondfeng force-pushed the validate-openapi-path branch 2 times, most recently from fe823e7 to c40c548 Compare October 4, 2018 14:38
@raymondfeng
Copy link
Contributor Author

@bajtos Addressed your coding comments and added docs. PTAL.

I would like to have it landed with our GA as the mapping of routes without variables are much faster now.


```ts
import {RestBindings} from '@loopback/rest';
app.bind(RestBindings.ROUTER).toClass(TrieRouter);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick: Which file would this be in and where can users import TrieRouter from?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch.

/**
* Check if there is custom router in the context
*/
const router = this.getSync(RestBindings.ROUTER, {optional: true});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need a try/catch block for the getSync call here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, as it uses an optional flag.

const server = await app.getServer(RestServer);
const handler = await server.get(RestBindings.HANDLER);
// tslint:disable-next-line:no-any
expect((handler as any)._routes._router).to.be.instanceof(TrieRouter);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick: why do we need to cast handler as any here? can we do const handler = await server.get<HttpHandler>(RestBindings.HANDLER); above?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a hacky way to test non-public properties.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@raymondfeng Please add a comment to the code to preserve this explanation for future readers.

Copy link
Member

@bajtos bajtos left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed the code in details, PTAL at my comments below.

Please update README in benchmark folder to show the new performance statistics. It would be great to have a similar README for your newly added benchmark, one that shows "baseline" data of the current implementation.


I am not very happy about having to review such a large change in the short time we have :(

One of my major concerns is that we don't have a Router test suite that we could run on both RegExpRouter and TrieRouter to verify that all routers are correctly handling all edge cases we have identified so far. Having two sets of tests (one for each router) is not manageable.

I would prefer very much to leave TrieRouter out of this pull request, probably with BaseRouter too. Adding an extension point (a custom router) can be easily done post-4.0 GA as it's a semver-minor change. Changing the API contract of an ill-designed extension point is much more difficult as it's a breaking change.

I am not convinced the current design of the extension point is good enough to serve us for the next 6+ months:

  • See my comments about BaseRouter design
  • There is no easy way how to test all aspects of a new Router to ensure it's fully compliant with our expectations
  • It's not clear to me if (and how) can users customize the router at Server vs. Application level.

Let's give us more time to find a good design for Router extensibility and implement a robust solution that's easy to use both for users and Router writers.

@@ -44,7 +44,8 @@
"path-to-regexp": "^2.2.0",
"qs": "^6.5.2",
"strong-error-handler": "^3.2.0",
"validator": "^10.4.0"
"validator": "^10.4.0",
"wayfarer": "^6.6.4"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AFAICT, wayfarer is not used by our code, can you remove this dependency please?

path: string,
spec: OperationObject,
handler: Function,
): Binding;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please use this new API to simplify Routing doc page - see https://loopback.io/doc/en/lb4/Routes.html#using-partial-openapi-spec-fragments

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please use this new API to simplify Routing doc page - see https://loopback.io/doc/en/lb4/Routes.html#using-partial-openapi-spec-fragments

This hasn't been addressed AFAICT, please take a look ☝️

const router = this.getSync(RestBindings.ROUTER, {optional: true});
const routingTable = new RoutingTable(router);

this._httpHandler = new HttpHandler(this, routingTable);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am wondering if it's a good idea for HttpHandler to accept RoutingTable instance. Have you considered an alternative design where HttpHandler accepts a router instance only?

const router = this.getSync(RestBindings.ROUTER, {optional: true});
this._httpHandler = new HttpHandler(this, router);

IMO, the RoutingTable class is an implementation detail of HttpHandler that's not open for extensibility to consumers.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please note that using the constructor the pass down a version of RestRouter is still internal implementation details, which we can change later. The public contract is to bind a custom RestRouter to the given key.

*
* @param path Path template with `{var}`
*/
export function toOpenApiPath(path: string = '/') {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I still don't understand the purpose of this toOpenApiPath function - what are the benefits of calling toOpenApiPath as compared to calling validatePath directly?

* Validate the path to be compatible with OpenAPI path template. No parameter
* modifier, custom pattern, or unnamed parameter is allowed.
*/
export function validatePath(path: string = '/') {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps validateApiPath would be a better name, to make it clear that we are validating REST API/URL paths and not filesystem paths?

debug('Route matched: %j', found);

if (found) {
route = found.node.value!;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you fix the type definition of trie.match to communicate the fact that value is always set when a route was found?

For example:

export interface ResolvedNode<T> {
   node: Node<T> & {value: T}; // the value is always set
   // tslint:disable-next-line:no-any
   params?: {[name: string]: any};
 }

const keys = path.split('/').filter(Boolean);
const params = {};
const resolved = search(keys, 0, params, this.root);
if (resolved && resolved.node.value != null) return resolved;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if (resolved && this.isNodeWithValue(resolved.node)) return resolved;

let i = 0;
for (const n of child.names) {
const val = match[++i];
resolved.params![n] = decodeURIComponent(val);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we avoid relying on ! parameter?

E.g.

// you should probably define a new type for param map (don't we already have one?)
const params: {[name: string]: any} = {};
let i = 0;
for (const n of child.names) {
  const val = match[++i];
  params[n] = decodeURIComponent(val);
}
return {params, node: child};

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we avoid relying on ! operator please?

PTAL ☝️

class TestController {}

const table = givenRoutingTable();
table.registerController(spec, TestController);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have you considered using route-base approach to avoid the complexity of dealing with controllers?

expect(route).to.have.property('pathParams');
expect(route.describe()).to.equal('TestController.greet');
});

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add tests to verify how templated paths like /hello/{name} are handled.

@raymondfeng raymondfeng force-pushed the validate-openapi-path branch 2 times, most recently from 32c7e4b to e05b421 Compare October 5, 2018 18:06
@raymondfeng
Copy link
Contributor Author

@bajtos I cleaned up the code based on your feedback.

The final benchmark now shows trie based router performs much better than regexp. I must have messed up some of the testing before.

name                 duration    count    found   missed
TrieRouter          0,1453883       10        8        2
RegExpRouter        0,1220030       10        8        2

name                 duration    count    found   missed
TrieRouter          0,2109957       40       35        5
RegExpRouter        0,8762936       40       35        5

name                 duration    count    found   missed
TrieRouter          0,4895252      160      140       20
RegExpRouter       0,52156699      160      140       20

name                 duration    count    found   missed
TrieRouter         0,16065852      640      560       80
RegExpRouter      0,304921026      640      560       80

name                 duration    count    found   missed
TrieRouter         0,60165877     2560     2240      320
RegExpRouter      4,592089555     2560     2240      320

I now make TrieRouter the default and have test/benchmark coverage for both TrieRouter and RegExpRouter.

@raymondfeng raymondfeng force-pushed the validate-openapi-path branch 3 times, most recently from b3215d3 to 8eecce6 Compare October 8, 2018 03:50
Copy link
Member

@bajtos bajtos left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The patch looks reasonable now. Not all of my comments have been addressed and I found few more places where to improve the implementation (see below), but I don't see any major problems now.

FWIW, LGTM.

path: string,
spec: OperationObject,
handler: Function,
): Binding;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please use this new API to simplify Routing doc page - see https://loopback.io/doc/en/lb4/Routes.html#using-partial-openapi-spec-fragments

This hasn't been addressed AFAICT, please take a look ☝️

*/
interface RegExpRouteEntry extends RouteEntry {
regexp?: RegExp;
keys?: pathToRegExp.Key[];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How can we have RegExpRouteEntry instance with undefined regexp and keys? AFAICT, these two properties are always set by our code.

this.routes.push(entry);
const path = toExpressPath(route.path);
entry.keys = [];
entry.regexp = pathToRegExp(path, entry.keys, {strict: false, end: true});
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you marked keys and regexp as optiona to support the code in addRouteWithPathVars, then I think the following implementation may be better:

const path = toExpressPath(route.path);
const keys = [];
const regexp = pathToRegExp(path, keys, {strict: false, end: true});
const entry: RegExpRouteEntry = Object.assign(route, {keys, regexp});

pathMatch: RegExpExecArray,
): PathParameterValues {
const pathParams: PathParameterValues = {};
for (const ix in route.keys!) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

route.keys is always set, we should not need to use ! here.

continue;
}

const match = r.regexp!.exec(request.path);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

r.regexp is always set, we should not need to use ! here.

this._sort();
for (const r of this.routes) {
debug('trying endpoint %s', inspect(r, {depth: 5}));
if (r.verb !== request.method!.toLowerCase()) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you sure we need ! after request.method? BaseRouter#getKeyForRequest does not use it and calls request.method.toLowerCase().

let i = 0;
for (const n of child.names) {
const val = match[++i];
resolved.params![n] = decodeURIComponent(val);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we avoid relying on ! operator please?

PTAL ☝️

@raymondfeng raymondfeng force-pushed the validate-openapi-path branch 2 times, most recently from 6d60f14 to 7b5430e Compare October 8, 2018 16:13
- validate and normalize paths for openapi
- add benchmark for routing
- make the router pluggable
- simplify app routing to plain functions
- optimize simple route mapping and use regexp as default
- add `rest.router` binding to allow customization
- add docs for routing requests
@raymondfeng
Copy link
Contributor Author

@bajtos I addressed your additional comments.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants