Skip to content

Commit

Permalink
Merge pull request #729 from wheresrhys/rhys/combine-calllog-normaliz…
Browse files Browse the repository at this point in the history
…edrequest

Rhys/combine calllog normalizedrequest
  • Loading branch information
wheresrhys authored Jul 21, 2024
2 parents f3b1423 + 9745435 commit 1ba3aa7
Show file tree
Hide file tree
Showing 7 changed files with 53 additions and 58 deletions.
11 changes: 8 additions & 3 deletions packages/core/src/CallHistory.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,22 @@
/** @typedef {import('./RequestUtils').NormalizedRequestOptions} NormalizedRequestOptions */
/** @typedef {import('./Matchers').RouteMatcher} RouteMatcher */
/** @typedef {import('./FetchMock').FetchMockConfig} FetchMockConfig */
import { normalizeRequest } from './RequestUtils.js';
import { createCallLog } from './RequestUtils.js';
import { isUrlMatcher } from './Matchers.js';
import Route from './Route.js';
import Router from './Router.js';

/**
* @typedef CallLog
* @property {any[]} arguments
* @property {string} url
* @property {NormalizedRequestOptions} options
* @property {Request} [request]
* @property {AbortSignal} [signal]
* @property {Route} [route]
* @property {Response} [response]
* @property {Object.<string, string>} [expressParameters]
* @property {Object.<string, string>} [queryParameters]
* @property {Promise<any>[]} pendingPromises
*/

