Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .chronus/changes/feat-spector-matchers-2026-2-12-14-17-25.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
changeKind: feature
packages:
- "@typespec/spec-api"
- "@typespec/spector"
- "@typespec/http-specs"
---

Add matcher framework for flexible value comparison in scenarios. `match.dateTime()` enables semantic datetime comparison that handles precision and timezone differences across languages.
165 changes: 29 additions & 136 deletions packages/http-specs/specs/encode/datetime/mockapi.ts
Original file line number Diff line number Diff line change
@@ -1,274 +1,167 @@
import {
CollectionFormat,
json,
MockRequest,
passOnSuccess,
ScenarioMockApi,
validateValueFormat,
ValidationError,
} from "@typespec/spec-api";
import { json, match, MockRequest, passOnSuccess, ScenarioMockApi } from "@typespec/spec-api";

export const Scenarios: Record<string, ScenarioMockApi> = {};

function createQueryServerTests(
uri: string,
paramData: any,
format: "rfc7231" | "rfc3339" | undefined,
value: any,
collectionFormat?: CollectionFormat,
format: "rfc7231" | "rfc3339" | "utcRfc3339" | undefined,
) {
return passOnSuccess({
uri,
method: "get",
request: {
query: paramData,
query: { value: format ? match.dateTime[format](value) : value },
},
response: {
status: 204,
},
handler(req: MockRequest) {
if (format) {
validateValueFormat(req.query["value"] as string, format);
if (Date.parse(req.query["value"] as string) !== Date.parse(value)) {
throw new ValidationError(`Wrong value`, value, req.query["value"]);
}
} else {
req.expect.containsQueryParam("value", value, collectionFormat);
}
return {
status: 204,
};
},
kind: "MockApiDefinition",
});
}
Scenarios.Encode_Datetime_Query_default = createQueryServerTests(
"/encode/datetime/query/default",
{
value: "2022-08-26T18:38:00.000Z",
},
"rfc3339",
"2022-08-26T18:38:00.000Z",
"utcRfc3339",
);
Scenarios.Encode_Datetime_Query_rfc3339 = createQueryServerTests(
"/encode/datetime/query/rfc3339",
{
value: "2022-08-26T18:38:00.000Z",
},
"rfc3339",
"2022-08-26T18:38:00.000Z",
"utcRfc3339",
);
Scenarios.Encode_Datetime_Query_rfc7231 = createQueryServerTests(
"/encode/datetime/query/rfc7231",
{
value: "Fri, 26 Aug 2022 14:38:00 GMT",
},
"rfc7231",
"Fri, 26 Aug 2022 14:38:00 GMT",
"rfc7231",
);
Scenarios.Encode_Datetime_Query_unixTimestamp = createQueryServerTests(
"/encode/datetime/query/unix-timestamp",
{
value: 1686566864,
},
undefined,
"1686566864",
undefined,
);
Scenarios.Encode_Datetime_Query_unixTimestampArray = createQueryServerTests(
"/encode/datetime/query/unix-timestamp-array",
{
value: [1686566864, 1686734256].join(","),
},
[1686566864, 1686734256].join(","),
undefined,
["1686566864", "1686734256"],
"csv",
);
function createPropertyServerTests(
uri: string,
data: any,
format: "rfc7231" | "rfc3339" | undefined,
value: any,
format: "rfc7231" | "rfc3339" | "utcRfc3339" | undefined,
) {
const matcherBody = { value: format ? match.dateTime[format](value) : value };
return passOnSuccess({
uri,
method: "post",
request: {
body: json(data),
body: json(matcherBody),
},
response: {
status: 200,
},
handler: (req: MockRequest) => {
if (format) {
validateValueFormat(req.body["value"], format);
if (Date.parse(req.body["value"]) !== Date.parse(value)) {
throw new ValidationError(`Wrong value`, value, req.body["value"]);
}
} else {
req.expect.coercedBodyEquals({ value: value });
}
return {
status: 200,
body: json({ value: value }),
};
body: json(matcherBody),
},
kind: "MockApiDefinition",
});
}
Scenarios.Encode_Datetime_Property_default = createPropertyServerTests(
"/encode/datetime/property/default",
{
value: "2022-08-26T18:38:00.000Z",
},
"rfc3339",
"2022-08-26T18:38:00.000Z",
"utcRfc3339",
);
Scenarios.Encode_Datetime_Property_rfc3339 = createPropertyServerTests(
"/encode/datetime/property/rfc3339",
{
value: "2022-08-26T18:38:00.000Z",
},
"rfc3339",
"2022-08-26T18:38:00.000Z",
"utcRfc3339",
);
Scenarios.Encode_Datetime_Property_rfc7231 = createPropertyServerTests(
"/encode/datetime/property/rfc7231",
{
value: "Fri, 26 Aug 2022 14:38:00 GMT",
},
"rfc7231",
"Fri, 26 Aug 2022 14:38:00 GMT",
"rfc7231",
);
Scenarios.Encode_Datetime_Property_unixTimestamp = createPropertyServerTests(
"/encode/datetime/property/unix-timestamp",
{
value: 1686566864,
},
undefined,
1686566864,
undefined,
);
Scenarios.Encode_Datetime_Property_unixTimestampArray = createPropertyServerTests(
"/encode/datetime/property/unix-timestamp-array",
{
value: [1686566864, 1686734256],
},
undefined,
[1686566864, 1686734256],
undefined,
);
function createHeaderServerTests(
uri: string,
data: any,
format: "rfc7231" | "rfc3339" | undefined,
value: any,
format: "rfc7231" | "rfc3339" | "utcRfc3339" | undefined,
) {
const matcherHeaders = { value: format ? match.dateTime[format](value) : value };
return passOnSuccess({
uri,
method: "get",
request: {
headers: data,
headers: matcherHeaders,
},
response: {
status: 204,
},
handler(req: MockRequest) {
if (format) {
validateValueFormat(req.headers["value"], format);
if (Date.parse(req.headers["value"]) !== Date.parse(value)) {
throw new ValidationError(`Wrong value`, value, req.headers["value"]);
}
} else {
req.expect.containsHeader("value", value);
}
return {
status: 204,
};
},
kind: "MockApiDefinition",
});
}
Scenarios.Encode_Datetime_Header_default = createHeaderServerTests(
"/encode/datetime/header/default",
{
value: "Fri, 26 Aug 2022 14:38:00 GMT",
},
"rfc7231",
"Fri, 26 Aug 2022 14:38:00 GMT",
"rfc7231",
);
Scenarios.Encode_Datetime_Header_rfc3339 = createHeaderServerTests(
"/encode/datetime/header/rfc3339",
{
value: "2022-08-26T18:38:00.000Z",
},
"rfc3339",
"2022-08-26T18:38:00.000Z",
"utcRfc3339",
);
Scenarios.Encode_Datetime_Header_rfc7231 = createHeaderServerTests(
"/encode/datetime/header/rfc7231",
{
value: "Fri, 26 Aug 2022 14:38:00 GMT",
},
"rfc7231",
"Fri, 26 Aug 2022 14:38:00 GMT",
"rfc7231",
);
Scenarios.Encode_Datetime_Header_unixTimestamp = createHeaderServerTests(
"/encode/datetime/header/unix-timestamp",
{
value: 1686566864,
},
1686566864,
undefined,
"1686566864",
);
Scenarios.Encode_Datetime_Header_unixTimestampArray = createHeaderServerTests(
"/encode/datetime/header/unix-timestamp-array",
{
value: [1686566864, 1686734256].join(","),
},
[1686566864, 1686734256].join(","),
undefined,
"1686566864,1686734256",
);
function createResponseHeaderServerTests(uri: string, data: any, value: any) {
function createResponseHeaderServerTests(uri: string, value: any) {
return passOnSuccess({
uri,
method: "get",
request: {},
response: {
status: 204,
headers: data,
headers: { value },
},
handler: (req: MockRequest) => {
return {
status: 204,
headers: { value: value },
headers: { value },
};
},
kind: "MockApiDefinition",
});
}
Scenarios.Encode_Datetime_ResponseHeader_default = createResponseHeaderServerTests(
"/encode/datetime/responseheader/default",
{
value: "Fri, 26 Aug 2022 14:38:00 GMT",
},
"Fri, 26 Aug 2022 14:38:00 GMT",
);
Scenarios.Encode_Datetime_ResponseHeader_rfc3339 = createResponseHeaderServerTests(
"/encode/datetime/responseheader/rfc3339",
{
value: "2022-08-26T18:38:00.000Z",
},
"2022-08-26T18:38:00.000Z",
);
Scenarios.Encode_Datetime_ResponseHeader_rfc7231 = createResponseHeaderServerTests(
"/encode/datetime/responseheader/rfc7231",
{
value: "Fri, 26 Aug 2022 14:38:00 GMT",
},
"Fri, 26 Aug 2022 14:38:00 GMT",
);
Scenarios.Encode_Datetime_ResponseHeader_unixTimestamp = createResponseHeaderServerTests(
"/encode/datetime/responseheader/unix-timestamp",
{
value: "1686566864",
},
1686566864,
"1686566864",
);
49 changes: 16 additions & 33 deletions packages/http-specs/specs/payload/pageable/mockapi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ import {
dyn,
dynItem,
json,
match,
MockRequest,
passOnSuccess,
ResolverConfig,
ScenarioMockApi,
ValidationError,
xml,
Expand Down Expand Up @@ -650,22 +650,6 @@ Scenarios.Payload_Pageable_XmlPagination_listWithContinuation = passOnSuccess([
},
]);

const xmlNextLinkFirstPage = (baseUrl: string) => `
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do these changes belong in this PR?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah pr also make base url a matcher which cleans that up

<PetListResult>
<Pets>
<Pet>
<Id>1</Id>
<Name>dog</Name>
</Pet>
<Pet>
<Id>2</Id>
<Name>cat</Name>
</Pet>
</Pets>
<NextLink>${baseUrl}/payload/pageable/xml/list-with-next-link/nextPage</NextLink>
</PetListResult>
`;

const XmlNextLinkSecondPage = `
<PetListResult>
<Pets>
Expand All @@ -688,26 +672,25 @@ Scenarios.Payload_Pageable_XmlPagination_listWithNextLink = passOnSuccess([
request: {},
response: {
status: 200,
body: {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do these belong in this pr?

contentType: "application/xml",
rawContent: {
serialize: (config: ResolverConfig) =>
`<?xml version='1.0' encoding='UTF-8'?>` + xmlNextLinkFirstPage(config.baseUrl),
},
},
body: xml`
<PetListResult>
<Pets>
<Pet>
<Id>1</Id>
<Name>dog</Name>
</Pet>
<Pet>
<Id>2</Id>
<Name>cat</Name>
</Pet>
</Pets>
<NextLink>${match.localUrl("/payload/pageable/xml/list-with-next-link/nextPage")}</NextLink>
</PetListResult>
`,
headers: {
"content-type": "application/xml; charset=utf-8",
},
},
handler: (req: MockRequest) => {
return {
status: 200,
body: xml(xmlNextLinkFirstPage(req.baseUrl)),
headers: {
"content-type": "application/xml",
},
};
},
kind: "MockApiDefinition",
},
{
Expand Down
Loading
Loading