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,