Expand Down Expand Up @@ -44,7 +49,7 @@ const isMatchedOrUnmatched = (filter) =>
class CallHistory {
/**
* @param {FetchMockConfig} globalConfig
* @param router
* @param {Router} router
*/
constructor(globalConfig, router) {
/** @type {CallLog[]} */
Expand Down Expand Up @@ -137,7 +142,7 @@ class CallHistory {
});

calls = calls.filter(({ url, options }) => {
return matcher(normalizeRequest(url, options, this.config.Request));
return matcher(createCallLog(url, options, this.config.Request));
});

return calls;
Expand Down
17 changes: 7 additions & 10 deletions packages/core/src/FetchMock.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,18 +44,18 @@ const defaultConfig = {
* @property {function(string | Request, RequestInit): Promise<Response>} fetchHandler
* @property {function(any,any,any): FetchMock} route
* @property {function(RouteResponse=): FetchMock} catch
* @property {function(boolean): Promise<any>} flush
* @property {function(RouteName[]=): boolean} done
* @property {function(MatcherDefinition):void} defineMatcher
* @property {function(object): void} removeRoutes
* @property {function():void} clearHistory
*/

const defaultRouter = new Router(defaultConfig);

/** @type {FetchMockCore} */
const FetchMock = {
config: defaultConfig,
router: new Router(defaultConfig),
callHistory: new CallHistory(defaultConfig, this.router),
router: defaultRouter,
callHistory: new CallHistory(defaultConfig, defaultRouter),
createInstance() {
const instance = Object.create(FetchMock);
instance.config = { ...this.config };
Expand All @@ -75,18 +75,15 @@ const FetchMock = {
*/
fetchHandler(requestInput, requestInit) {
// TODO move into router
const normalizedRequest = requestUtils.normalizeRequest(
const callLog = requestUtils.createCallLog(
requestInput,
requestInit,
this.config.Request,
);

/** @type {Promise<any>[]} */
const pendingPromises = [];
const callLog = { ...normalizedRequest, pendingPromises };
this.callHistory.recordCall(callLog);
const responsePromise = this.router.execute(callLog, normalizedRequest);
pendingPromises.push(responsePromise);
const responsePromise = this.router.execute(callLog);
callLog.pendingPromises.push(responsePromise);
return responsePromise;
},
/**
Expand Down
10 changes: 4 additions & 6 deletions packages/core/src/Matchers.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
//@type-check
/** @typedef {import('./Route').RouteConfig} RouteConfig */
/** @typedef {import('./RequestUtils').NormalizedRequest} NormalizedRequest */
/** @typedef {import('./CallHistory').CallLog} CallLog */
import glob from 'globrex';
import * as regexparam from 'regexparam';
import querystring from 'querystring';
Expand Down Expand Up @@ -30,9 +30,8 @@ export const isUrlMatcher = (matcher) =>
export const isFunctionMatcher = (matcher) => typeof matcher === 'function';

/** @typedef {string | RegExp | URL} RouteMatcherUrl */
/** @typedef {function(string): boolean} UrlMatcher */
/** @typedef {function(string): UrlMatcher} UrlMatcherGenerator */
/** @typedef {function(NormalizedRequest): boolean} RouteMatcherFunction */
/** @typedef {function(string): RouteMatcherFunction} UrlMatcherGenerator */
/** @typedef {function(CallLog): boolean} RouteMatcherFunction */
/** @typedef {function(RouteConfig): RouteMatcherFunction} MatcherGenerator */
/** @typedef {RouteMatcherUrl | RouteMatcherFunction} RouteMatcher */

Expand Down Expand Up @@ -156,8 +155,7 @@ const getParamsMatcher = ({ params: expectedParams, url: matcherUrl }) => {
const getBodyMatcher = (route) => {
const { body: expectedBody } = route;

return ({ url, options: { body, method = 'get' } }) => {
console.log({ url, body, method });
return ({ options: { body, method = 'get' } }) => {
if (method.toLowerCase() === 'get') {
// GET requests don’t send a body so the body matcher should be ignored for them
return true;
Expand Down
24 changes: 12 additions & 12 deletions packages/core/src/RequestUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,7 @@ const protocolRelativeUrlRX = new RegExp('^//', 'i');
*/

/** @typedef {RequestInit | (RequestInit & DerivedRequestOptions) } NormalizedRequestOptions */
/**
* @typedef NormalizedRequest
* @property {string} url
* @property {NormalizedRequestOptions} options
* @property {Request} [request]
* @property {AbortSignal} [signal]
*/
/** @typedef {import('./CallHistory').CallLog} CallLog */

/**
*
Expand Down Expand Up @@ -51,9 +45,11 @@ const isRequest = (urlOrRequest, Request) =>
* @param {string|Request} urlOrRequest
* @param {RequestInit} options
* @param {typeof Request} Request
* @returns {NormalizedRequest}
* @returns {CallLog}
*/
export function normalizeRequest(urlOrRequest, options = {}, Request) {
export function createCallLog(urlOrRequest, options, Request) {
/** @type {Promise<any>[]} */
const pendingPromises = [];
if (isRequest(urlOrRequest, Request)) {
/** @type {NormalizedRequestOptions} */
const derivedOptions = {
Expand All @@ -67,13 +63,15 @@ export function normalizeRequest(urlOrRequest, options = {}, Request) {
if (urlOrRequest.headers) {
derivedOptions.headers = normalizeHeaders(urlOrRequest.headers);
}
const normalizedRequestObject = {
const callLog = {
arguments: [urlOrRequest, options],
url: normalizeUrl(urlOrRequest.url),
options: Object.assign(derivedOptions, options),
request: urlOrRequest,
signal: (options && options.signal) || urlOrRequest.signal,
pendingPromises,
};
return normalizedRequestObject;
return callLog;
}
if (
typeof urlOrRequest === 'string' ||
Expand All @@ -82,9 +80,11 @@ export function normalizeRequest(urlOrRequest, options = {}, Request) {
(typeof urlOrRequest === 'object' && 'href' in urlOrRequest)
) {
return {
arguments: [urlOrRequest, options],
url: normalizeUrl(urlOrRequest),
options,
options: options || {},
signal: options && options.signal,
pendingPromises,
};
}
if (typeof urlOrRequest === 'object') {
Expand Down
7 changes: 4 additions & 3 deletions packages/core/src/Route.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { builtInMatchers } from './Matchers';
import statusTextMap from './StatusTextMap';

/** @typedef {import('./Matchers').RouteMatcher} RouteMatcher */
/** @typedef {import('./CallHistory').CallLog} CallLog */
/** @typedef {import('./Matchers').RouteMatcherFunction} RouteMatcherFunction */
/** @typedef {import('./Matchers').RouteMatcherUrl} RouteMatcherUrl */
/** @typedef {import('./Matchers').MatcherDefinition} MatcherDefinition */
Expand All @@ -28,7 +29,7 @@ import statusTextMap from './StatusTextMap';
/** @typedef {RouteResponseConfig | object} RouteResponseObjectData */
/** @typedef {Response | number| string | RouteResponseObjectData } RouteResponseData */
/** @typedef {Promise<RouteResponseData>} RouteResponsePromise */
/** @typedef {function(string, RequestInit, Request=): (RouteResponseData|RouteResponsePromise)} RouteResponseFunction */
/** @typedef {function(CallLog): (RouteResponseData|RouteResponsePromise)} RouteResponseFunction */
/** @typedef {RouteResponseData | RouteResponsePromise | RouteResponseFunction} RouteResponse*/

/** @typedef {string} RouteName */
Expand Down Expand Up @@ -161,8 +162,8 @@ class Route {
}
const originalMatcher = this.matcher;
let timesLeft = this.config.repeat;
this.matcher = (url, options, request) => {
const match = timesLeft && originalMatcher(url, options, request);
this.matcher = (callLog) => {
const match = timesLeft && originalMatcher(callLog);
if (match) {
timesLeft--;
return true;
Expand Down
38 changes: 16 additions & 22 deletions packages/core/src/Router.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import { isUrlMatcher, isFunctionMatcher } from './Matchers.js';
/** @typedef {import('./Matchers').RouteMatcher} RouteMatcher */
/** @typedef {import('./FetchMock').FetchMockConfig} FetchMockConfig */
/** @typedef {import('./FetchMock')} FetchMock */
/** @typedef {import('./RequestUtils').NormalizedRequest} NormalizedRequest */
/** @typedef {import('./CallHistory').CallLog} CallLog */

/** @typedef {'body' |'headers' |'throws' |'status' |'redirectUrl' } ResponseConfigProp */
Expand Down Expand Up @@ -95,11 +94,10 @@ function shouldSendAsObject(responseInput) {
}

/**
* @param {RouteResponse} response
* @param {NormalizedRequest} normalizedRequest
* @param {CallLog} callLog
* @returns
*/
const resolveUntilResponseConfig = async (response, normalizedRequest) => {
const resolveUntilResponseConfig = async (callLog) => {
// We want to allow things like
// - function returning a Promise for a response
// - delaying (using a timeout Promise) a function's execution to generate
Expand All @@ -108,9 +106,11 @@ const resolveUntilResponseConfig = async (response, normalizedRequest) => {
// or vice versa. So to keep it DRY, and flexible, we keep trying until we
// have something that looks like neither Promise nor function
//eslint-disable-next-line no-constant-condition
let response = callLog.route.config.response;
// eslint-disable-next-line no-constant-condition
while (true) {
if (typeof response === 'function') {
response = response(normalizedRequest);
response = response(callLog);
} else if (isPromise(response)) {
response = await response; // eslint-disable-line no-await-in-loop
} else {
Expand All @@ -120,6 +120,8 @@ const resolveUntilResponseConfig = async (response, normalizedRequest) => {
};

export default class Router {
/** @type {Route[]} */
routes = [];
/**
* @param {FetchMockConfig} fetchMockConfig
* @param {object} [inheritedRoutes]
Expand All @@ -144,42 +146,38 @@ export default class Router {

/**
* @param {CallLog} callLog
* @param {NormalizedRequest} normalizedRequest
* @returns {Promise<Response>}
*/
execute(callLog, normalizedRequest) {
execute(callLog) {
// TODO make abort vs reject neater
return new Promise(async (resolve, reject) => {
const { url, options, request, pendingPromises } = callLog;
if (normalizedRequest.signal) {
if (callLog.signal) {
const abort = () => {
// TODO may need to bring that flushy thing back.
// Add a test to combvine flush with abort
// done();
reject(new DOMException('The operation was aborted.', 'AbortError'));
};
if (normalizedRequest.signal.aborted) {
if (callLog.signal.aborted) {
abort();
}
normalizedRequest.signal.addEventListener('abort', abort);
callLog.signal.addEventListener('abort', abort);
}
console.log(this.routes);
if (this.needsToReadBody(request)) {
options.body = await options.body;
}

const routesToTry = this.fallbackRoute
? [...this.routes, this.fallbackRoute]
: this.routes;
const route = routesToTry.find((route) =>
route.matcher(normalizedRequest),
);
const route = routesToTry.find((route) => route.matcher(callLog));

if (route) {
try {
callLog.route = route;
const { response, responseOptions, responseInput } =
await this.generateResponse(route, callLog);
await this.generateResponse(callLog);
const observableResponse = this.createObservableResponse(
response,
responseOptions,
Expand All @@ -206,16 +204,12 @@ export default class Router {

/**
*
* @param {Route} route
* @param {CallLog} callLog
* @returns {Promise<{response: Response, responseOptions: ResponseInit, responseInput: RouteResponseConfig}>}
*/
// eslint-disable-next-line class-methods-use-this
async generateResponse(route, callLog) {
const responseInput = await resolveUntilResponseConfig(
route.config.response,
callLog,
);
async generateResponse(callLog) {
const responseInput = await resolveUntilResponseConfig(callLog);

// If the response is a pre-made Response, respond with it
if (responseInput instanceof Response) {
Expand All @@ -233,7 +227,7 @@ export default class Router {
throw responseConfig.throws;
}

return route.constructResponse(responseConfig);
return callLog.route.constructResponse(responseConfig);
}
/**
*
Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/__tests__/Matchers/body.test.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { describe, expect, it } from 'vitest';
import Route from '../../Route.js';
import Router from '../../Router.js';
import { normalizeRequest } from '../../RequestUtils.js';
import { createCallLog } from '../../RequestUtils.js';
describe('body matching', () => {
//TODO add a test for matching an asynchronous body
it('should not match if no body provided in request', () => {
Expand Down Expand Up @@ -45,7 +45,7 @@ describe('body matching', () => {
Response,
});
const router = new Router({ Request, Headers }, { routes: [route] });
const normalizedRequest = normalizeRequest(
const normalizedRequest = createCallLog(
new Request('http://a.com/', {
method: 'POST',
body: JSON.stringify({ foo: 'bar' }),
Expand Down

0 comments on commit 1ba3aa7

Please sign in to comment.