Skip to content

Commit afbb783

Browse files
kibertoadkobik
authored andcommitted
Match path to endpoint regardless of id name (#74)
* Match path to endpoint regardless of id name * Add memoization to improve performance * Address code review comment
1 parent a5f76cc commit afbb783

5 files changed

Lines changed: 326 additions & 6 deletions

File tree

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,8 @@
5656
"author": "Idan Tovi",
5757
"license": "Apache-2.0",
5858
"dependencies": {
59-
"api-schema-builder": "^1.0.9"
59+
"api-schema-builder": "^1.0.9",
60+
"memoizee": "^0.4.14"
6061
},
6162
"devDependencies": {
6263
"@typescript-eslint/eslint-plugin": "^1.9.0",

src/middleware.js

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
'use strict';
22

3+
const SchemaEndpointResolver = require('./utils/schemaEndpointResolver');
4+
35
const InputValidationError = require('./inputValidationError'),
46
apiSchemaBuilder = require('api-schema-builder');
57
const allowedFrameworks = ['express', 'koa'];
68

79
let schemas = {};
810
let middlewareOptions;
911
let framework;
12+
let schemaEndpointResolver;
1013

1114
function init(swaggerPath, options) {
1215
middlewareOptions = options || {};
@@ -15,9 +18,10 @@ function init(swaggerPath, options) {
1518
});
1619

1720
framework = frameworkToLoad ? require(`./frameworks/${frameworkToLoad}`) : require('./frameworks/express');
21+
schemaEndpointResolver = new SchemaEndpointResolver();
1822

1923
// build schema for requests only
20-
let schemaBuilderOptions = Object.assign({}, options, {buildRequests: true, buildResponses: false});
24+
let schemaBuilderOptions = Object.assign({}, options, { buildRequests: true, buildResponses: false });
2125
return apiSchemaBuilder.buildSchema(swaggerPath, schemaBuilderOptions).then((receivedSchemas) => {
2226
schemas = receivedSchemas;
2327
});
@@ -52,17 +56,19 @@ function _validateRequest(requestOptions) {
5256

5357
function _validateBody(body, path, method) {
5458
return new Promise(function (resolve, reject) {
55-
if (schemas[path] && schemas[path][method] && schemas[path][method].body && !schemas[path][method].body.validate(body)) {
56-
return reject(schemas[path][method].body.errors);
59+
const methodSchema = schemaEndpointResolver.getMethodSchema(schemas, path, method);
60+
if (methodSchema && methodSchema.body && !methodSchema.body.validate(body)) {
61+
return reject(methodSchema.body.errors);
5762
}
5863
return resolve();
5964
});
6065
}
6166

6267
function _validateParams(headers, pathParams, query, files, path, method) {
6368
return new Promise(function (resolve, reject) {
64-
if (schemas[path] && schemas[path][method] && schemas[path][method].parameters && !schemas[path][method].parameters.validate({ query: query, headers: headers, path: pathParams, files: files })) {
65-
return reject(schemas[path][method].parameters.errors);
69+
const methodSchema = schemaEndpointResolver.getMethodSchema(schemas, path, method);
70+
if (methodSchema && methodSchema.parameters && !methodSchema.parameters.validate({ query: query, headers: headers, path: pathParams, files: files })) {
71+
return reject(methodSchema.parameters.errors);
6672
}
6773

6874
return resolve();
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
const memoize = require('memoizee');
2+
3+
// This logic is wrapped into class to have isolated memoization contexts
4+
class SchemaEndpointResolver {
5+
constructor() {
6+
this.getMethodSchema = memoize(getMethodSchemaInternal);
7+
}
8+
}
9+
10+
function getMethodSchemaInternal(schemas, path, method) {
11+
const methodLowerCase = method.toLowerCase();
12+
const routePath = pathMatcher(schemas, path);
13+
const route = schemas[routePath];
14+
15+
if (route && route[methodLowerCase]) {
16+
return route[methodLowerCase];
17+
}
18+
}
19+
20+
function pathMatcher(routes, path) {
21+
return Object
22+
.keys(routes)
23+
.find((route) => {
24+
const routeArr = route.split('/');
25+
const pathArr = path.split('/');
26+
27+
if (routeArr.length !== pathArr.length) return false;
28+
29+
return routeArr.every((seg, idx) => {
30+
if (seg === pathArr[idx]) return true;
31+
32+
// if current path segment is param
33+
if (seg.startsWith(':') && pathArr[idx]) return true;
34+
35+
return false;
36+
});
37+
});
38+
}
39+
40+
module.exports = SchemaEndpointResolver;
Lines changed: 228 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,228 @@
1+
openapi: 3.0.0
2+
servers:
3+
- url: 'https://api.paymentsos.com/'
4+
info:
5+
x-logo:
6+
url: payos_logo_blue_pad.png
7+
backgroundColor: white
8+
description: >
9+
# Overview
10+
11+
version: 1.2.0
12+
title: PaymentsOS API
13+
tags:
14+
- name: Tokens
15+
description: >-
16+
Tokenization is a process that safeguards sensitive card data, converting
17+
a card's details to a representative token.
18+
x-tagGroups:
19+
- name: Reference
20+
tags:
21+
- Tokens
22+
security:
23+
- app-id: []
24+
private-key: []
25+
paths:
26+
/pets/{petId}:
27+
get:
28+
summary: get pet
29+
security:
30+
- public-key: []
31+
description: >-
32+
tags:
33+
- pets
34+
parameters:
35+
- name: petId
36+
in: path
37+
required: true
38+
description: The id of the pet to retrieve
39+
schema:
40+
type: string
41+
operationId: getPet
42+
responses:
43+
'200':
44+
description: pet
45+
headers:
46+
x-zooz-request-id:
47+
description: request id
48+
schema:
49+
type: string
50+
content:
51+
application/json:
52+
schema:
53+
$ref: '#/components/schemas/pet'
54+
'400':
55+
description: Bad request
56+
headers:
57+
x-zooz-request-id:
58+
description: request id
59+
schema:
60+
type: string
61+
content:
62+
application/json:
63+
schema:
64+
$ref: '#/components/schemas/error_model'
65+
'401':
66+
description: Unauthorize
67+
headers:
68+
x-zooz-request-id:
69+
description: request id
70+
schema:
71+
type: string
72+
content:
73+
application/json:
74+
schema:
75+
$ref: '#/components/schemas/error_model'
76+
'500':
77+
description: Internal error
78+
headers:
79+
x-zooz-request-id:
80+
description: request id
81+
schema:
82+
type: string
83+
content:
84+
application/json:
85+
schema:
86+
$ref: '#/components/schemas/error_model'
87+
put:
88+
summary: update pet
89+
security:
90+
- public-key: []
91+
description: >-
92+
tags:
93+
- pets
94+
95+
requestBody:
96+
content:
97+
application/json:
98+
schema:
99+
$ref: '#/components/schemas/pet'
100+
parameters:
101+
- name: petId
102+
in: path
103+
required: true
104+
description: The id of the pet to retrieve
105+
schema:
106+
type: string
107+
operationId: updatePet
108+
responses:
109+
'200':
110+
description: pet
111+
headers:
112+
x-zooz-request-id:
113+
description: request id
114+
schema:
115+
type: string
116+
content:
117+
application/json:
118+
schema:
119+
$ref: '#/components/schemas/pet'
120+
'400':
121+
description: Bad request
122+
headers:
123+
x-zooz-request-id:
124+
description: request id
125+
schema:
126+
type: string
127+
content:
128+
application/json:
129+
schema:
130+
$ref: '#/components/schemas/error_model'
131+
'401':
132+
description: Unauthorize
133+
headers:
134+
x-zooz-request-id:
135+
description: request id
136+
schema:
137+
type: string
138+
content:
139+
application/json:
140+
schema:
141+
$ref: '#/components/schemas/error_model'
142+
'500':
143+
description: Internal error
144+
headers:
145+
x-zooz-request-id:
146+
description: request id
147+
schema:
148+
type: string
149+
content:
150+
application/json:
151+
schema:
152+
$ref: '#/components/schemas/error_model'
153+
components:
154+
parameters:
155+
public-key:
156+
name: public-key
157+
in: header
158+
required: true
159+
example: f8948bac-8f29-4a82-8cef-704fb1e6b7ca
160+
schema:
161+
type: string
162+
schemas:
163+
error_model:
164+
required:
165+
- description
166+
- category
167+
properties:
168+
category:
169+
type: string
170+
description: Error code.
171+
description:
172+
type: string
173+
description: Error message for the developer.
174+
more_info:
175+
type: string
176+
description: 'More info about the error, can include link to the documentation.'
177+
pet:
178+
description: pet
179+
type: object
180+
oneOf:
181+
- $ref: '#/components/schemas/dog_object'
182+
- $ref: '#/components/schemas/cat_object'
183+
pets:
184+
type: array
185+
items:
186+
$ref: "#/components/schemas/pet"
187+
dog_object:
188+
type: object
189+
required:
190+
- bark
191+
properties:
192+
bark:
193+
type: string
194+
dog_multiple:
195+
type: object
196+
required:
197+
- dog_age
198+
discriminator:
199+
propertyName: model
200+
oneOf:
201+
- $ref: '#/components/schemas/small_dog'
202+
- $ref: '#/components/schemas/big_dog'
203+
properties:
204+
dog_age:
205+
type: string
206+
cat_object:
207+
type: object
208+
required:
209+
- fur
210+
properties:
211+
fur:
212+
type: string
213+
pattern: '^\d+$'
214+
215+
small_dog:
216+
type: object
217+
required:
218+
- max_length
219+
properties:
220+
max_length:
221+
type: string
222+
big_dog:
223+
type: object
224+
required:
225+
- min_length
226+
properties:
227+
min_length:
228+
type: string
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
const chai = require('chai'),
2+
expect = chai.expect;
3+
const apiSchemaBuilder = require('api-schema-builder');
4+
const SchemaEndpointResolver = require('../../src/utils/schemaEndpointResolver');
5+
const swaggerPath = 'test/openapi3/pets-parametrized.yaml';
6+
7+
describe('schemaEndpointResolver', () => {
8+
let schemas;
9+
let schemaEndpointResolver;
10+
before(() => {
11+
let schemaBuilderOptions = {buildRequests: true, buildResponses: true};
12+
return apiSchemaBuilder.buildSchema(swaggerPath, schemaBuilderOptions).then((receivedSchemas) => {
13+
schemas = receivedSchemas;
14+
});
15+
});
16+
beforeEach(() => {
17+
schemaEndpointResolver = new SchemaEndpointResolver();
18+
});
19+
20+
it('resolves exact path correctly', () => {
21+
const endpoint = schemaEndpointResolver.getMethodSchema(schemas, '/pets/:petId', 'get');
22+
expect(endpoint).to.be.ok;
23+
});
24+
25+
it('resolves using non-case-sensitive method', () => {
26+
const endpoint = schemaEndpointResolver.getMethodSchema(schemas, '/pets/:petId', 'GET');
27+
expect(endpoint).to.be.ok;
28+
});
29+
30+
it('resolves ignoring path parameter name', () => {
31+
const endpoint = schemaEndpointResolver.getMethodSchema(schemas, '/pets/:id', 'GET');
32+
expect(endpoint).to.be.ok;
33+
});
34+
35+
it('uses all three params to memoize result', () => {
36+
const endpointGet = schemaEndpointResolver.getMethodSchema(schemas, '/pets/:id', 'GET');
37+
expect(endpointGet.body).to.be.undefined;
38+
39+
const endpointPut = schemaEndpointResolver.getMethodSchema(schemas, '/pets/:id', 'PUT');
40+
expect(endpointPut.body).to.be.ok;
41+
42+
const endpointGet2 = schemaEndpointResolver.getMethodSchema(schemas, '/pets/:id', 'GET');
43+
expect(endpointGet2.body).to.be.undefined;
44+
});
45+
});

0 commit comments

Comments
 (0)