37 lines
1.2 KiB
TypeScript
37 lines
1.2 KiB
TypeScript
import { Option, Schema } from "effect"
|
|
import type { RequestSnapshot } from "./schema"
|
|
|
|
const JsonValue = Schema.fromJsonString(Schema.Unknown)
|
|
export const decodeJson = Schema.decodeUnknownOption(JsonValue)
|
|
|
|
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
value !== null && typeof value === "object" && !Array.isArray(value)
|
|
|
|
export const canonicalizeJson = (value: unknown): unknown => {
|
|
if (Array.isArray(value)) return value.map(canonicalizeJson)
|
|
if (isRecord(value)) {
|
|
return Object.fromEntries(
|
|
Object.keys(value)
|
|
.toSorted()
|
|
.map((key) => [key, canonicalizeJson(value[key])]),
|
|
)
|
|
}
|
|
return value
|
|
}
|
|
|
|
export type RequestMatcher = (incoming: RequestSnapshot, recorded: RequestSnapshot) => boolean
|
|
|
|
export const canonicalSnapshot = (snapshot: RequestSnapshot): string =>
|
|
JSON.stringify({
|
|
method: snapshot.method,
|
|
url: snapshot.url,
|
|
headers: canonicalizeJson(snapshot.headers),
|
|
body: Option.match(decodeJson(snapshot.body), {
|
|
onNone: () => snapshot.body,
|
|
onSome: canonicalizeJson,
|
|
}),
|
|
})
|
|
|
|
export const defaultMatcher: RequestMatcher = (incoming, recorded) =>
|
|
canonicalSnapshot(incoming) === canonicalSnapshot(recorded)
|