diff --git a/.changeset/move-openapi-example-to-fixtures.md b/.changeset/move-openapi-example-to-fixtures.md
new file mode 100644
index 000000000..90cbfdfd1
--- /dev/null
+++ b/.changeset/move-openapi-example-to-fixtures.md
@@ -0,0 +1,5 @@
+---
+"counterfact": patch
+---
+
+Moved `openapi-example.yaml` from the repository root into `test/fixtures/openapi-example.yaml` and expanded it with many OpenAPI edge cases: CRUD operations on `/users` and `/users/{userId}`, polymorphic events via `oneOf`/`allOf`/`discriminator`, nullable fields, enum types, integer formats, file upload via `multipart/form-data`, cookie parameters, deprecated endpoints, multiple response content types, a no-body `204` health-check endpoint, and free-form `additionalProperties` objects.
diff --git a/openapi-example.yaml b/openapi-example.yaml
deleted file mode 100644
index 847940595..000000000
--- a/openapi-example.yaml
+++ /dev/null
@@ -1,128 +0,0 @@
-openapi: 3.0.3
-info:
- version: 1.0.0
- title: Sample API
- description: A sample API to illustrate OpenAPI concepts
-paths:
- /:
- get:
- description: the root
- responses:
- default:
- description: root
- content:
- text/plain:
- schema:
- type: string
- examples:
- root:
- value: This is the root.
-
- /count:
- get:
- description: outputs the number of time each URL was visited
- responses:
- default:
- description: Successful response
- content:
- application/json:
- schema:
- type: string
- examples:
- no visits:
- value: You have not visited anyone yet
- /hello/kitty:
- get:
- description: HTML with a hello kitty image
- operationId: helloKitty
- responses:
- default:
- description: Successful response
- content:
- application/json:
- schema:
- type: string
- examples:
- hello kitty:
- value: >-
-
- /hello/{name}:
- get:
- parameters:
- - in: path
- name: name
- required: true
- schema:
- type: string
- description: says hello to the name
- description: says hello to someone
- responses:
- default:
- description: Successful response
- headers:
- x-test:
- schema:
- type: string
- content:
- application/json:
- schema:
- type: string
- example: an example string
- examples:
- hello-example1:
- value: Hello, example1
- hello-example2:
- value: Hello, example2
- /path-one:
- get:
- responses:
- "200":
- description: test
- content:
- application/json:
- schema:
- $ref: "#/components/schemas/Recursive"
- "400":
- $ref: "#/components/responses/BadRequest"
- "/weird/path/with:colon":
- get:
- responses:
- "200":
- description: "this path has a colon in it"
- content:
- application/json:
- schema:
- type: string
-
-components:
- responses:
- BadRequest:
- description: The request is malformed and so cannot be processed
- content:
- application/json:
- schema:
- $ref: "#/components/schemas/Error"
-
- schemas:
- Error:
- title: Error
- description: A generic error message suitable for 4xx and 5xx responses
- type: object
- properties:
- code:
- type: string
- description: A machine readable error code
- message:
- type: string
- description: A detailed description of the error
- required:
- - message
- Recursive:
- oneOf:
- - { $ref: "#/components/schemas/Recursive" }
-
- securitySchemes:
- basicAuth:
- type: http
- scheme: basic
diff --git a/package.json b/package.json
index 8f0da8d68..efe0bd7a5 100644
--- a/package.json
+++ b/package.json
@@ -76,7 +76,7 @@
"lint:quickfix": "eslint --fix . eslint --fix demo-ts --rule=\"import/namespace: 0,etc/no-deprecated:0,import/no-cycle:0,no-explicit-type-exports/no-explicit-type-exports:0,import/no-deprecated:0,import/no-self-import:0,import/default:0,import/no-named-as-default:0\" --ignore-pattern dist --ignore-pattern out",
"go:petstore": "yarn build && yarn counterfact https://petstore3.swagger.io/api/v3/openapi.json out",
"go:petstore2": "yarn build && yarn counterfact https://petstore.swagger.io/v2/swagger.json out",
- "go:example": "yarn build && node ./bin/counterfact.js ./openapi-example.yaml out",
+ "go:example": "yarn build && node ./bin/counterfact.js ./test/fixtures/openapi-example.yaml out",
"counterfact": "./bin/counterfact.js",
"postinstall": "patch-package"
},
diff --git a/test/app.test.ts b/test/app.test.ts
index 766aba43a..a39969e20 100644
--- a/test/app.test.ts
+++ b/test/app.test.ts
@@ -61,7 +61,7 @@ describe("handleMswRequest", () => {
await (app as any).createMswHandlers(
{
...mockConfig,
- openApiPath: "openapi-example.yaml",
+ openApiPath: "test/fixtures/openapi-example.yaml",
},
MockModuleLoader,
);
@@ -120,7 +120,7 @@ describe("createMswHandlers", () => {
const handlers = await (app as any).createMswHandlers(
{
...mockConfig,
- openApiPath: "openapi-example.yaml",
+ openApiPath: "test/fixtures/openapi-example.yaml",
},
MockModuleLoader,
);
diff --git a/test/fixtures/openapi-example.yaml b/test/fixtures/openapi-example.yaml
new file mode 100644
index 000000000..38fa70fa7
--- /dev/null
+++ b/test/fixtures/openapi-example.yaml
@@ -0,0 +1,891 @@
+openapi: 3.0.3
+info:
+ version: 1.0.0
+ title: Sample API
+ description: A sample API to illustrate OpenAPI concepts and edge cases
+
+# Global security: endpoints can override or opt out
+security:
+ - basicAuth: []
+
+paths:
+ # -----------------------------------------------------------------------
+ # Root (plain text, default response, no parameters)
+ # -----------------------------------------------------------------------
+ /:
+ get:
+ operationId: getRoot
+ description: the root
+ security: [] # opt out of global security
+ responses:
+ default:
+ description: root
+ content:
+ text/plain:
+ schema:
+ type: string
+ examples:
+ root:
+ value: This is the root.
+
+ # -----------------------------------------------------------------------
+ # Stateful counter (no examples – handler must supply the value)
+ # -----------------------------------------------------------------------
+ /count:
+ get:
+ operationId: getCount
+ description: outputs the number of times each URL was visited
+ responses:
+ "200":
+ description: Successful response
+ content:
+ application/json:
+ schema:
+ type: integer
+ description: Total number of visits
+ "500":
+ $ref: "#/components/responses/InternalServerError"
+
+ # -----------------------------------------------------------------------
+ # Static path that returns HTML embedded in JSON
+ # -----------------------------------------------------------------------
+ /hello/kitty:
+ get:
+ operationId: helloKitty
+ description: HTML with a hello kitty image
+ responses:
+ default:
+ description: Successful response
+ content:
+ application/json:
+ schema:
+ type: string
+ examples:
+ hello kitty:
+ value: >-
+
+
+ # -----------------------------------------------------------------------
+ # Path parameter + multiple named examples + response header
+ # -----------------------------------------------------------------------
+ /hello/{name}:
+ get:
+ operationId: sayHello
+ description: says hello to someone
+ parameters:
+ - in: path
+ name: name
+ required: true
+ schema:
+ type: string
+ description: the name to greet
+ - in: query
+ name: language
+ required: false
+ schema:
+ type: string
+ enum: [en, es, fr, de, ja]
+ default: en
+ description: language for the greeting
+ - in: header
+ name: x-request-id
+ required: false
+ schema:
+ type: string
+ format: uuid
+ description: idempotency key
+ responses:
+ "200":
+ description: Successful response
+ headers:
+ x-test:
+ schema:
+ type: string
+ x-rate-limit-remaining:
+ schema:
+ type: integer
+ description: Remaining requests in the current window
+ content:
+ application/json:
+ schema:
+ type: string
+ example: an example string
+ examples:
+ hello-example1:
+ value: Hello, example1
+ hello-example2:
+ value: Hello, example2
+ "400":
+ $ref: "#/components/responses/BadRequest"
+
+ # -----------------------------------------------------------------------
+ # Recursive schema + $ref response
+ # -----------------------------------------------------------------------
+ /path-one:
+ get:
+ operationId: getPathOne
+ responses:
+ "200":
+ description: test
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/Recursive"
+ "400":
+ $ref: "#/components/responses/BadRequest"
+
+ # -----------------------------------------------------------------------
+ # Colon in path (edge case for routers)
+ # -----------------------------------------------------------------------
+ "/weird/path/with:colon":
+ get:
+ operationId: weirdColonPath
+ responses:
+ "200":
+ description: this path has a colon in it
+ content:
+ application/json:
+ schema:
+ type: string
+
+ # -----------------------------------------------------------------------
+ # CRUD resource: full set of HTTP methods, request body, 201/204
+ # -----------------------------------------------------------------------
+ /users:
+ get:
+ operationId: listUsers
+ description: List all users with optional filtering and pagination
+ parameters:
+ - in: query
+ name: page
+ required: false
+ schema:
+ type: integer
+ minimum: 1
+ default: 1
+ - in: query
+ name: pageSize
+ required: false
+ schema:
+ type: integer
+ minimum: 1
+ maximum: 100
+ default: 20
+ - in: query
+ name: active
+ required: false
+ schema:
+ type: boolean
+ - in: query
+ name: role
+ required: false
+ schema:
+ type: string
+ enum: [admin, editor, viewer]
+ - in: header
+ name: x-correlation-id
+ required: false
+ schema:
+ type: string
+ responses:
+ "200":
+ description: Paginated list of users
+ headers:
+ x-total-count:
+ schema:
+ type: integer
+ x-next-page:
+ schema:
+ type: string
+ nullable: true
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/UserList"
+ examples:
+ empty:
+ value:
+ items: []
+ total: 0
+ page: 1
+ pageSize: 20
+ withUsers:
+ value:
+ items:
+ - id: "u-001"
+ name: Alice
+ email: alice@example.com
+ role: admin
+ active: true
+ createdAt: "2024-01-15T09:00:00Z"
+ - id: "u-002"
+ name: Bob
+ email: bob@example.com
+ role: viewer
+ active: false
+ createdAt: "2024-03-22T14:30:00Z"
+ total: 2
+ page: 1
+ pageSize: 20
+ "400":
+ $ref: "#/components/responses/BadRequest"
+ "401":
+ $ref: "#/components/responses/Unauthorized"
+ "500":
+ $ref: "#/components/responses/InternalServerError"
+
+ post:
+ operationId: createUser
+ description: Create a new user
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CreateUserRequest"
+ examples:
+ minimal:
+ value:
+ name: Charlie
+ email: charlie@example.com
+ full:
+ value:
+ name: Dana
+ email: dana@example.com
+ role: editor
+ metadata:
+ department: Engineering
+ startDate: "2025-06-01"
+ responses:
+ "201":
+ description: User created
+ headers:
+ location:
+ schema:
+ type: string
+ format: uri
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/User"
+ "400":
+ $ref: "#/components/responses/BadRequest"
+ "409":
+ description: A user with that email already exists
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/Error"
+ "500":
+ $ref: "#/components/responses/InternalServerError"
+
+ /users/{userId}:
+ get:
+ operationId: getUser
+ description: Fetch a single user by ID
+ parameters:
+ - in: path
+ name: userId
+ required: true
+ schema:
+ type: string
+ description: Unique user identifier
+ responses:
+ "200":
+ description: The requested user
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/User"
+ "404":
+ $ref: "#/components/responses/NotFound"
+ "500":
+ $ref: "#/components/responses/InternalServerError"
+
+ put:
+ operationId: replaceUser
+ description: Replace a user record entirely (idempotent)
+ parameters:
+ - in: path
+ name: userId
+ required: true
+ schema:
+ type: string
+ description: Unique user identifier
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CreateUserRequest"
+ responses:
+ "200":
+ description: User replaced
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/User"
+ "400":
+ $ref: "#/components/responses/BadRequest"
+ "404":
+ $ref: "#/components/responses/NotFound"
+
+ patch:
+ operationId: patchUser
+ description: Partially update a user record
+ parameters:
+ - in: path
+ name: userId
+ required: true
+ schema:
+ type: string
+ description: Unique user identifier
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/PatchUserRequest"
+ responses:
+ "200":
+ description: User updated
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/User"
+ "400":
+ $ref: "#/components/responses/BadRequest"
+ "404":
+ $ref: "#/components/responses/NotFound"
+
+ delete:
+ operationId: deleteUser
+ description: Delete a user (soft-delete)
+ parameters:
+ - in: path
+ name: userId
+ required: true
+ schema:
+ type: string
+ description: Unique user identifier
+ responses:
+ "204":
+ description: User deleted, no content
+ "404":
+ $ref: "#/components/responses/NotFound"
+
+ # -----------------------------------------------------------------------
+ # allOf / oneOf / anyOf schema composition
+ # -----------------------------------------------------------------------
+ /events:
+ post:
+ operationId: createEvent
+ description: >
+ Accept a polymorphic event. The request body may be any one of the
+ known event subtypes (oneOf), demonstrating discriminator support.
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ oneOf:
+ - $ref: "#/components/schemas/ClickEvent"
+ - $ref: "#/components/schemas/PurchaseEvent"
+ - $ref: "#/components/schemas/SignUpEvent"
+ discriminator:
+ propertyName: type
+ mapping:
+ click: "#/components/schemas/ClickEvent"
+ purchase: "#/components/schemas/PurchaseEvent"
+ signup: "#/components/schemas/SignUpEvent"
+ responses:
+ "202":
+ description: Event accepted
+ "400":
+ $ref: "#/components/responses/BadRequest"
+
+ # -----------------------------------------------------------------------
+ # Nullable fields, enum, and integer formats
+ # -----------------------------------------------------------------------
+ /products/{productId}:
+ get:
+ operationId: getProduct
+ parameters:
+ - in: path
+ name: productId
+ required: true
+ schema:
+ type: integer
+ format: int64
+ responses:
+ "200":
+ description: A product
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/Product"
+ "404":
+ $ref: "#/components/responses/NotFound"
+
+ # -----------------------------------------------------------------------
+ # File upload (multipart/form-data)
+ # -----------------------------------------------------------------------
+ /uploads:
+ post:
+ operationId: uploadFile
+ description: Upload a binary file with metadata
+ requestBody:
+ required: true
+ content:
+ multipart/form-data:
+ schema:
+ type: object
+ required: [file]
+ properties:
+ file:
+ type: string
+ format: binary
+ description: The file to upload
+ label:
+ type: string
+ description: Human-readable label for the file
+ tags:
+ type: array
+ items:
+ type: string
+ responses:
+ "201":
+ description: File uploaded
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ id:
+ type: string
+ url:
+ type: string
+ format: uri
+ "413":
+ description: File too large
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/Error"
+
+ # -----------------------------------------------------------------------
+ # Cookie parameter + deprecated endpoint
+ # -----------------------------------------------------------------------
+ /legacy/items:
+ get:
+ operationId: listItemsLegacy
+ deprecated: true
+ description: Deprecated – use /users instead
+ parameters:
+ - in: cookie
+ name: session_id
+ required: false
+ schema:
+ type: string
+ responses:
+ "200":
+ description: Items (legacy format)
+ content:
+ application/json:
+ schema:
+ type: array
+ items:
+ type: string
+
+ # -----------------------------------------------------------------------
+ # Multiple response content types
+ # -----------------------------------------------------------------------
+ /reports/{reportId}:
+ get:
+ operationId: getReport
+ description: Download a report in the requested format
+ parameters:
+ - in: path
+ name: reportId
+ required: true
+ schema:
+ type: string
+ - in: header
+ name: Accept
+ required: false
+ schema:
+ type: string
+ enum:
+ - application/json
+ - text/csv
+ - application/pdf
+ responses:
+ "200":
+ description: Report content
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ rows:
+ type: array
+ items:
+ type: object
+ additionalProperties: true
+ text/csv:
+ schema:
+ type: string
+ description: CSV-formatted report
+ application/pdf:
+ schema:
+ type: string
+ format: binary
+ "404":
+ $ref: "#/components/responses/NotFound"
+
+ # -----------------------------------------------------------------------
+ # Empty (204) response only
+ # -----------------------------------------------------------------------
+ /ping:
+ get:
+ operationId: ping
+ description: Health-check – returns 204 with no body
+ security: []
+ responses:
+ "204":
+ description: Service is alive
+
+ # -----------------------------------------------------------------------
+ # Additional properties (free-form object)
+ # -----------------------------------------------------------------------
+ /metadata:
+ put:
+ operationId: setMetadata
+ description: Replace the free-form metadata store
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ type: object
+ additionalProperties: true
+ responses:
+ "200":
+ description: Metadata saved
+ content:
+ application/json:
+ schema:
+ type: object
+ additionalProperties: true
+
+components:
+ # -----------------------------------------------------------------------
+ # Reusable responses
+ # -----------------------------------------------------------------------
+ responses:
+ BadRequest:
+ description: The request is malformed and so cannot be processed
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/Error"
+ Unauthorized:
+ description: Authentication credentials are missing or invalid
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/Error"
+ NotFound:
+ description: The requested resource does not exist
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/Error"
+ InternalServerError:
+ description: An unexpected error occurred on the server
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/Error"
+
+ # -----------------------------------------------------------------------
+ # Schemas
+ # -----------------------------------------------------------------------
+ schemas:
+ # Generic error envelope
+ Error:
+ title: Error
+ description: A generic error message suitable for 4xx and 5xx responses
+ type: object
+ properties:
+ code:
+ type: string
+ description: A machine-readable error code
+ message:
+ type: string
+ description: A human-readable description of the error
+ details:
+ type: array
+ items:
+ type: object
+ properties:
+ field:
+ type: string
+ issue:
+ type: string
+ description: Field-level validation errors
+ required:
+ - message
+
+ # Recursive / self-referential schema
+ Recursive:
+ oneOf:
+ - $ref: "#/components/schemas/Recursive"
+
+ # User domain
+ User:
+ title: User
+ type: object
+ required: [id, name, email, role, active, createdAt]
+ properties:
+ id:
+ type: string
+ description: Opaque unique identifier
+ name:
+ type: string
+ minLength: 1
+ maxLength: 200
+ email:
+ type: string
+ format: email
+ role:
+ type: string
+ enum: [admin, editor, viewer]
+ default: viewer
+ active:
+ type: boolean
+ createdAt:
+ type: string
+ format: date-time
+ updatedAt:
+ type: string
+ format: date-time
+ nullable: true
+ metadata:
+ type: object
+ additionalProperties:
+ type: string
+ nullable: true
+ description: Arbitrary key/value pairs attached to the user
+
+ CreateUserRequest:
+ title: CreateUserRequest
+ type: object
+ required: [name, email]
+ properties:
+ name:
+ type: string
+ minLength: 1
+ maxLength: 200
+ email:
+ type: string
+ format: email
+ role:
+ type: string
+ enum: [admin, editor, viewer]
+ default: viewer
+ metadata:
+ type: object
+ additionalProperties:
+ type: string
+ nullable: true
+
+ PatchUserRequest:
+ title: PatchUserRequest
+ description: >
+ All fields are optional; only provided fields are updated (JSON Merge Patch
+ semantics). Setting a nullable field to null clears it.
+ type: object
+ properties:
+ name:
+ type: string
+ minLength: 1
+ maxLength: 200
+ nullable: true
+ email:
+ type: string
+ format: email
+ nullable: true
+ role:
+ type: string
+ enum: [admin, editor, viewer]
+ nullable: true
+ active:
+ type: boolean
+ nullable: true
+ metadata:
+ type: object
+ additionalProperties:
+ type: string
+ nullable: true
+
+ UserList:
+ title: UserList
+ type: object
+ required: [items, total, page, pageSize]
+ properties:
+ items:
+ type: array
+ items:
+ $ref: "#/components/schemas/User"
+ total:
+ type: integer
+ minimum: 0
+ page:
+ type: integer
+ minimum: 1
+ pageSize:
+ type: integer
+ minimum: 1
+ maximum: 100
+
+ # Product domain (enum + nullable + integer format)
+ Product:
+ title: Product
+ type: object
+ required: [id, name, status, price]
+ properties:
+ id:
+ type: integer
+ format: int64
+ name:
+ type: string
+ description:
+ type: string
+ nullable: true
+ status:
+ type: string
+ enum: [available, out_of_stock, discontinued]
+ price:
+ type: number
+ format: double
+ minimum: 0
+ discountedPrice:
+ type: number
+ format: double
+ minimum: 0
+ nullable: true
+ tags:
+ type: array
+ items:
+ type: string
+ default: []
+ relatedIds:
+ type: array
+ items:
+ type: integer
+ format: int64
+ description: IDs of related products
+ attributes:
+ description: >
+ Demonstrates anyOf: each attribute value is either a string,
+ a number, or a boolean.
+ type: object
+ additionalProperties:
+ anyOf:
+ - type: string
+ - type: number
+ - type: boolean
+
+ # Event domain (oneOf / discriminator)
+ BaseEvent:
+ title: BaseEvent
+ type: object
+ required: [type, occurredAt]
+ properties:
+ type:
+ type: string
+ description: Discriminator field
+ occurredAt:
+ type: string
+ format: date-time
+ sessionId:
+ type: string
+ nullable: true
+
+ ClickEvent:
+ title: ClickEvent
+ allOf:
+ - $ref: "#/components/schemas/BaseEvent"
+ - type: object
+ required: [targetUrl, elementId]
+ properties:
+ targetUrl:
+ type: string
+ format: uri
+ elementId:
+ type: string
+
+ PurchaseEvent:
+ title: PurchaseEvent
+ allOf:
+ - $ref: "#/components/schemas/BaseEvent"
+ - type: object
+ required: [orderId, totalAmount, currency]
+ properties:
+ orderId:
+ type: string
+ totalAmount:
+ type: number
+ format: double
+ minimum: 0
+ currency:
+ type: string
+ minLength: 3
+ maxLength: 3
+ description: ISO 4217 currency code
+ items:
+ type: array
+ items:
+ type: object
+ required: [productId, quantity]
+ properties:
+ productId:
+ type: integer
+ format: int64
+ quantity:
+ type: integer
+ minimum: 1
+
+ SignUpEvent:
+ title: SignUpEvent
+ allOf:
+ - $ref: "#/components/schemas/BaseEvent"
+ - type: object
+ required: [userId, email]
+ properties:
+ userId:
+ type: string
+ email:
+ type: string
+ format: email
+ referralCode:
+ type: string
+ nullable: true
+
+ # -----------------------------------------------------------------------
+ # Security schemes
+ # -----------------------------------------------------------------------
+ securitySchemes:
+ basicAuth:
+ type: http
+ scheme: basic
+ bearerAuth:
+ type: http
+ scheme: bearer
+ bearerFormat: JWT
+ apiKey:
+ type: apiKey
+ in: header
+ name: x-api-key
diff --git a/test/typescript-generator/__snapshots__/generate.test.ts.snap b/test/typescript-generator/__snapshots__/generate.test.ts.snap
index 567127e01..77a3b4c2c 100644
--- a/test/typescript-generator/__snapshots__/generate.test.ts.snap
+++ b/test/typescript-generator/__snapshots__/generate.test.ts.snap
@@ -1520,9 +1520,9 @@ exports[`end-to-end test generates the same code for pet store that it did on th
`;
exports[`end-to-end test generates the same code for the example that it did on the last test run 1`] = `
-"routes/index.ts:import type { HTTP_GET } from "../types/paths/index.types.js";
+"routes/index.ts:import type { getRoot } from "../types/paths/index.types.js";
-export const GET: HTTP_GET = async ($) => {
+export const GET: getRoot = async ($) => {
return $.response[200].random();
};
"
@@ -1544,7 +1544,7 @@ import type { HttpStatusCode } from "../../counterfact-types/index.ts";
/**
* the root
*/
-export type HTTP_GET = (
+export type getRoot = (
$: OmitValueWhenNever<{
query: never;
path: never;
@@ -1585,9 +1585,9 @@ export type HTTP_GET = (
`;
exports[`end-to-end test generates the same code for the example that it did on the last test run 3`] = `
-"routes/count.ts:import type { HTTP_GET } from "../types/paths/count.types.js";
+"routes/count.ts:import type { getCount } from "../types/paths/count.types.js";
-export const GET: HTTP_GET = async ($) => {
+export const GET: getCount = async ($) => {
return $.response[200].random();
};
"
@@ -1604,12 +1604,13 @@ import type { MaybePromise } from "../../counterfact-types/index.ts";
import type { COUNTERFACT_RESPONSE } from "../../counterfact-types/index.ts";
import type { Context } from "@@CONTEXT_FILE_TOKEN@@";
import type { ResponseBuilderFactory } from "../../counterfact-types/index.ts";
-import type { HttpStatusCode } from "../../counterfact-types/index.ts";
+import type { InternalServerError } from "../#/components/responses/InternalServerError.js";
+import type { Error } from "../components/schemas/Error.js";
/**
- * outputs the number of time each URL was visited
+ * outputs the number of times each URL was visited
*/
-export type HTTP_GET = (
+export type getCount = (
$: OmitValueWhenNever<{
query: never;
path: never;
@@ -1618,18 +1619,17 @@ export type HTTP_GET = (
body: never;
context: Context;
response: ResponseBuilderFactory<{
- [statusCode in HttpStatusCode]: {
+ 200: {
headers: never;
requiredHeaders: never;
content: {
"application/json": {
- schema: string;
+ schema: number;
};
};
- examples: {
- "no visits": unknown;
- };
+ examples: {};
};
+ 500: InternalServerError;
}>;
x: WideOperationArgument;
proxy: (url: string) => COUNTERFACT_RESPONSE;
@@ -1638,9 +1638,14 @@ export type HTTP_GET = (
}>,
) => MaybePromise<
| {
- status: number | undefined;
+ status: 200;
contentType?: "application/json";
- body?: string;
+ body?: number;
+ }
+ | {
+ status: 500;
+ contentType?: "application/json";
+ body?: Error;
}
| { status: 415; contentType: "text/plain"; body: string }
| COUNTERFACT_RESPONSE
@@ -1715,9 +1720,9 @@ export type helloKitty = (
`;
exports[`end-to-end test generates the same code for the example that it did on the last test run 7`] = `
-"routes/hello/{name}.ts:import type { HTTP_GET } from "../../types/paths/hello/{name}.types.js";
+"routes/hello/{name}.ts:import type { sayHello } from "../../types/paths/hello/{name}.types.js";
-export const GET: HTTP_GET = async ($) => {
+export const GET: sayHello = async ($) => {
return $.response[200].random();
};
"
@@ -1734,23 +1739,25 @@ import type { MaybePromise } from "../../../counterfact-types/index.ts";
import type { COUNTERFACT_RESPONSE } from "../../../counterfact-types/index.ts";
import type { Context } from "@@CONTEXT_FILE_TOKEN@@";
import type { ResponseBuilderFactory } from "../../../counterfact-types/index.ts";
-import type { HttpStatusCode } from "../../../counterfact-types/index.ts";
+import type { BadRequest } from "../../#/components/responses/BadRequest.js";
+import type { Error } from "../../components/schemas/Error.js";
/**
* says hello to someone
*/
-export type HTTP_GET = (
+export type sayHello = (
$: OmitValueWhenNever<{
- query: never;
- path: HTTP_GET_Path;
- headers: never;
+ query: sayHello_Query;
+ path: sayHello_Path;
+ headers: sayHello_Headers;
cookie: never;
body: never;
context: Context;
response: ResponseBuilderFactory<{
- [statusCode in HttpStatusCode]: {
+ 200: {
headers: {
"x-test": { schema: string };
+ "x-rate-limit-remaining": { schema: number };
};
requiredHeaders: never;
content: {
@@ -1763,6 +1770,7 @@ export type HTTP_GET = (
"hello-example2": unknown;
};
};
+ 400: BadRequest;
}>;
x: WideOperationArgument;
proxy: (url: string) => COUNTERFACT_RESPONSE;
@@ -1771,28 +1779,47 @@ export type HTTP_GET = (
}>,
) => MaybePromise<
| {
- status: number | undefined;
+ status: 200;
contentType?: "application/json";
body?: string;
}
+ | {
+ status: 400;
+ contentType?: "application/json";
+ body?: Error;
+ }
| { status: 415; contentType: "text/plain"; body: string }
| COUNTERFACT_RESPONSE
| { ALL_REMAINING_HEADERS_ARE_OPTIONAL: COUNTERFACT_RESPONSE }
>;
-export type HTTP_GET_Path = {
+export type sayHello_Query = {
+ /**
+ * language for the greeting
+ */
+ language?: "en" | "es" | "fr" | "de" | "ja";
+};
+
+export type sayHello_Path = {
/**
- * says hello to the name
+ * the name to greet
*/
name: string;
};
+
+export type sayHello_Headers = {
+ /**
+ * idempotency key
+ */
+ "x-request-id"?: string;
+};
"
`;
exports[`end-to-end test generates the same code for the example that it did on the last test run 9`] = `
-"routes/path-one.ts:import type { HTTP_GET } from "../types/paths/path-one.types.js";
+"routes/path-one.ts:import type { getPathOne } from "../types/paths/path-one.types.js";
-export const GET: HTTP_GET = async ($) => {
+export const GET: getPathOne = async ($) => {
return $.response[200].random();
};
"
@@ -1813,7 +1840,7 @@ import type { Recursive } from "../components/schemas/Recursive.js";
import type { BadRequest } from "../#/components/responses/BadRequest.js";
import type { Error } from "../components/schemas/Error.js";
-export type HTTP_GET = (
+export type getPathOne = (
$: OmitValueWhenNever<{
query: never;
path: never;
@@ -1858,9 +1885,9 @@ export type HTTP_GET = (
`;
exports[`end-to-end test generates the same code for the example that it did on the last test run 11`] = `
-"routes/weird/path/with:colon.ts:import type { HTTP_GET } from "../../../types/paths/weird/path/with∶colon.types.js";
+"routes/weird/path/with:colon.ts:import type { weirdColonPath } from "../../../types/paths/weird/path/with∶colon.types.js";
-export const GET: HTTP_GET = async ($) => {
+export const GET: weirdColonPath = async ($) => {
return $.response[200].random();
};
"
@@ -1878,7 +1905,7 @@ import type { COUNTERFACT_RESPONSE } from "../../../../counterfact-types/index.t
import type { Context } from "@@CONTEXT_FILE_TOKEN@@";
import type { ResponseBuilderFactory } from "../../../../counterfact-types/index.ts";
-export type HTTP_GET = (
+export type weirdColonPath = (
$: OmitValueWhenNever<{
query: never;
path: never;
@@ -1917,51 +1944,1224 @@ export type HTTP_GET = (
`;
exports[`end-to-end test generates the same code for the example that it did on the last test run 13`] = `
-"types/components/schemas/Recursive.ts:import type { Recursive } from "./Recursive.js";
+"routes/users.ts:import type { listUsers } from "../types/paths/users.types.js";
+import type { createUser } from "../types/paths/users.types.js";
-export type Recursive = Recursive;
+export const GET: listUsers = async ($) => {
+ return $.response[200].random();
+};
+
+export const POST: createUser = async ($) => {
+ return $.response[201].random();
+};
"
`;
exports[`end-to-end test generates the same code for the example that it did on the last test run 14`] = `
-"types/#/components/responses/BadRequest.ts:import type { Error } from "../../../components/schemas/Error.js";
+"types/paths/users.types.ts:// This code was automatically generated from an OpenAPI description.
+// Do not edit this file. Edit the OpenAPI file instead.
+// For more information, see https://github.com/pmcelhaney/counterfact/blob/main/docs/faq.md
-export type BadRequest = {
- headers: never;
- requiredHeaders: never;
- content: {
- "application/json": {
- schema: Error;
- };
- };
- examples: {};
+import type { WideOperationArgument } from "../../counterfact-types/index.ts";
+import type { OmitValueWhenNever } from "../../counterfact-types/index.ts";
+import type { MaybePromise } from "../../counterfact-types/index.ts";
+import type { COUNTERFACT_RESPONSE } from "../../counterfact-types/index.ts";
+import type { Context } from "@@CONTEXT_FILE_TOKEN@@";
+import type { ResponseBuilderFactory } from "../../counterfact-types/index.ts";
+import type { UserList } from "../components/schemas/UserList.js";
+import type { BadRequest } from "../#/components/responses/BadRequest.js";
+import type { Unauthorized } from "../#/components/responses/Unauthorized.js";
+import type { InternalServerError } from "../#/components/responses/InternalServerError.js";
+import type { Error } from "../components/schemas/Error.js";
+import type { CreateUserRequest } from "../components/schemas/CreateUserRequest.js";
+import type { User } from "../components/schemas/User.js";
+
+/**
+ * List all users with optional filtering and pagination
+ */
+export type listUsers = (
+ $: OmitValueWhenNever<{
+ query: listUsers_Query;
+ path: never;
+ headers: listUsers_Headers;
+ cookie: never;
+ body: never;
+ context: Context;
+ response: ResponseBuilderFactory<{
+ 200: {
+ headers: {
+ "x-total-count": { schema: number };
+ "x-next-page": { schema: string };
+ };
+ requiredHeaders: never;
+ content: {
+ "application/json": {
+ schema: UserList;
+ };
+ };
+ examples: {
+ empty: unknown;
+ withUsers: unknown;
+ };
+ };
+ 400: BadRequest;
+ 401: Unauthorized;
+ 500: InternalServerError;
+ }>;
+ x: WideOperationArgument;
+ proxy: (url: string) => COUNTERFACT_RESPONSE;
+ user: { username?: string; password?: string };
+ delay: (milliseconds: number, maxMilliseconds?: number) => Promise;
+ }>,
+) => MaybePromise<
+ | {
+ status: 200;
+ contentType?: "application/json";
+ body?: UserList;
+ }
+ | {
+ status: 400;
+ contentType?: "application/json";
+ body?: Error;
+ }
+ | {
+ status: 401;
+ contentType?: "application/json";
+ body?: Error;
+ }
+ | {
+ status: 500;
+ contentType?: "application/json";
+ body?: Error;
+ }
+ | { status: 415; contentType: "text/plain"; body: string }
+ | COUNTERFACT_RESPONSE
+ | { ALL_REMAINING_HEADERS_ARE_OPTIONAL: COUNTERFACT_RESPONSE }
+>;
+
+/**
+ * Create a new user
+ */
+export type createUser = (
+ $: OmitValueWhenNever<{
+ query: never;
+ path: never;
+ headers: never;
+ cookie: never;
+ body: CreateUserRequest;
+ context: Context;
+ response: ResponseBuilderFactory<{
+ 201: {
+ headers: {
+ location: { schema: string };
+ };
+ requiredHeaders: never;
+ content: {
+ "application/json": {
+ schema: User;
+ };
+ };
+ examples: {};
+ };
+ 400: BadRequest;
+ 409: {
+ headers: never;
+ requiredHeaders: never;
+ content: {
+ "application/json": {
+ schema: Error;
+ };
+ };
+ examples: {};
+ };
+ 500: InternalServerError;
+ }>;
+ x: WideOperationArgument;
+ proxy: (url: string) => COUNTERFACT_RESPONSE;
+ user: { username?: string; password?: string };
+ delay: (milliseconds: number, maxMilliseconds?: number) => Promise;
+ }>,
+) => MaybePromise<
+ | {
+ status: 201;
+ contentType?: "application/json";
+ body?: User;
+ }
+ | {
+ status: 400;
+ contentType?: "application/json";
+ body?: Error;
+ }
+ | {
+ status: 409;
+ contentType?: "application/json";
+ body?: Error;
+ }
+ | {
+ status: 500;
+ contentType?: "application/json";
+ body?: Error;
+ }
+ | { status: 415; contentType: "text/plain"; body: string }
+ | COUNTERFACT_RESPONSE
+ | { ALL_REMAINING_HEADERS_ARE_OPTIONAL: COUNTERFACT_RESPONSE }
+>;
+
+export type listUsers_Query = {
+ page?: number;
+ pageSize?: number;
+ active?: boolean;
+ role?: "admin" | "editor" | "viewer";
};
+
+export type listUsers_Headers = { "x-correlation-id"?: string };
"
`;
exports[`end-to-end test generates the same code for the example that it did on the last test run 15`] = `
-"types/components/schemas/Error.ts:/**
- * A generic error message suitable for 4xx and 5xx responses
- */
-export type Error = {
- /**
- * A machine readable error code
- */
- code?: string;
- /**
- * A detailed description of the error
- */
- message: string;
+"routes/users/{userId}.ts:import type { getUser } from "../../types/paths/users/{userId}.types.js";
+import type { replaceUser } from "../../types/paths/users/{userId}.types.js";
+import type { patchUser } from "../../types/paths/users/{userId}.types.js";
+import type { deleteUser } from "../../types/paths/users/{userId}.types.js";
+
+export const GET: getUser = async ($) => {
+ return $.response[200].random();
+};
+
+export const PUT: replaceUser = async ($) => {
+ return $.response[200].random();
+};
+
+export const PATCH: patchUser = async ($) => {
+ return $.response[200].random();
+};
+
+export const DELETE: deleteUser = async ($) => {
+ return $.response[204].empty();
};
"
`;
exports[`end-to-end test generates the same code for the example that it did on the last test run 16`] = `
-".cache
-"
-`;
+"types/paths/users/{userId}.types.ts:// This code was automatically generated from an OpenAPI description.
+// Do not edit this file. Edit the OpenAPI file instead.
+// For more information, see https://github.com/pmcelhaney/counterfact/blob/main/docs/faq.md
-exports[`end-to-end test generates the same code for the example that it did on the last test run 17`] = `
+import type { WideOperationArgument } from "../../../counterfact-types/index.ts";
+import type { OmitValueWhenNever } from "../../../counterfact-types/index.ts";
+import type { MaybePromise } from "../../../counterfact-types/index.ts";
+import type { COUNTERFACT_RESPONSE } from "../../../counterfact-types/index.ts";
+import type { Context } from "@@CONTEXT_FILE_TOKEN@@";
+import type { ResponseBuilderFactory } from "../../../counterfact-types/index.ts";
+import type { User } from "../../components/schemas/User.js";
+import type { NotFound } from "../../#/components/responses/NotFound.js";
+import type { InternalServerError } from "../../#/components/responses/InternalServerError.js";
+import type { Error } from "../../components/schemas/Error.js";
+import type { CreateUserRequest } from "../../components/schemas/CreateUserRequest.js";
+import type { BadRequest } from "../../#/components/responses/BadRequest.js";
+import type { PatchUserRequest } from "../../components/schemas/PatchUserRequest.js";
+
+/**
+ * Fetch a single user by ID
+ */
+export type getUser = (
+ $: OmitValueWhenNever<{
+ query: never;
+ path: getUser_Path;
+ headers: never;
+ cookie: never;
+ body: never;
+ context: Context;
+ response: ResponseBuilderFactory<{
+ 200: {
+ headers: never;
+ requiredHeaders: never;
+ content: {
+ "application/json": {
+ schema: User;
+ };
+ };
+ examples: {};
+ };
+ 404: NotFound;
+ 500: InternalServerError;
+ }>;
+ x: WideOperationArgument;
+ proxy: (url: string) => COUNTERFACT_RESPONSE;
+ user: { username?: string; password?: string };
+ delay: (milliseconds: number, maxMilliseconds?: number) => Promise;
+ }>,
+) => MaybePromise<
+ | {
+ status: 200;
+ contentType?: "application/json";
+ body?: User;
+ }
+ | {
+ status: 404;
+ contentType?: "application/json";
+ body?: Error;
+ }
+ | {
+ status: 500;
+ contentType?: "application/json";
+ body?: Error;
+ }
+ | { status: 415; contentType: "text/plain"; body: string }
+ | COUNTERFACT_RESPONSE
+ | { ALL_REMAINING_HEADERS_ARE_OPTIONAL: COUNTERFACT_RESPONSE }
+>;
+
+/**
+ * Replace a user record entirely (idempotent)
+ */
+export type replaceUser = (
+ $: OmitValueWhenNever<{
+ query: never;
+ path: replaceUser_Path;
+ headers: never;
+ cookie: never;
+ body: CreateUserRequest;
+ context: Context;
+ response: ResponseBuilderFactory<{
+ 200: {
+ headers: never;
+ requiredHeaders: never;
+ content: {
+ "application/json": {
+ schema: User;
+ };
+ };
+ examples: {};
+ };
+ 400: BadRequest;
+ 404: NotFound;
+ }>;
+ x: WideOperationArgument;
+ proxy: (url: string) => COUNTERFACT_RESPONSE;
+ user: { username?: string; password?: string };
+ delay: (milliseconds: number, maxMilliseconds?: number) => Promise;
+ }>,
+) => MaybePromise<
+ | {
+ status: 200;
+ contentType?: "application/json";
+ body?: User;
+ }
+ | {
+ status: 400;
+ contentType?: "application/json";
+ body?: Error;
+ }
+ | {
+ status: 404;
+ contentType?: "application/json";
+ body?: Error;
+ }
+ | { status: 415; contentType: "text/plain"; body: string }
+ | COUNTERFACT_RESPONSE
+ | { ALL_REMAINING_HEADERS_ARE_OPTIONAL: COUNTERFACT_RESPONSE }
+>;
+
+/**
+ * Partially update a user record
+ */
+export type patchUser = (
+ $: OmitValueWhenNever<{
+ query: never;
+ path: patchUser_Path;
+ headers: never;
+ cookie: never;
+ body: PatchUserRequest;
+ context: Context;
+ response: ResponseBuilderFactory<{
+ 200: {
+ headers: never;
+ requiredHeaders: never;
+ content: {
+ "application/json": {
+ schema: User;
+ };
+ };
+ examples: {};
+ };
+ 400: BadRequest;
+ 404: NotFound;
+ }>;
+ x: WideOperationArgument;
+ proxy: (url: string) => COUNTERFACT_RESPONSE;
+ user: { username?: string; password?: string };
+ delay: (milliseconds: number, maxMilliseconds?: number) => Promise;
+ }>,
+) => MaybePromise<
+ | {
+ status: 200;
+ contentType?: "application/json";
+ body?: User;
+ }
+ | {
+ status: 400;
+ contentType?: "application/json";
+ body?: Error;
+ }
+ | {
+ status: 404;
+ contentType?: "application/json";
+ body?: Error;
+ }
+ | { status: 415; contentType: "text/plain"; body: string }
+ | COUNTERFACT_RESPONSE
+ | { ALL_REMAINING_HEADERS_ARE_OPTIONAL: COUNTERFACT_RESPONSE }
+>;
+
+/**
+ * Delete a user (soft-delete)
+ */
+export type deleteUser = (
+ $: OmitValueWhenNever<{
+ query: never;
+ path: deleteUser_Path;
+ headers: never;
+ cookie: never;
+ body: never;
+ context: Context;
+ response: ResponseBuilderFactory<{
+ 204: {
+ headers: never;
+ requiredHeaders: never;
+ content: never;
+ examples: {};
+ };
+ 404: NotFound;
+ }>;
+ x: WideOperationArgument;
+ proxy: (url: string) => COUNTERFACT_RESPONSE;
+ user: { username?: string; password?: string };
+ delay: (milliseconds: number, maxMilliseconds?: number) => Promise;
+ }>,
+) => MaybePromise<
+ | {
+ status: 204;
+ }
+ | {
+ status: 404;
+ contentType?: "application/json";
+ body?: Error;
+ }
+ | { status: 415; contentType: "text/plain"; body: string }
+ | COUNTERFACT_RESPONSE
+ | { ALL_REMAINING_HEADERS_ARE_OPTIONAL: COUNTERFACT_RESPONSE }
+>;
+
+export type getUser_Path = {
+ /**
+ * Unique user identifier
+ */
+ userId: string;
+};
+
+export type replaceUser_Path = {
+ /**
+ * Unique user identifier
+ */
+ userId: string;
+};
+
+export type patchUser_Path = {
+ /**
+ * Unique user identifier
+ */
+ userId: string;
+};
+
+export type deleteUser_Path = {
+ /**
+ * Unique user identifier
+ */
+ userId: string;
+};
+"
+`;
+
+exports[`end-to-end test generates the same code for the example that it did on the last test run 17`] = `
+"routes/events.ts:import type { createEvent } from "../types/paths/events.types.js";
+
+export const POST: createEvent = async ($) => {
+ return $.response[202].empty();
+};
+"
+`;
+
+exports[`end-to-end test generates the same code for the example that it did on the last test run 18`] = `
+"types/paths/events.types.ts:// This code was automatically generated from an OpenAPI description.
+// Do not edit this file. Edit the OpenAPI file instead.
+// For more information, see https://github.com/pmcelhaney/counterfact/blob/main/docs/faq.md
+
+import type { WideOperationArgument } from "../../counterfact-types/index.ts";
+import type { OmitValueWhenNever } from "../../counterfact-types/index.ts";
+import type { MaybePromise } from "../../counterfact-types/index.ts";
+import type { COUNTERFACT_RESPONSE } from "../../counterfact-types/index.ts";
+import type { Context } from "@@CONTEXT_FILE_TOKEN@@";
+import type { ResponseBuilderFactory } from "../../counterfact-types/index.ts";
+import type { ClickEvent } from "../components/schemas/ClickEvent.js";
+import type { PurchaseEvent } from "../components/schemas/PurchaseEvent.js";
+import type { SignUpEvent } from "../components/schemas/SignUpEvent.js";
+import type { BadRequest } from "../#/components/responses/BadRequest.js";
+import type { Error } from "../components/schemas/Error.js";
+
+/**
+ * Accept a polymorphic event. The request body may be any one of the known event subtypes (oneOf), demonstrating discriminator support.
+ *
+ */
+export type createEvent = (
+ $: OmitValueWhenNever<{
+ query: never;
+ path: never;
+ headers: never;
+ cookie: never;
+ body: ClickEvent | PurchaseEvent | SignUpEvent;
+ context: Context;
+ response: ResponseBuilderFactory<{
+ 202: {
+ headers: never;
+ requiredHeaders: never;
+ content: never;
+ examples: {};
+ };
+ 400: BadRequest;
+ }>;
+ x: WideOperationArgument;
+ proxy: (url: string) => COUNTERFACT_RESPONSE;
+ user: { username?: string; password?: string };
+ delay: (milliseconds: number, maxMilliseconds?: number) => Promise;
+ }>,
+) => MaybePromise<
+ | {
+ status: 202;
+ }
+ | {
+ status: 400;
+ contentType?: "application/json";
+ body?: Error;
+ }
+ | { status: 415; contentType: "text/plain"; body: string }
+ | COUNTERFACT_RESPONSE
+ | { ALL_REMAINING_HEADERS_ARE_OPTIONAL: COUNTERFACT_RESPONSE }
+>;
+"
+`;
+
+exports[`end-to-end test generates the same code for the example that it did on the last test run 19`] = `
+"routes/products/{productId}.ts:import type { getProduct } from "../../types/paths/products/{productId}.types.js";
+
+export const GET: getProduct = async ($) => {
+ return $.response[200].random();
+};
+"
+`;
+
+exports[`end-to-end test generates the same code for the example that it did on the last test run 20`] = `
+"types/paths/products/{productId}.types.ts:// This code was automatically generated from an OpenAPI description.
+// Do not edit this file. Edit the OpenAPI file instead.
+// For more information, see https://github.com/pmcelhaney/counterfact/blob/main/docs/faq.md
+
+import type { WideOperationArgument } from "../../../counterfact-types/index.ts";
+import type { OmitValueWhenNever } from "../../../counterfact-types/index.ts";
+import type { MaybePromise } from "../../../counterfact-types/index.ts";
+import type { COUNTERFACT_RESPONSE } from "../../../counterfact-types/index.ts";
+import type { Context } from "@@CONTEXT_FILE_TOKEN@@";
+import type { ResponseBuilderFactory } from "../../../counterfact-types/index.ts";
+import type { Product } from "../../components/schemas/Product.js";
+import type { NotFound } from "../../#/components/responses/NotFound.js";
+import type { Error } from "../../components/schemas/Error.js";
+
+export type getProduct = (
+ $: OmitValueWhenNever<{
+ query: never;
+ path: getProduct_Path;
+ headers: never;
+ cookie: never;
+ body: never;
+ context: Context;
+ response: ResponseBuilderFactory<{
+ 200: {
+ headers: never;
+ requiredHeaders: never;
+ content: {
+ "application/json": {
+ schema: Product;
+ };
+ };
+ examples: {};
+ };
+ 404: NotFound;
+ }>;
+ x: WideOperationArgument;
+ proxy: (url: string) => COUNTERFACT_RESPONSE;
+ user: { username?: string; password?: string };
+ delay: (milliseconds: number, maxMilliseconds?: number) => Promise;
+ }>,
+) => MaybePromise<
+ | {
+ status: 200;
+ contentType?: "application/json";
+ body?: Product;
+ }
+ | {
+ status: 404;
+ contentType?: "application/json";
+ body?: Error;
+ }
+ | { status: 415; contentType: "text/plain"; body: string }
+ | COUNTERFACT_RESPONSE
+ | { ALL_REMAINING_HEADERS_ARE_OPTIONAL: COUNTERFACT_RESPONSE }
+>;
+
+export type getProduct_Path = { productId: number };
+"
+`;
+
+exports[`end-to-end test generates the same code for the example that it did on the last test run 21`] = `
+"routes/uploads.ts:import type { uploadFile } from "../types/paths/uploads.types.js";
+
+export const POST: uploadFile = async ($) => {
+ return $.response[201].random();
+};
+"
+`;
+
+exports[`end-to-end test generates the same code for the example that it did on the last test run 22`] = `
+"types/paths/uploads.types.ts:// This code was automatically generated from an OpenAPI description.
+// Do not edit this file. Edit the OpenAPI file instead.
+// For more information, see https://github.com/pmcelhaney/counterfact/blob/main/docs/faq.md
+
+import type { WideOperationArgument } from "../../counterfact-types/index.ts";
+import type { OmitValueWhenNever } from "../../counterfact-types/index.ts";
+import type { MaybePromise } from "../../counterfact-types/index.ts";
+import type { COUNTERFACT_RESPONSE } from "../../counterfact-types/index.ts";
+import type { Context } from "@@CONTEXT_FILE_TOKEN@@";
+import type { ResponseBuilderFactory } from "../../counterfact-types/index.ts";
+import type { Error } from "../components/schemas/Error.js";
+
+/**
+ * Upload a binary file with metadata
+ */
+export type uploadFile = (
+ $: OmitValueWhenNever<{
+ query: never;
+ path: never;
+ headers: never;
+ cookie: never;
+ body: never;
+ context: Context;
+ response: ResponseBuilderFactory<{
+ 201: {
+ headers: never;
+ requiredHeaders: never;
+ content: {
+ "application/json": {
+ schema: {
+ id?: string;
+ /**
+ * @format uri
+ */
+ url?: string;
+ };
+ };
+ };
+ examples: {};
+ };
+ 413: {
+ headers: never;
+ requiredHeaders: never;
+ content: {
+ "application/json": {
+ schema: Error;
+ };
+ };
+ examples: {};
+ };
+ }>;
+ x: WideOperationArgument;
+ proxy: (url: string) => COUNTERFACT_RESPONSE;
+ user: { username?: string; password?: string };
+ delay: (milliseconds: number, maxMilliseconds?: number) => Promise;
+ }>,
+) => MaybePromise<
+ | {
+ status: 201;
+ contentType?: "application/json";
+ body?: {
+ id?: string;
+ /**
+ * @format uri
+ */
+ url?: string;
+ };
+ }
+ | {
+ status: 413;
+ contentType?: "application/json";
+ body?: Error;
+ }
+ | { status: 415; contentType: "text/plain"; body: string }
+ | COUNTERFACT_RESPONSE
+ | { ALL_REMAINING_HEADERS_ARE_OPTIONAL: COUNTERFACT_RESPONSE }
+>;
+"
+`;
+
+exports[`end-to-end test generates the same code for the example that it did on the last test run 23`] = `
+"routes/legacy/items.ts:import type { listItemsLegacy } from "../../types/paths/legacy/items.types.js";
+
+export const GET: listItemsLegacy = async ($) => {
+ return $.response[200].random();
+};
+"
+`;
+
+exports[`end-to-end test generates the same code for the example that it did on the last test run 24`] = `
+"types/paths/legacy/items.types.ts:// This code was automatically generated from an OpenAPI description.
+// Do not edit this file. Edit the OpenAPI file instead.
+// For more information, see https://github.com/pmcelhaney/counterfact/blob/main/docs/faq.md
+
+import type { WideOperationArgument } from "../../../counterfact-types/index.ts";
+import type { OmitValueWhenNever } from "../../../counterfact-types/index.ts";
+import type { MaybePromise } from "../../../counterfact-types/index.ts";
+import type { COUNTERFACT_RESPONSE } from "../../../counterfact-types/index.ts";
+import type { Context } from "@@CONTEXT_FILE_TOKEN@@";
+import type { ResponseBuilderFactory } from "../../../counterfact-types/index.ts";
+
+/**
+ * Deprecated – use /users instead
+ * @deprecated
+ */
+export type listItemsLegacy = (
+ $: OmitValueWhenNever<{
+ query: never;
+ path: never;
+ headers: never;
+ cookie: listItemsLegacy_Cookie;
+ body: never;
+ context: Context;
+ response: ResponseBuilderFactory<{
+ 200: {
+ headers: never;
+ requiredHeaders: never;
+ content: {
+ "application/json": {
+ schema: Array;
+ };
+ };
+ examples: {};
+ };
+ }>;
+ x: WideOperationArgument;
+ proxy: (url: string) => COUNTERFACT_RESPONSE;
+ user: { username?: string; password?: string };
+ delay: (milliseconds: number, maxMilliseconds?: number) => Promise;
+ }>,
+) => MaybePromise<
+ | {
+ status: 200;
+ contentType?: "application/json";
+ body?: Array;
+ }
+ | { status: 415; contentType: "text/plain"; body: string }
+ | COUNTERFACT_RESPONSE
+ | { ALL_REMAINING_HEADERS_ARE_OPTIONAL: COUNTERFACT_RESPONSE }
+>;
+
+export type listItemsLegacy_Cookie = { session_id?: string };
+"
+`;
+
+exports[`end-to-end test generates the same code for the example that it did on the last test run 25`] = `
+"routes/reports/{reportId}.ts:import type { getReport } from "../../types/paths/reports/{reportId}.types.js";
+
+export const GET: getReport = async ($) => {
+ return $.response[200].random();
+};
+"
+`;
+
+exports[`end-to-end test generates the same code for the example that it did on the last test run 26`] = `
+"types/paths/reports/{reportId}.types.ts:// This code was automatically generated from an OpenAPI description.
+// Do not edit this file. Edit the OpenAPI file instead.
+// For more information, see https://github.com/pmcelhaney/counterfact/blob/main/docs/faq.md
+
+import type { WideOperationArgument } from "../../../counterfact-types/index.ts";
+import type { OmitValueWhenNever } from "../../../counterfact-types/index.ts";
+import type { MaybePromise } from "../../../counterfact-types/index.ts";
+import type { COUNTERFACT_RESPONSE } from "../../../counterfact-types/index.ts";
+import type { Context } from "@@CONTEXT_FILE_TOKEN@@";
+import type { ResponseBuilderFactory } from "../../../counterfact-types/index.ts";
+import type { NotFound } from "../../#/components/responses/NotFound.js";
+import type { Error } from "../../components/schemas/Error.js";
+
+/**
+ * Download a report in the requested format
+ */
+export type getReport = (
+ $: OmitValueWhenNever<{
+ query: never;
+ path: getReport_Path;
+ headers: getReport_Headers;
+ cookie: never;
+ body: never;
+ context: Context;
+ response: ResponseBuilderFactory<{
+ 200: {
+ headers: never;
+ requiredHeaders: never;
+ content: {
+ "application/json": {
+ schema: { rows?: Array<{ [key: string]: unknown }> };
+ };
+ "text/csv": {
+ schema: string;
+ };
+ "application/pdf": {
+ schema: Uint8Array | string;
+ };
+ };
+ examples: {};
+ };
+ 404: NotFound;
+ }>;
+ x: WideOperationArgument;
+ proxy: (url: string) => COUNTERFACT_RESPONSE;
+ user: { username?: string; password?: string };
+ delay: (milliseconds: number, maxMilliseconds?: number) => Promise;
+ }>,
+) => MaybePromise<
+ | {
+ status: 200;
+ contentType?: "application/json";
+ body?: { rows?: Array<{ [key: string]: unknown }> };
+ }
+ | {
+ status: 200;
+ contentType?: "text/csv";
+ body?: string;
+ }
+ | {
+ status: 200;
+ contentType?: "application/pdf";
+ body?: Uint8Array | string;
+ }
+ | {
+ status: 404;
+ contentType?: "application/json";
+ body?: Error;
+ }
+ | { status: 415; contentType: "text/plain"; body: string }
+ | COUNTERFACT_RESPONSE
+ | { ALL_REMAINING_HEADERS_ARE_OPTIONAL: COUNTERFACT_RESPONSE }
+>;
+
+export type getReport_Path = { reportId: string };
+
+export type getReport_Headers = {
+ Accept?: "application/json" | "text/csv" | "application/pdf";
+};
+"
+`;
+
+exports[`end-to-end test generates the same code for the example that it did on the last test run 27`] = `
+"routes/ping.ts:import type { ping } from "../types/paths/ping.types.js";
+
+export const GET: ping = async ($) => {
+ return $.response[204].empty();
+};
+"
+`;
+
+exports[`end-to-end test generates the same code for the example that it did on the last test run 28`] = `
+"types/paths/ping.types.ts:// This code was automatically generated from an OpenAPI description.
+// Do not edit this file. Edit the OpenAPI file instead.
+// For more information, see https://github.com/pmcelhaney/counterfact/blob/main/docs/faq.md
+
+import type { WideOperationArgument } from "../../counterfact-types/index.ts";
+import type { OmitValueWhenNever } from "../../counterfact-types/index.ts";
+import type { MaybePromise } from "../../counterfact-types/index.ts";
+import type { COUNTERFACT_RESPONSE } from "../../counterfact-types/index.ts";
+import type { Context } from "@@CONTEXT_FILE_TOKEN@@";
+import type { ResponseBuilderFactory } from "../../counterfact-types/index.ts";
+
+/**
+ * Health-check – returns 204 with no body
+ */
+export type ping = (
+ $: OmitValueWhenNever<{
+ query: never;
+ path: never;
+ headers: never;
+ cookie: never;
+ body: never;
+ context: Context;
+ response: ResponseBuilderFactory<{
+ 204: {
+ headers: never;
+ requiredHeaders: never;
+ content: never;
+ examples: {};
+ };
+ }>;
+ x: WideOperationArgument;
+ proxy: (url: string) => COUNTERFACT_RESPONSE;
+ user: { username?: string; password?: string };
+ delay: (milliseconds: number, maxMilliseconds?: number) => Promise;
+ }>,
+) => MaybePromise<
+ | {
+ status: 204;
+ }
+ | { status: 415; contentType: "text/plain"; body: string }
+ | COUNTERFACT_RESPONSE
+ | { ALL_REMAINING_HEADERS_ARE_OPTIONAL: COUNTERFACT_RESPONSE }
+>;
+"
+`;
+
+exports[`end-to-end test generates the same code for the example that it did on the last test run 29`] = `
+"routes/metadata.ts:import type { setMetadata } from "../types/paths/metadata.types.js";
+
+export const PUT: setMetadata = async ($) => {
+ return $.response[200].random();
+};
+"
+`;
+
+exports[`end-to-end test generates the same code for the example that it did on the last test run 30`] = `
+"types/paths/metadata.types.ts:// This code was automatically generated from an OpenAPI description.
+// Do not edit this file. Edit the OpenAPI file instead.
+// For more information, see https://github.com/pmcelhaney/counterfact/blob/main/docs/faq.md
+
+import type { WideOperationArgument } from "../../counterfact-types/index.ts";
+import type { OmitValueWhenNever } from "../../counterfact-types/index.ts";
+import type { MaybePromise } from "../../counterfact-types/index.ts";
+import type { COUNTERFACT_RESPONSE } from "../../counterfact-types/index.ts";
+import type { Context } from "@@CONTEXT_FILE_TOKEN@@";
+import type { ResponseBuilderFactory } from "../../counterfact-types/index.ts";
+
+/**
+ * Replace the free-form metadata store
+ */
+export type setMetadata = (
+ $: OmitValueWhenNever<{
+ query: never;
+ path: never;
+ headers: never;
+ cookie: never;
+ body: { [key: string]: unknown };
+ context: Context;
+ response: ResponseBuilderFactory<{
+ 200: {
+ headers: never;
+ requiredHeaders: never;
+ content: {
+ "application/json": {
+ schema: { [key: string]: unknown };
+ };
+ };
+ examples: {};
+ };
+ }>;
+ x: WideOperationArgument;
+ proxy: (url: string) => COUNTERFACT_RESPONSE;
+ user: { username?: string; password?: string };
+ delay: (milliseconds: number, maxMilliseconds?: number) => Promise;
+ }>,
+) => MaybePromise<
+ | {
+ status: 200;
+ contentType?: "application/json";
+ body?: { [key: string]: unknown };
+ }
+ | { status: 415; contentType: "text/plain"; body: string }
+ | COUNTERFACT_RESPONSE
+ | { ALL_REMAINING_HEADERS_ARE_OPTIONAL: COUNTERFACT_RESPONSE }
+>;
+"
+`;
+
+exports[`end-to-end test generates the same code for the example that it did on the last test run 31`] = `
+"types/#/components/responses/InternalServerError.ts:import type { Error } from "../../../components/schemas/Error.js";
+
+export type InternalServerError = {
+ headers: never;
+ requiredHeaders: never;
+ content: {
+ "application/json": {
+ schema: Error;
+ };
+ };
+ examples: {};
+};
+"
+`;
+
+exports[`end-to-end test generates the same code for the example that it did on the last test run 32`] = `
+"types/components/schemas/Error.ts:/**
+ * A generic error message suitable for 4xx and 5xx responses
+ */
+export type Error = {
+ /**
+ * A machine-readable error code
+ */
+ code?: string;
+ /**
+ * A human-readable description of the error
+ */
+ message: string;
+ /**
+ * Field-level validation errors
+ */
+ details?: Array<{ field?: string; issue?: string }>;
+};
+"
+`;
+
+exports[`end-to-end test generates the same code for the example that it did on the last test run 33`] = `
+"types/#/components/responses/BadRequest.ts:import type { Error } from "../../../components/schemas/Error.js";
+
+export type BadRequest = {
+ headers: never;
+ requiredHeaders: never;
+ content: {
+ "application/json": {
+ schema: Error;
+ };
+ };
+ examples: {};
+};
+"
+`;
+
+exports[`end-to-end test generates the same code for the example that it did on the last test run 34`] = `
+"types/components/schemas/Recursive.ts:import type { Recursive } from "./Recursive.js";
+
+export type Recursive = Recursive;
+"
+`;
+
+exports[`end-to-end test generates the same code for the example that it did on the last test run 35`] = `
+"types/components/schemas/UserList.ts:import type { User } from "./User.js";
+
+export type UserList = {
+ items: Array;
+ total: number;
+ page: number;
+ pageSize: number;
+};
+"
+`;
+
+exports[`end-to-end test generates the same code for the example that it did on the last test run 36`] = `
+"types/#/components/responses/Unauthorized.ts:import type { Error } from "../../../components/schemas/Error.js";
+
+export type Unauthorized = {
+ headers: never;
+ requiredHeaders: never;
+ content: {
+ "application/json": {
+ schema: Error;
+ };
+ };
+ examples: {};
+};
+"
+`;
+
+exports[`end-to-end test generates the same code for the example that it did on the last test run 37`] = `
+"types/components/schemas/CreateUserRequest.ts:export type CreateUserRequest = {
+ name: string;
+ /**
+ * @format email
+ */
+ email: string;
+ /**
+ * @default "viewer"
+ */
+ role?: "admin" | "editor" | "viewer";
+ metadata?: { [key: string]: string };
+};
+"
+`;
+
+exports[`end-to-end test generates the same code for the example that it did on the last test run 38`] = `
+"types/components/schemas/User.ts:export type User = {
+ /**
+ * Opaque unique identifier
+ */
+ id: string;
+ name: string;
+ /**
+ * @format email
+ */
+ email: string;
+ /**
+ * @default "viewer"
+ */
+ role: "admin" | "editor" | "viewer";
+ active: boolean;
+ /**
+ * @format date-time
+ */
+ createdAt: string;
+ /**
+ * @format date-time
+ */
+ updatedAt?: string;
+ /**
+ * Arbitrary key/value pairs attached to the user
+ */
+ metadata?: { [key: string]: string };
+};
+"
+`;
+
+exports[`end-to-end test generates the same code for the example that it did on the last test run 39`] = `
+"types/#/components/responses/NotFound.ts:import type { Error } from "../../../components/schemas/Error.js";
+
+export type NotFound = {
+ headers: never;
+ requiredHeaders: never;
+ content: {
+ "application/json": {
+ schema: Error;
+ };
+ };
+ examples: {};
+};
+"
+`;
+
+exports[`end-to-end test generates the same code for the example that it did on the last test run 40`] = `
+"types/components/schemas/PatchUserRequest.ts:/**
+ * All fields are optional; only provided fields are updated (JSON Merge Patch semantics). Setting a nullable field to null clears it.
+ *
+ */
+export type PatchUserRequest = {
+ name?: string;
+ /**
+ * @format email
+ */
+ email?: string;
+ role?: "admin" | "editor" | "viewer";
+ active?: boolean;
+ metadata?: { [key: string]: string };
+};
+"
+`;
+
+exports[`end-to-end test generates the same code for the example that it did on the last test run 41`] = `
+"types/components/schemas/ClickEvent.ts:import type { BaseEvent } from "./BaseEvent.js";
+
+export type ClickEvent = BaseEvent & {
+ /**
+ * @format uri
+ */
+ targetUrl: string;
+ elementId: string;
+};
+"
+`;
+
+exports[`end-to-end test generates the same code for the example that it did on the last test run 42`] = `
+"types/components/schemas/PurchaseEvent.ts:import type { BaseEvent } from "./BaseEvent.js";
+
+export type PurchaseEvent = BaseEvent & {
+ orderId: string;
+ /**
+ * @format double
+ */
+ totalAmount: number;
+ /**
+ * ISO 4217 currency code
+ */
+ currency: string;
+ items?: Array<{
+ /**
+ * @format int64
+ */
+ productId: number;
+ quantity: number;
+ }>;
+};
+"
+`;
+
+exports[`end-to-end test generates the same code for the example that it did on the last test run 43`] = `
+"types/components/schemas/SignUpEvent.ts:import type { BaseEvent } from "./BaseEvent.js";
+
+export type SignUpEvent = BaseEvent & {
+ userId: string;
+ /**
+ * @format email
+ */
+ email: string;
+ referralCode?: string;
+};
+"
+`;
+
+exports[`end-to-end test generates the same code for the example that it did on the last test run 44`] = `
+"types/components/schemas/Product.ts:export type Product = {
+ /**
+ * @format int64
+ */
+ id: number;
+ name: string;
+ description?: string;
+ status: "available" | "out_of_stock" | "discontinued";
+ /**
+ * @format double
+ */
+ price: number;
+ /**
+ * @format double
+ */
+ discountedPrice?: number;
+ /**
+ * @default []
+ */
+ tags?: Array;
+ /**
+ * IDs of related products
+ */
+ relatedIds?: Array;
+ /**
+ * Demonstrates anyOf: each attribute value is either a string, a number, or a boolean.
+ *
+ */
+ attributes?: { [key: string]: unknown };
+};
+"
+`;
+
+exports[`end-to-end test generates the same code for the example that it did on the last test run 45`] = `
+"types/components/schemas/BaseEvent.ts:export type BaseEvent = {
+ /**
+ * Discriminator field
+ */
+ type: string;
+ /**
+ * @format date-time
+ */
+ occurredAt: string;
+ sessionId?: string;
+};
+"
+`;
+
+exports[`end-to-end test generates the same code for the example that it did on the last test run 46`] = `
+".cache
+"
+`;
+
+exports[`end-to-end test generates the same code for the example that it did on the last test run 47`] = `
"This directory contains compiled JS files from the paths directory. Do not edit these files directly.
"
`;
diff --git a/test/typescript-generator/generate.test.ts b/test/typescript-generator/generate.test.ts
index 2048cf8c2..a33699224 100644
--- a/test/typescript-generator/generate.test.ts
+++ b/test/typescript-generator/generate.test.ts
@@ -41,7 +41,7 @@ describe("end-to-end test", () => {
};
await generate(
- "./openapi-example.yaml",
+ "./test/fixtures/openapi-example.yaml",
basePath,
{ routes: true, types: true },
repository,