-
Notifications
You must be signed in to change notification settings - Fork 338
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(router): WIP dummy router implementation and specs
SL-72
- Loading branch information
Chris Miaskowski
committed
Oct 16, 2018
1 parent
ad33b25
commit 2dc3f8b
Showing
8 changed files
with
378 additions
and
36 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,85 @@ | ||
import { Chance } from 'chance'; | ||
import { router } from '../index'; | ||
import { oneRandomHttpMethod, randomUrl, randomPath, pickSetOfHttpMethods, pickOneHttpMethod } from '@stoplight/prism-http/router/__tests__/utils'; | ||
|
||
const chance = new Chance(); | ||
|
||
describe('http router', () => { | ||
describe.skip('route()', () => { | ||
test('should return null if no resources given', async () => { | ||
const resource = await router.route({ | ||
resources: [], | ||
input: { | ||
method: oneRandomHttpMethod(), | ||
url: randomUrl() | ||
} | ||
}); | ||
|
||
expect(resource).toBeNull(); | ||
}); | ||
|
||
describe('given a resource', () => { | ||
test('should not match if no server defined', async () => { | ||
const resource = await router.route({ | ||
resources: [{ | ||
id: chance.guid(), | ||
method: oneRandomHttpMethod(), | ||
path: randomPath(), | ||
responses: [], | ||
servers: [] | ||
}], | ||
input: { | ||
method: oneRandomHttpMethod(), | ||
url: randomUrl() | ||
} | ||
}); | ||
|
||
expect(resource).toBeNull(); | ||
}); | ||
|
||
test('given a concrete matching server and unmatched methods should not match', async () => { | ||
const url = randomUrl(); | ||
const [ resourceMethod, requestMethod ] = pickSetOfHttpMethods(2); | ||
const resource = await router.route({ | ||
resources: [{ | ||
id: chance.guid(), | ||
method: resourceMethod, | ||
path: randomPath(), | ||
responses: [], | ||
servers: [{ | ||
url: url.toString(), | ||
}] | ||
}], | ||
input: { | ||
method: requestMethod, | ||
url | ||
} | ||
}); | ||
|
||
expect(resource).toBeNull(); | ||
}); | ||
|
||
test('given a concrete matching server and matched methods and unmatched path should not match', async () => { | ||
const url = randomUrl(); | ||
const method = pickOneHttpMethod(); | ||
const resource = await router.route({ | ||
resources: [{ | ||
id: chance.guid(), | ||
method, | ||
path: randomPath(), | ||
responses: [], | ||
servers: [{ | ||
url: url.toString(), | ||
}] | ||
}], | ||
input: { | ||
method, | ||
url | ||
} | ||
}); | ||
|
||
expect(resource).toBeNull(); | ||
}); | ||
}); | ||
}); | ||
}); |
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,143 @@ | ||
import { Chance } from 'chance'; | ||
import { matchPath } from "@stoplight/prism-http/router/matchPath"; | ||
import { randomPath } from "@stoplight/prism-http/router/__tests__/utils"; | ||
|
||
const chance = new Chance(); | ||
|
||
describe('matchPath()', () => { | ||
|
||
test('request path must start with a slash or throw error', () => { | ||
const requestPath = randomPath({ leadingSlash: false }); | ||
const operationPath = randomPath({ leadingSlash: true }); | ||
expect(() => matchPath(requestPath, { path: operationPath })).toThrow('The request path must start with a slash.'); | ||
}); | ||
|
||
test('option path must start with a slash or throw error', () => { | ||
const requestPath = randomPath({ leadingSlash: true }); | ||
const operationPath = randomPath({ leadingSlash: false }); | ||
expect(() => matchPath(requestPath, { path: operationPath })).toThrow('The operation path must start with a slash.'); | ||
}); | ||
|
||
test('root path should match another root path', () => { | ||
const path = '/'; | ||
expect(matchPath(path, { path })).toBeTruthy(); | ||
}); | ||
|
||
test('any concrete path should match an equal concrete path', () => { | ||
// e.g. /a/b/c should match /a/b/c | ||
const path = randomPath({ | ||
pathFragments: chance.natural({ min: 1, max: 6 }), | ||
includeTemplates: false, | ||
}); | ||
|
||
expect(matchPath(path, { path }).toBeTruthy(); | ||
}); | ||
|
||
test('any conrecte request path should match same length templated path', () => { | ||
// e.g. /a/b/c should match /a/{x}/c | ||
const pathFragments = chance.natural({ min: 1, max: 6 }); | ||
const trailingSlash = chance.bool(); | ||
const requestPath = randomPath({ | ||
pathFragments, | ||
includeTemplates: false, | ||
trailingSlash | ||
}); | ||
const operationPath = randomPath({ | ||
pathFragments, | ||
includeTemplates: true, | ||
trailingSlash | ||
}); | ||
|
||
expect(matchPath(requestPath, { path: operationPath })).toBeTruthy(); | ||
}); | ||
|
||
test('none request path should not match path with less fragments', () => { | ||
// e.g. /a/b/c should not match /a/b | ||
// e.g. /a/b/c should not match /{a}/b | ||
const trailingSlash = chance.bool(); | ||
const requestPath = randomPath({ | ||
pathFragments: chance.natural({ min: 4, max: 6 }), | ||
includeTemplates: false, | ||
trailingSlash, | ||
}); | ||
const operationPath = randomPath({ | ||
pathFragments: chance.natural({ min: 1, max: 3 }), | ||
trailingSlash, | ||
}); | ||
|
||
expect(matchPath(requestPath, { path: operationPath })).toBeFalsy(); | ||
}); | ||
|
||
test('none request path should not match concrete path with more fragments', () => { | ||
// e.g. /a/b should not match /a/b/c | ||
// e.g. /a/b/ should not match /a/b/c | ||
const requestPath = randomPath({ | ||
pathFragments: chance.natural({ min: 4, max: 6 }), | ||
includeTemplates: false, | ||
}); | ||
const operationPath = randomPath({ | ||
pathFragments: chance.natural({ min: 1, max: 3 }), | ||
includeTemplates: false, | ||
}); | ||
|
||
expect(matchPath(requestPath, { path: operationPath })).toBeFalsy(); | ||
}); | ||
|
||
test('reqest path should match a templated path and resolve variables', () => { | ||
expect(matchPath('/a'), { '/{a}'}).toEqual([ | ||
{ name: 'a', value: 'a' } | ||
]); | ||
|
||
expect(matchPath('/a/b'), { '/{a}/{b}'}).toEqual([ | ||
{ name: 'a', value: 'a' }, | ||
{ name: 'b', value: 'b' }, | ||
]); | ||
|
||
expect(matchPath('/a/b'), { '/a/{b}'}).toEqual([ | ||
{ name: 'b', value: 'b' }, | ||
]); | ||
}); | ||
|
||
test('request path should match a template path and resolve undefined variables', () => { | ||
expect(matchPath('/'), { '/{a}'}).toEqual([ | ||
{ name: 'a', value: undefined }, | ||
]); | ||
|
||
expect(matchPath('//'), { '/{a}/'}).toEqual([ | ||
{ name: 'a', value: undefined }, | ||
]); | ||
|
||
expect(matchPath('//b'), { '/{a}/{b}'}).toEqual([ | ||
{ name: 'a', value: undefined }, | ||
{ name: 'b', value: 'b' }, | ||
]); | ||
|
||
expect(matchPath('/a/'), { '/{a}/{b}'}).toEqual([ | ||
{ name: 'a', value: 'a' }, | ||
{ name: 'b', value: undefined }, | ||
]); | ||
|
||
expect(matchPath('//'), { '/{a}/{b}'}).toEqual([ | ||
{ name: 'a', value: undefined }, | ||
{ name: 'b', value: undefined }, | ||
]); | ||
}); | ||
|
||
test('none path should match templated operation with more path fragments', () => { | ||
// e.g. `/a/b` should not match /{x}/{y}/{z} | ||
// e.g. `/a` should not match /{x}/{y}/{z} | ||
const trailingSlash = chance.bool(); | ||
const requestPath = randomPath({ | ||
pathFragments: chance.natural({ min: 1, max: 3 }), | ||
includeTemplates: false, | ||
trailingSlash: false, | ||
}); | ||
const operationPath = randomPath({ | ||
pathFragments: chance.natural({ min: 4, max: 6 }), | ||
includeTemplates: false, | ||
trailingSlash: false, | ||
}); | ||
|
||
expect(matchPath(requestPath, { path: operationPath })).toBeFalsy(); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
import { Chance } from 'chance'; | ||
import { IHttpMethod } from "@stoplight/prism-http/types"; | ||
|
||
const chance = new Chance(); | ||
const httpMethods = ['get', 'put', 'post', 'delete', 'options', 'head', 'patch', 'trace']; | ||
|
||
export function pickOneHttpMethod(): IHttpMethod { | ||
return chance.pickone(httpMethods) as IHttpMethod; | ||
} | ||
|
||
export function pickSetOfHttpMethods(count: number = 2): IHttpMethod[] { | ||
return chance.unique(pickOneHttpMethod, count) as IHttpMethod[]; | ||
} | ||
|
||
export function randomUrl(): URL { | ||
return new URL(chance.url()); | ||
} | ||
|
||
export function randomArray<T>(itemGenerator: () => T, length: number = 1): T[] { | ||
return new Array(length).fill(null).map(itemGenerator); | ||
} | ||
|
||
const defaultRandomPathOptions = { | ||
pathFragments: 3, | ||
includeTemplates: true, | ||
leadingSlash: true | ||
}; | ||
|
||
interface RandomPathOptions { | ||
pathFragments?: number; | ||
includeTemplates?: boolean; | ||
trailingSlash?: boolean; | ||
leadingSlash?: boolean; | ||
} | ||
|
||
export function randomPath(opts: RandomPathOptions = defaultRandomPathOptions): string { | ||
opts = Object.assign(opts, defaultRandomPathOptions); | ||
const randomPathFragments = randomArray( | ||
() => (opts.includeTemplates && chance.coin() === 'heads') ? `{${chance.word()}}` : chance.word(), | ||
opts.pathFragments | ||
); | ||
let leadingSlash = chance.pickone(['/', '']); | ||
let trailingSlash = chance.pickone(['/', '']); | ||
if (opts.leadingSlash !== undefined) { | ||
leadingSlash = opts.leadingSlash ? '/' : ''; | ||
} | ||
if (opts.trailingSlash !== undefined) { | ||
trailingSlash = opts.trailingSlash ? '/' : ''; | ||
} | ||
return `${leadingSlash}${randomPathFragments.join('/')}${trailingSlash}` | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,11 +1,76 @@ | ||
import { types } from '@stoplight/prism-core'; | ||
import { IHttpOperation } from '@stoplight/types'; | ||
import { IHttpOperation, IServer } from '@stoplight/types'; | ||
|
||
import * as t from '../types'; | ||
|
||
type Nullable<T> = T | null; | ||
|
||
export const router: types.IRouter<IHttpOperation, t.IHttpRequest, t.IHttpConfig> = { | ||
route: async ({ resources, input, config }) => { | ||
// given input and config, find and return the matching resource | ||
throw new Error('Method not implemented.'); | ||
throw new Error('Not implemented yet'); | ||
}, | ||
}; | ||
|
||
function route(operations: IHttpOperation[], request: t.IHttpRequest) { | ||
return disambiguate(<IMatch[]>operations | ||
.map(operation => match(request, operation)) | ||
.filter(match => match !== null) | ||
); | ||
} | ||
|
||
interface IMatch { | ||
operation: IHttpOperation; | ||
possibleMatches: [IServerMatch, IPathMatch][]; | ||
} | ||
|
||
function match(request: t.IHttpRequest, operation: IHttpOperation): Nullable<IMatch> { | ||
if (!matchByMethod(request, operation)) { | ||
return null; | ||
} | ||
return matchByUrl(request, operation); | ||
} | ||
|
||
function matchByUrl(request: t.IHttpRequest, operation: IHttpOperation): Nullable<IMatch> { | ||
const requestUrl = request.url; | ||
const { path, servers } = operation; | ||
const possibleMatches = <[IServerMatch, IPathMatch][]>servers | ||
.map(server => { | ||
const serverMatch = matchServer(server, requestUrl); | ||
if (!serverMatch) return null; | ||
const pathMatch = matchPath(serverMatch.path, operation); | ||
if (!pathMatch) return null; | ||
return [serverMatch, pathMatch]; | ||
}) | ||
.filter(match => match !== null); | ||
|
||
if (!possibleMatches.length) return null; | ||
|
||
return { | ||
operation, | ||
possibleMatches | ||
} | ||
} | ||
|
||
|
||
|
||
interface IServerMatch { | ||
baseUrl: string; | ||
path: string; | ||
variables: {}; | ||
} | ||
|
||
function matchPath(requestPath: string, operation: IHttpOperation): IPathMatch { | ||
throw new Error('not implemented'); | ||
} | ||
|
||
function matchServer(server: IServer, requestUrl: URL): IServerMatch { | ||
throw new Error('not implemneted'); | ||
} | ||
|
||
function matchByMethod(request: t.IHttpRequest, operation: IHttpOperation): boolean { | ||
throw new Error('Not implemented'); | ||
} | ||
|
||
function disambiguate(matches: IMatch[]): IMatch { | ||
throw new Error('Not implemented'); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
import { IHttpOperation } from "@stoplight/types/http"; | ||
|
||
// returns true if matched concrete | ||
// returns false if not matched | ||
// returns path param values if matched templated | ||
export function matchPath(requestPath: string, operation: Partial<IHttpOperation>): boolean | [ { name: string, value: string } ] { | ||
throw new Error('not implemented'); | ||
} |
Oops, something went wrong.