Skip to content

Commit d1bc2a6

Browse files
docs(spec-api): Add JSDoc for public APIs [skip chg] (#12236)
Mock authors need to know how scenarios, request coverage, matching, validation, and response helpers interact, including what body representations helpers actually accept and return. Document the public spec API together with its resolver, coverage, and format-specific limitations. No matching or mock-server behavior changes. Consolidates [12246](#12246). Tracked in #12228.
1 parent 4a69775 commit d1bc2a6

11 files changed

Lines changed: 327 additions & 66 deletions

File tree

‎packages/spec-api/src/expectation.ts‎

Lines changed: 34 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -13,67 +13,72 @@ import type { CollectionFormat, RequestExt, Resolver, ResolverConfig } from "./t
1313
import { ValidationError } from "./validation-error.js";
1414

1515
/**
16-
* Class containing all the expectations that can be run on the request.
16+
* Assertions against one incoming request. A mismatch throws {@link ValidationError}
17+
* with expected and actual values; successful assertions return normally.
1718
*/
1819
export class RequestExpectation {
20+
/** Binds assertions to the supplied request without copying its data. */
1921
public constructor(private originalRequest: RequestExt) {}
2022
/**
21-
* Expect the raw body of the request to match the given string.
22-
* @param rawBody Raw request body.
23-
* @throws {ValidationError} if there is an error.
23+
* Requires strict equality of raw strings or Buffer bytes, without converting between them.
24+
* An undefined expectation requires an empty raw payload.
25+
* @throws {ValidationError} when the payload differs.
2426
*/
2527
public rawBodyEquals(expectedRawBody: string | Buffer | undefined): void {
2628
validateRawBodyEquals(this.originalRequest, expectedRawBody);
2729
}
2830

2931
/**
30-
* Expect the body of the request to match the given object.
31-
* @param rawBody Raw request body.
32-
* @throws {ValidationError} if there is an error.
32+
* Compares the parsed body using {@link matchValues}, including nested value matchers.
33+
* A null or undefined expectation instead requires an empty raw payload.
34+
* @throws {ValidationError} when the payload differs.
3335
*/
3436
public bodyEquals(expectedRawBody: unknown | undefined): void {
3537
validateBodyEquals(this.originalRequest, expectedRawBody);
3638
}
3739

3840
/**
39-
* Expect the coerced body of the request to match the given object.
40-
* @param rawBody Raw request body.
41-
* @throws {ValidationError} if there is an error.
41+
* JSON-round-trips the actual body and normalizes UTC datetime strings before
42+
* matcher-aware comparison. Removes 3–7 fractional digits and replaces zero offsets
43+
* with `Z`; the expected value is not normalized.
44+
* A null or undefined expectation requires an empty raw payload.
45+
* @throws {ValidationError} when the payload differs.
4246
*/
4347
public coercedBodyEquals(expectedRawBody: unknown | undefined): void {
4448
validateCoercedDateBodyEquals(this.originalRequest, expectedRawBody);
4549
}
4650

4751
/**
48-
* Expect the body of the request to be empty.
49-
* @throws {ValidationError} if there is an error.
52+
* Requires an empty raw body and rejects a nonempty parsed Buffer.
53+
* Other parsed values do not determine emptiness.
54+
* @throws {ValidationError} when a body is present.
5055
*/
5156
public bodyEmpty(): void {
5257
validateBodyEmpty(this.originalRequest);
5358
}
5459

5560
/**
56-
* Expect the body of the request to be not empty.
61+
* Requires a nonempty raw payload, falling back to the parsed Buffer when rawBody is empty.
5762
* @throws {ValidationError} if there is an error.
5863
*/
5964
public bodyNotEmpty(): void {
6065
validateBodyNotEmpty(this.originalRequest);
6166
}
6267

6368
/**
64-
* Expect the header of the request contains the expected key/value pair
65-
* @param headerName Key expected in header
66-
* @param expectedValue Values expected in header
67-
* @throws {ValidationError} if there is an error.
69+
* Requires the parsed header value to equal the expected string exactly.
70+
* Use a lowercase header name; other headers are not checked.
71+
* @throws {ValidationError} when the header is absent or differs.
6872
*/
6973
public containsHeader(headerName: string, expectedValue: string): void {
7074
validateHeader(this.originalRequest, headerName, expectedValue);
7175
}
7276

7377
/**
74-
* Expect the query string of the request contains the expected name/value pair.
75-
* @param paramName Name of the query parameter.
76-
* @param expectedValue Value expected of the query parameter.
78+
* Requires a query parameter to equal the expected string or encoded collection.
79+
* For arrays, specify `multi` for repeated parameters or a delimiter format;
80+
* array element order is significant. Other query parameters are not checked.
81+
* @throws {ValidationError} when the parameter is absent or differs.
7782
*/
7883
public containsQueryParam(
7984
paramName: string,
@@ -84,9 +89,10 @@ export class RequestExpectation {
8489
}
8590

8691
/**
87-
* Check if two requests are equal
88-
* @param actual Actual value
89-
* @param expected Expected value
92+
* Compares arbitrary values using {@link matchValues}, accepting matchers in the
93+
* expected tree. The failure message includes the first mismatching value's path.
94+
* @param message Prefix for the mismatch detail.
95+
* @throws {ValidationError} when the values do not match.
9096
*/
9197
public deepEqual(actual: unknown, expected: unknown, message = "Values not deep equal"): void {
9298
const result = matchValues(actual, expected);
@@ -96,12 +102,11 @@ export class RequestExpectation {
96102
}
97103

98104
/**
99-
* Expect the body of the request to be semantically equivalent to the provided XML.
100-
* Accepts a plain string or a Resolver (e.g. from `xml\`...\``).
101-
* When a Resolver with matchers is provided, matcher-aware comparison is used.
102-
* The XML declaration prefix will automatically be added.
103-
* @param expectedBody expected XML body as a string or Resolver.
104-
* @param config resolver config (required when expectedBody is a Resolver).
105+
* Compares parsed XML using string-valued tag content. Plain strings have an XML declaration
106+
* added; Resolvers supply serialized XML, with template matchers recovered through getMatchers.
107+
* @param expectedBody XML string or Resolver. The xml helper returns a MockBody; for tagged
108+
* XML, narrow its rawContent to a Resolver and pass that, not the body wrapper.
109+
* @param config Context for resolving dynamic values; defaults to an empty base URL.
105110
* @throws {ValidationError} if there is an error.
106111
*/
107112
public xmlBodyEquals(expectedBody: string | Resolver, config?: ResolverConfig): void {

‎packages/spec-api/src/match-engine.ts‎

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,18 @@
1010
/** Symbol used to identify matcher objects */
1111
export const MatcherSymbol: unique symbol = Symbol.for("SpectorMatcher");
1212

13-
/** Result of a match operation */
14-
export type MatchResult = { pass: true } | { pass: false; message: string };
13+
/** Comparison outcome; narrow on `pass` before reading the failure message. */
14+
export type MatchResult =
15+
| {
16+
/** The actual value satisfies the expectation. */
17+
pass: true;
18+
}
19+
| {
20+
/** The actual value does not satisfy the expectation. */
21+
pass: false;
22+
/** Reason for failure, including a value path when returned by matchValues. */
23+
message: string;
24+
};
1525

1626
const OK: MatchResult = Object.freeze({ pass: true });
1727

@@ -43,14 +53,19 @@ export interface MockValueMatcher<T = unknown> {
4353

4454
/** Configuration available to matchers at runtime */
4555
export interface MatcherConfig {
56+
/** Server origin used by URL matchers; defaults to an empty string when no config is supplied. */
4657
baseUrl: string;
4758
}
4859

4960
const emptyConfig: MatcherConfig = { baseUrl: "" };
5061

62+
/** Behavior supplied to createMatcher, optionally created by a factory that captures runtime config. */
5163
interface MatcherImpl<T> {
64+
/** Checks the incoming value and returns an outcome rather than throwing for a mismatch. */
5265
check(actual: unknown): MatchResult;
66+
/** Provides the plain value emitted when the matcher is serialized. */
5367
serialize(): T;
68+
/** Optional diagnostic description; falls back to String(serialize()) when omitted. */
5469
toString?: () => string;
5570
}
5671

@@ -111,7 +126,14 @@ function pathErr(message: string, path: string): MatchResult {
111126
/**
112127
* Recursively compares actual vs expected values.
113128
* When a MockValueMatcher is encountered in the expected tree, delegates to matcher.check().
114-
* Otherwise uses strict equality semantics (same as deep-equal with strict: true).
129+
* Compares primitive values without coercion, arrays by length and position, Buffers
130+
* by bytes, and objects by enumerable string keys and their values. Extra object keys
131+
* fail; an undefined expected property can be omitted from the actual object.
132+
* This is intended for payload trees, not general-purpose class or Date equality.
133+
*
134+
* @param path Initial diagnostic path; defaults to `$`.
135+
* @param config Context forwarded to expected matchers; defaults to an empty base URL.
136+
* @returns The first mismatch with its path, or a passing result. Neither tree is modified.
115137
*/
116138
export function matchValues(
117139
actual: unknown,

‎packages/spec-api/src/matchers/datetime.ts‎

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,11 +47,17 @@ function createDateTimeMatcher(
4747
});
4848
}
4949

50+
/** Datetime matcher factories exposed through match.dateTime. */
5051
export const dateTimeMatcher = {
52+
/**
53+
* Matches strings with RFC 3339 syntax that parse to the same millisecond timestamp.
54+
* Fractional precision and offsets may differ; a zone is optional in the accepted syntax.
55+
* @throws {Error} if the expected datetime cannot be parsed.
56+
*/
5157
rfc3339(value: string): MockValueMatcher<string> {
5258
return createDateTimeMatcher(value, "match.dateTime.rfc3339", "rfc3339", rfc3339Pattern);
5359
},
54-
/** Like rfc3339 but rejects timezone offsets — only Z (UTC) suffix is allowed. */
60+
/** Like rfc3339 but requires a Z (UTC) suffix, rejecting offsets and missing zones. */
5561
utcRfc3339(value: string): MockValueMatcher<string> {
5662
return createDateTimeMatcher(
5763
value,
@@ -60,6 +66,10 @@ export const dateTimeMatcher = {
6066
utcRfc3339Pattern,
6167
);
6268
},
69+
/**
70+
* Matches HTTP-date strings ending in GMT that parse to the expected millisecond timestamp.
71+
* @throws {Error} if the expected datetime cannot be parsed.
72+
*/
6373
rfc7231(value: string): MockValueMatcher<string> {
6474
return createDateTimeMatcher(value, "match.dateTime.rfc7231", "rfc7231", rfc7231Pattern);
6575
},

‎packages/spec-api/src/matchers/local-url.ts‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
import { createMatcher, err, type MockValueMatcher, ok } from "../match-engine.js";
22

3+
/**
4+
* Creates the match.localUrl expectation by concatenating the runtime base URL and path.
5+
* Matching requires the exact resulting string; no slash insertion or URL normalization is done.
6+
*/
37
export function baseUrlMatcher(path: string): MockValueMatcher<string> {
48
return createMatcher((config) => ({
59
check(actual: unknown) {

‎packages/spec-api/src/matchers/string.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { createMatcher, err, type MockValueMatcher, ok } from "../match-engine.js";
22

3+
/** String comparison factories exposed through match.string. */
34
export const stringMatcher = {
5+
/** Matches strings after lowercasing both values; serialization preserves the expected spelling. */
46
caseInsensitive(value: string): MockValueMatcher<string> {
57
const normalized = value.toLowerCase();
68
return createMatcher({

‎packages/spec-api/src/mock-request.ts‎

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,47 @@
11
import { RequestExpectation } from "./expectation.js";
22
import type { RequestExt } from "./types.js";
33

4+
/** Handler-facing view of an Express request, with assertion helpers and parsed request data. */
45
export class MockRequest {
6+
/** Assertions bound to the original request; failed assertions throw ValidationError. */
57
public readonly expect: RequestExpectation;
68

9+
/** Incoming request origin (`protocol://host`), including the port when present. */
710
public readonly baseUrl: string;
8-
public readonly headers: { [key: string]: string };
9-
public readonly query: { [key: string]: string | string[] };
10-
public readonly params: { [key: string]: string };
11+
/** Parsed headers using Express/Node's lowercase names; values are not copied or normalized here. */
12+
public readonly headers: {
13+
/** Header value retained from the original request, without additional conversion. */
14+
[key: string]: string;
15+
};
16+
/** Values returned by Express's query parser, including arrays for repeated parameters. */
17+
public readonly query: {
18+
/** One query value, or the ordered values of a repeated parameter. */
19+
[key: string]: string | string[];
20+
};
21+
/** Named route parameters extracted by Express, shared with the original request. */
22+
public readonly params: {
23+
/** Route segment captured for a named parameter such as `:id`. */
24+
[key: string]: string;
25+
};
26+
/**
27+
* Payload supplied by the server's parsing middleware: parsed JSON or form values,
28+
* text, or a Buffer containing raw bytes for binary payloads. Retained without conversion.
29+
*/
1130
public readonly body: any;
31+
/** Multipart uploads as provided by the upload middleware; absent when no files were parsed. */
1232
public readonly files?:
1333
| {
34+
/** Uploaded files associated with this form field, in middleware order. */
1435
[fieldname: string]: Express.Multer.File[];
1536
}
1637
| Express.Multer.File[]
1738
| undefined;
1839

19-
public constructor(public originalRequest: RequestExt) {
40+
/** Wraps a request without cloning its parsed data and binds assertions to that same request. */
41+
public constructor(
42+
/** Underlying Express request, including rawBody for byte- or text-level assertions. */
43+
public originalRequest: RequestExt,
44+
) {
2045
this.baseUrl = getRequestBaseUrl(originalRequest);
2146
this.expect = new RequestExpectation(originalRequest);
2247
this.headers = originalRequest.headers as { [key: string]: string };

0 commit comments

Comments
 (0)