Skip to content

Commit

Permalink
Merge branch 'main' into types-directory
Browse files Browse the repository at this point in the history
  • Loading branch information
pmcelhaney authored May 30, 2024
2 parents ab4cf9c + ae7614a commit a03a4ed
Show file tree
Hide file tree
Showing 8 changed files with 93 additions and 45 deletions.
5 changes: 0 additions & 5 deletions .changeset/hip-ravens-teach.md

This file was deleted.

19 changes: 19 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,24 @@
# counterfact

## 0.43.2

### Patch Changes

- b09fb59: fixed the type system to address that fact that json() handles multiple content types

## 0.43.1

### Patch Changes

- 9e285a5: reverted type change for .json() -- it had unintended consequences

## 0.43.0

### Minor Changes

- 425e893: turn the proxy on or off for individual paths
- 4fed190: expand the .json() shortcut to include variations of JSON and XML content types

## 0.42.1

### Patch Changes
Expand Down
6 changes: 3 additions & 3 deletions docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,9 +111,9 @@ The `$.response` object is used to build a valid response for the URL and reques
- `.random()` returns random data, using `examples` and other metadata from the OpenAPI document.
- `.header(name, value)` adds a response header. It will only show up when a response header is expected and you haven't already provided it.
- `.match(contentType, content)` is used to return content which matches the content type. If the API is intended to serve one of multiple content types, depending on the client's `Accepts:` header, you can chain multiple `match()` calls.
- `.json(content)` is shorthand for `.match("application/json", content)`
- `.text(content)` is shorthand for `.match("text/plain", content)`
- `.html(content)` is shorthand for `.match("text/html", content)`
- `.json(content)`, `.text(content)`, `.html(content)`, and `.xml(content)` are shorthands for the `match()` function, e.g. `.text(content)` is shorthand for `.match("text/plain", content)`.
- if the content type is XML, you can pass a JSON object, and Counterfact will automatically convert it to XML for you
- The `.json()` shortcut handles both JSON and XML.

You can build a response by chaining one or more of these functions, e.g.

Expand Down
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "counterfact",
"version": "0.42.1",
"version": "0.43.2",
"description": "a library for building a fake REST API for testing",
"type": "module",
"main": "./src/server/counterfact.js",
Expand Down Expand Up @@ -68,11 +68,11 @@
"eslint-plugin-jest": "28.5.0",
"eslint-plugin-jest-dom": "5.4.0",
"eslint-plugin-no-explicit-type-exports": "0.12.1",
"eslint-plugin-unused-imports": "3.2.0",
"eslint-plugin-unused-imports": "4.0.0",
"husky": "9.0.11",
"jest": "29.7.0",
"node-mocks-http": "1.14.1",
"nodemon": "3.1.0",
"nodemon": "3.1.1",
"rimraf": "5.0.7",
"stryker-cli": "1.0.2",
"supertest": "7.0.0",
Expand Down
6 changes: 5 additions & 1 deletion src/server/response-builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,11 @@ export function createResponseBuilder(
},

json(this: ResponseBuilder, body: unknown) {
return this.match("application/json", body);
return this.match("application/json", body)
.match("text/json", body)
.match("text/x-json", body)
.match("application/xml", body)
.match("text/xml", body);
},

