Skip to content

Commit

Permalink
feat(router): WIP dummy router implementation and specs
Browse files Browse the repository at this point in the history
SL-72
  • Loading branch information
Chris Miaskowski committed Oct 16, 2018
1 parent ad33b25 commit 2dc3f8b
Show file tree
Hide file tree
Showing 8 changed files with 378 additions and 36 deletions.
85 changes: 85 additions & 0 deletions packages/http/src/router/__tests__/index.spec.ts
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();
});
});
});
});
5 changes: 0 additions & 5 deletions packages/http/src/router/__tests__/index.ts

This file was deleted.

143 changes: 143 additions & 0 deletions packages/http/src/router/__tests__/matchPath.spec.ts
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();
});
});
51 changes: 51 additions & 0 deletions packages/http/src/router/__tests__/utils.ts
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}`
}
71 changes: 68 additions & 3 deletions packages/http/src/router/index.ts
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');
}
8 changes: 8 additions & 0 deletions packages/http/src/router/matchPath.ts
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');
}
Loading

0 comments on commit 2dc3f8b

Please sign in to comment.