Skip to content

Commit

Permalink
[WIP] Add diff formatting
Browse files Browse the repository at this point in the history
  • Loading branch information
joshuajaco committed Mar 10, 2024
1 parent ab0d4c5 commit 8e434fe
Show file tree
Hide file tree
Showing 5 changed files with 197 additions and 15 deletions.
86 changes: 78 additions & 8 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,15 @@
"lint:fix": "npm run lint -- --fix",
"prepublishOnly": "npm run build",
"test": "nyc -r=lcov -r=text node --trace-warnings -r ts-node/register --test \"**/*.test.ts\"",
"test2": "node --trace-warnings -r ts-node/register --test tests/Formatter.test.ts",
"typecheck": "tsc"
},
"dependencies": {
"@types/express": "^4.17.21",
"body-parser": "^1.20.2",
"deep-equal": "^2.2.3",
"express": "^4.18.2"
"express": "^4.18.2",
"jest-diff": "^29.7.0"
},
"devDependencies": {
"@types/deep-equal": "^1.0.4",
Expand Down
108 changes: 108 additions & 0 deletions src/Formatter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import {
matchBody,
Matcher,
MatcherObj,
matchHeaders,
matchMethod,
matchPath,
matchQuery,
Request,
} from "./matchRequest";
import { diff } from "jest-diff";

export function formatMatcher(matcher: Matcher) {
if (typeof matcher === "function") return matcher.toString();
return JSON.stringify(matcher, null, 2);
}

export function formatDiffs(matcher: Matcher, requests: Request[]) {
return requests
.map((request) => [request, score(matcher, request)] as const)
.toSorted(([, scoreA], [, scoreB]) => scoreB - scoreA)
.map(
([request]) =>
`${request.method} ${request.path}:\n${formatDiff(matcher, request)}`,
)
.join("\n\n");
}

function formatDiff(matcher: Matcher, request: Request) {
if (typeof matcher === "function") throw "ay";

const req = sanitizeRequest(request);

const actual: MatcherObj = {};

if (matcher.method) actual.method = req.method;
if (matcher.path) actual.path = req.path;

if (matcher.query) {
actual.query =
req.query && filterKeys(req.query, Object.keys(matcher.query));
}

if (matcher.headers) {
actual.headers =
req.headers && filterKeys(req.headers, Object.keys(matcher.headers));
}

if (matcher.body) actual.body = req.body;

return diff(matcher, actual);
}

function score(matcher: Matcher, request: Request): number {
if (typeof matcher === "function") throw "ay";

let maxPoints = 0;
let points = 0;

if (matcher.method) {
maxPoints += 1;
if (matchMethod(matcher, request)) points += 1;
}

if (matcher.path) {
maxPoints += 3;
if (matchPath(matcher, request)) points += 3;
}

if (matcher.query) {
maxPoints += 3;
if (matchQuery(matcher, request)) points += 3;
}

if (matcher.headers) {
maxPoints += 2;
if (matchHeaders(matcher, request)) points += 2;
}

if (matcher.body) {
maxPoints += 4;
if (matchBody(matcher, request)) points += 4;
}

return (points / maxPoints) * 100;
}

function sanitizeRequest(request: Request): MatcherObj {
return {
method: request.method,
path: request.path,
query: request.query,
headers: request.headers,
body: request.body ? tryParse(request.body.toString()) : undefined,
};
}

function tryParse(body: string): string | Record<never, unknown> {
try {
return JSON.parse(body);
} catch {
return body;
}
}

function filterKeys<T>(obj: T, keys: Array<keyof T>): Partial<T> {
return Object.fromEntries(keys.map((key) => [key, obj[key]])) as Partial<T>;
}
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ export type {
Call,
Options,
} from "./MockServer";
export { formatMatcher, formatDiffs } from "./Formatter";
13 changes: 7 additions & 6 deletions src/matchRequest.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type express from "express";
import deepEqual from "deep-equal";
import type http from "node:http";

/**
* request the server was called with
Expand Down Expand Up @@ -30,7 +31,7 @@ export type MatcherObj = {
* Headers explicitly set to `undefined` will not match when provided
* @see [Documentation]{@link https://github.com/joshuajaco/mocaron#matcherobj}
*/
headers?: Record<string, string | undefined>;
headers?: http.IncomingHttpHeaders;
/**
* body to match against -
* If an `object` is given it will be compared to the request body parsed as JSON
Expand Down Expand Up @@ -64,13 +65,13 @@ export function matchRequest(matcher: Matcher, req: Request): boolean {
);
}

function matchMethod(matcher: MatcherObj, req: Request) {
export function matchMethod(matcher: MatcherObj, req: Request) {
return (
!matcher.method || matcher.method.toLowerCase() === req.method.toLowerCase()
);
}

function matchPath(matcher: MatcherObj, req: Request) {
export function matchPath(matcher: MatcherObj, req: Request) {
return (
!matcher.path ||
(matcher.path instanceof RegExp
Expand All @@ -79,21 +80,21 @@ function matchPath(matcher: MatcherObj, req: Request) {
);
}

function matchQuery(matcher: MatcherObj, req: Request) {
export function matchQuery(matcher: MatcherObj, req: Request) {
if (!matcher.query) return true;
return Object.entries(matcher.query).every(([k, v]) =>
deepEqual(req.query[k], v, { strict: true }),
);
}

function matchHeaders(matcher: MatcherObj, req: Request) {
export function matchHeaders(matcher: MatcherObj, req: Request) {
if (!matcher.headers) return true;
return Object.entries(matcher.headers).every(
([k, v]) => req.headers[k.toLowerCase()] === v,
);
}

function matchBody(matcher: MatcherObj, req: Request) {
export function matchBody(matcher: MatcherObj, req: Request) {
if (matcher.body == null) return true;

if (!req.body) return false;
Expand Down

0 comments on commit 8e434fe

Please sign in to comment.