match(
Expand Down
70 changes: 45 additions & 25 deletions src/server/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ interface Example {

type MediaType = `${string}/${string}`;

type OmitAll<T, K extends (keyof T)[]> = {
[P in keyof T as P extends K[number] ? never : P]: T[P];
};

type OmitValueWhenNever<Base> = Pick<
Base,
{
Expand All @@ -24,25 +28,34 @@ type OmitValueWhenNever<Base> = Pick<
interface OpenApiResponse {
content: { [key: MediaType]: OpenApiContent };
headers: { [key: string]: OpenApiHeader };
requiredHeaders: string
requiredHeaders: string;
}

interface OpenApiResponses {
[key: string]: OpenApiResponse;
}

type IfHasKey<SomeObject, Key, Yes, No> = Key extends keyof SomeObject
? Yes
type IfHasKey<SomeObject, Keys extends (keyof any)[], Yes, No> = Keys extends [
infer FirstKey,
...infer RestKeys,
]
? FirstKey extends keyof SomeObject
? Yes
: RestKeys extends (keyof any)[]
? IfHasKey<SomeObject, RestKeys, Yes, No>
: No
: No;

type MaybeShortcut<
ContentType extends MediaType,
ContentTypes extends MediaType[],
Response extends OpenApiResponse,
> = IfHasKey<
Response["content"],
ContentType,
(body: Response["content"][ContentType]["schema"]) => GenericResponseBuilder<{
content: NeverIfEmpty<Omit<Response["content"], ContentType>>;
ContentTypes,
(
body: Response["content"][ArrayToUnion<ContentTypes>]["schema"],
) => GenericResponseBuilder<{
content: NeverIfEmpty<OmitAll<Response["content"], ContentTypes>>;
headers: Response["headers"];
requiredHeaders: Response["requiredHeaders"];
}>,
Expand All @@ -55,7 +68,7 @@ type MatchFunction<Response extends OpenApiResponse> = <
ContentType extends MediaType & keyof Response["content"],
>(
contentType: ContentType,
body: Response["content"][ContentType]["schema"]
body: Response["content"][ContentType]["schema"],
) => GenericResponseBuilder<{
content: NeverIfEmpty<Omit<Response["content"], ContentType>>;
headers: Response["headers"];
Expand All @@ -66,7 +79,7 @@ type HeaderFunction<Response extends OpenApiResponse> = <
Header extends string & keyof Response["headers"],
>(
header: Header,
value: Response["headers"][Header]["schema"]
value: Response["headers"][Header]["schema"],
) => GenericResponseBuilder<{
content: NeverIfEmpty<Response["content"]>;
headers: NeverIfEmpty<Omit<Response["headers"], Header>>;
Expand All @@ -92,27 +105,36 @@ interface ResponseBuilder {
xml: (body: unknown) => ResponseBuilder;
}

type ArrayToUnion<T extends readonly any[]> = T[number];

type GenericResponseBuilderInner<
Response extends OpenApiResponse = OpenApiResponse,
> = OmitValueWhenNever<{
header: [keyof Response["headers"]] extends [never]
? never
: HeaderFunction<Response>;
html: MaybeShortcut<"text/html", Response>;
json: MaybeShortcut<"application/json", Response>;
html: MaybeShortcut<["text/html"], Response>;
json: MaybeShortcut<["application/json", "text/json", "text/x-json", "application/xml", "text/xml"], Response>;
match: [keyof Response["content"]] extends [never]
? never
: MatchFunction<Response>;
random: [keyof Response["content"]] extends [never]
? never
: RandomFunction<Response>;
text: MaybeShortcut<"text/plain", Response>;
xml: MaybeShortcut<"application/xml" | "text/xml", Response>;
text: MaybeShortcut<["text/plain"], Response>;
xml: MaybeShortcut<["application/xml", "text/xml"], Response>;
}>;

type GenericResponseBuilder<
Response extends OpenApiResponse = OpenApiResponse,
> = {} extends OmitValueWhenNever<Response> ? "COUNTERFACT_RESPONSE" : keyof OmitValueWhenNever<Response> extends "headers" ? { header: HeaderFunction<Response>, ALL_REMAINING_HEADERS_ARE_OPTIONAL: "COUNTERFACT_RESPONSE" } : GenericResponseBuilderInner<Response>;
> = {} extends OmitValueWhenNever<Response>
? "COUNTERFACT_RESPONSE"
: keyof OmitValueWhenNever<Response> extends "headers"
? {
ALL_REMAINING_HEADERS_ARE_OPTIONAL: "COUNTERFACT_RESPONSE";
header: HeaderFunction<Response>;
}
: GenericResponseBuilderInner<Response>;

type ResponseBuilderFactory<
Responses extends OpenApiResponses = OpenApiResponses,
Expand Down Expand Up @@ -205,7 +227,7 @@ interface OpenApiOperation {
};
}

type WideResponseBuilder = {
interface WideResponseBuilder {
header: (body: unknown) => WideResponseBuilder;
html: (body: unknown) => WideResponseBuilder;
json: (body: unknown) => WideResponseBuilder;
Expand All @@ -215,26 +237,24 @@ type WideResponseBuilder = {
xml: (body: unknown) => WideResponseBuilder;
}

type WideOperationArgument = {
path: Record<string, string>;
query: Record<string, string>;
header: Record<string, string>;
interface WideOperationArgument {
body: unknown;
response: Record<number, WideResponseBuilder>;
context: unknown;
header: { [key: string]: string };
path: { [key: string]: string };
proxy: (url: string) => { proxyUrl: string };
context: unknown
};
query: { [key: string]: string };
response: { [key: number]: WideResponseBuilder };
}

export type {
HttpStatusCode,
MediaType,
OmitValueWhenNever,
OpenApiOperation,
OpenApiParameters,
OpenApiResponse,
ResponseBuilder,
ResponseBuilderFactory,
WideOperationArgument,
OmitValueWhenNever
};


10 changes: 10 additions & 0 deletions test/server/response-builder.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,16 @@ describe("a response builder", () => {
expect(response?.content).toStrictEqual([
{ body: "hello", type: "text/plain" },
{ body: { hello: "world" }, type: "application/json" },
{ body: { hello: "world" }, type: "text/json" },
{ body: { hello: "world" }, type: "text/x-json" },
{
body: "<root><hello>world</hello></root>",
type: "application/xml",
},
{
body: "<root><hello>world</hello></root>",
type: "text/xml",
},
{ body: "<h1>Hello World</h1>", type: "text/html" },
{
body: "<root><hello>world</hello></root>",
Expand Down
16 changes: 8 additions & 8 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -5572,10 +5572,10 @@ eslint-plugin-unicorn@^48.0.1:
semver "^7.5.4"
strip-indent "^3.0.0"

eslint-plugin-unused-imports@3.2.0:
version "3.2.0"
resolved "https://registry.yarnpkg.com/eslint-plugin-unused-imports/-/eslint-plugin-unused-imports-3.2.0.tgz#63a98c9ad5f622cd9f830f70bc77739f25ccfe0d"
integrity sha512-6uXyn6xdINEpxE1MtDjxQsyXB37lfyO2yKGVVgtD7WEWQGORSOZjgrD6hBhvGv4/SO+TOlS+UnC6JppRqbuwGQ==
eslint-plugin-unused-imports@4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/eslint-plugin-unused-imports/-/eslint-plugin-unused-imports-4.0.0.tgz#93f3a7ee6088221e4a1d7127866e05d5917a9f65"
integrity sha512-mzM+y2B7XYpQryVa1usT+Y/BdNAtAZiXzwpSyDCboFoJN/LZRN67TNvQxKtuTK/Aplya3sLNQforiubzPPaIcQ==
dependencies:
eslint-rule-composer "^0.3.0"

Expand Down Expand Up @@ -8821,10 +8821,10 @@ node-source-walk@^7.0.0:
dependencies:
"@babel/parser" "^7.24.4"

[email protected].0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/nodemon/-/nodemon-3.1.0.tgz#ff7394f2450eb6a5e96fe4180acd5176b29799c9"
integrity sha512-xqlktYlDMCepBJd43ZQhjWwMw2obW/JRvkrLxq5RCNcuDDX1DbcPT+qT1IlIIdf+DhnWs90JpTMe+Y5KxOchvA==
[email protected].1:
version "3.1.1"
resolved "https://registry.yarnpkg.com/nodemon/-/nodemon-3.1.1.tgz#779631d9da3a7f8a995b16976c87621818c19a8d"
integrity sha512-k43xGaDtaDIcufn0Fc6fTtsdKSkV/hQzoQFigNH//GaKta28yoKVYXCnV+KXRqfT/YzsFaQU9VdeEG+HEyxr6A==
dependencies:
chokidar "^3.5.2"
debug "^4"
Expand Down

0 comments on commit a03a4ed

Please sign in to comment.