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 => 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)