From 47f967fa543587d987a2d9df7ed914b50825d7ce Mon Sep 17 00:00:00 2001 From: Michael Nahkies Date: Sat, 20 Apr 2024 12:11:57 +0100 Subject: [PATCH 1/2] feat: add train-travel integration test --- .../docs/getting-started.md | 1 + .../train-travel.yaml | 1218 +++++++++++++++++ .../src/core/loaders/generic.loader.ts | 11 +- .../src/core/loaders/utils.ts | 8 + .../src/core/openapi-loader.ts | 10 +- scripts/generate.mjs | 4 +- scripts/refresh-data.sh | 3 + 7 files changed, 1248 insertions(+), 7 deletions(-) create mode 100644 integration-tests-definitions/docs/getting-started.md create mode 100644 integration-tests-definitions/train-travel.yaml diff --git a/integration-tests-definitions/docs/getting-started.md b/integration-tests-definitions/docs/getting-started.md new file mode 100644 index 000000000..a1ba4abfa --- /dev/null +++ b/integration-tests-definitions/docs/getting-started.md @@ -0,0 +1 @@ +(stub file to keep train-travel.yaml happy) diff --git a/integration-tests-definitions/train-travel.yaml b/integration-tests-definitions/train-travel.yaml new file mode 100644 index 000000000..c2d478ee5 --- /dev/null +++ b/integration-tests-definitions/train-travel.yaml @@ -0,0 +1,1218 @@ +openapi: 3.1.0 +info: + title: Train Travel API + description: | + API for finding and booking train trips across Europe. + + ## Run in Postman + + Experiment with this API in Postman, using our Postman Collection. + + [![Run In Postman](https://run.pstmn.io/button.svg =128pxx32px)](https://app.getpostman.com/run-collection/9265903-7a75a0d0-b108-4436-ba54-c6139698dc08?action=collection%2Ffork&source=rip_markdown&collection-url=entityId%3D9265903-7a75a0d0-b108-4436-ba54-c6139698dc08%26entityType%3Dcollection%26workspaceId%3Df507f69d-9564-419c-89a2-cb8e4c8c7b8f) + version: 1.0.0 + contact: + name: Train Support + url: https://example.com/support + email: support@example.com + license: + name: Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International + identifier: CC-BY-NC-SA-4.0 + x-feedbackLink: + label: Submit Feedback + url: https://github.com/bump-sh-examples/train-travel-api/issues/new + +servers: + - url: https://try.microcks.io/rest/Train+Travel+API/1.0.0 + description: Mock Server + x-internal: false + + - url: https://api.example.com + description: Production + x-internal: false + +security: + - OAuth2: + - read + +x-topics: + - title: Getting started + content: + $ref: ./docs/getting-started.md + +tags: + - name: Stations + description: | + Find and filter train stations across Europe, including their location + and local timezone. + - name: Trips + description: | + Timetables and routes for train trips between stations, including pricing + and availability. + - name: Bookings + description: | + Create and manage bookings for train trips, including passenger details + and optional extras. + - name: Payments + description: | + Pay for bookings using a card or bank account, and view payment + status and history. + + > warn + > Bookings usually expire within 1 hour so you'll need to make your payment + > before the expiry date + +paths: + /stations: + get: + summary: Get a list of train stations + description: Returns a paginated and searchable list of all train stations. + operationId: get-stations + tags: + - Stations + parameters: + - $ref: '#/components/parameters/page' + - $ref: '#/components/parameters/limit' + - name: coordinates + in: query + description: > + The latitude and longitude of the user's location, to narrow down + the search results to sites within a proximity of this location. + required: false + schema: + type: string + example: 52.5200,13.4050 + - name: search + in: query + description: > + A search term to filter the list of stations by name or address. + required: false + schema: + type: string + examples: + - Milano Centrale + - Paris + - name: country + in: query + description: Filter stations by country code + required: false + schema: + type: string + format: iso-country-code + example: DE + responses: + '200': + description: OK + headers: + Cache-Control: + $ref: '#/components/headers/Cache-Control' + RateLimit: + $ref: '#/components/headers/RateLimit' + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/Wrapper-Collection' + - properties: + data: + type: array + items: + $ref: '#/components/schemas/Station' + - properties: + links: + allOf: + - $ref: '#/components/schemas/Links-Self' + - $ref: '#/components/schemas/Links-Pagination' + example: + data: + - id: efdbb9d1-02c2-4bc3-afb7-6788d8782b1e + name: Berlin Hauptbahnhof + address: Invalidenstraße 10557 Berlin, Germany + country_code: DE + timezone: Europe/Berlin + - id: b2e783e1-c824-4d63-b37a-d8d698862f1d + name: Paris Gare du Nord + address: 18 Rue de Dunkerque 75010 Paris, France + country_code: FR + timezone: Europe/Paris + links: + self: https://api.example.com/stations&page=2 + next: https://api.example.com/stations?page=3 + prev: https://api.example.com/stations?page=1 + application/xml: + schema: + allOf: + - $ref: '#/components/schemas/Wrapper-Collection' + - properties: + data: + type: array + xml: + name: stations + wrapped: true + items: + $ref: '#/components/schemas/Station' + - properties: + links: + allOf: + - $ref: '#/components/schemas/Links-Self' + - $ref: '#/components/schemas/Links-Pagination' + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '429': + $ref: '#/components/responses/TooManyRequests' + '500': + $ref: '#/components/responses/InternalServerError' + /trips: + get: + summary: Get available train trips + description: > + Returns a list of available train trips between the specified origin and + destination stations on the given date, and allows for filtering by + bicycle and dog allowances. + operationId: get-trips + tags: + - Trips + parameters: + - $ref: '#/components/parameters/page' + - $ref: '#/components/parameters/limit' + - name: origin + in: query + description: The ID of the origin station + required: true + schema: + type: string + format: uuid + example: efdbb9d1-02c2-4bc3-afb7-6788d8782b1e + - name: destination + in: query + description: The ID of the destination station + required: true + schema: + type: string + format: uuid + example: b2e783e1-c824-4d63-b37a-d8d698862f1d + - name: date + in: query + description: The date and time of the trip in ISO 8601 format in origin station's timezone. + required: true + schema: + type: string + format: date-time + example: '2024-02-01T09:00:00Z' + - name: bicycles + in: query + description: Only return trips where bicycles are known to be allowed + required: false + schema: + type: boolean + default: false + - name: dogs + in: query + description: Only return trips where dogs are known to be allowed + required: false + schema: + type: boolean + default: false + responses: + '200': + description: A list of available train trips + headers: + Cache-Control: + $ref: '#/components/headers/Cache-Control' + RateLimit: + $ref: '#/components/headers/RateLimit' + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/Wrapper-Collection' + - properties: + data: + type: array + items: + $ref: '#/components/schemas/Trip' + - properties: + links: + allOf: + - $ref: '#/components/schemas/Links-Self' + - $ref: '#/components/schemas/Links-Pagination' + example: + data: + - id: ea399ba1-6d95-433f-92d1-83f67b775594 + origin: efdbb9d1-02c2-4bc3-afb7-6788d8782b1e + destination: b2e783e1-c824-4d63-b37a-d8d698862f1d + departure_time: '2024-02-01T10:00:00Z' + arrival_time: '2024-02-01T16:00:00Z' + price: 50 + operator: Deutsche Bahn + bicycles_allowed: true + dogs_allowed: true + - id: 4d67459c-af07-40bb-bb12-178dbb88e09f + origin: b2e783e1-c824-4d63-b37a-d8d698862f1d + destination: efdbb9d1-02c2-4bc3-afb7-6788d8782b1e + departure_time: '2024-02-01T12:00:00Z' + arrival_time: '2024-02-01T18:00:00Z' + price: 50 + operator: SNCF + bicycles_allowed: true + dogs_allowed: true + links: + self: https://api.example.com/trips?origin=efdbb9d1-02c2-4bc3-afb7-6788d8782b1e&destination=b2e783e1-c824-4d63-b37a-d8d698862f1d&date=2024-02-01 + next: https://api.example.com/trips?origin=efdbb9d1-02c2-4bc3-afb7-6788d8782b1e&destination=b2e783e1-c824-4d63-b37a-d8d698862f1d&date=2024-02-01&page=2 + application/xml: + schema: + allOf: + - $ref: '#/components/schemas/Wrapper-Collection' + - properties: + data: + type: array + xml: + name: trips + wrapped: true + items: + $ref: '#/components/schemas/Trip' + - properties: + links: + allOf: + - $ref: '#/components/schemas/Links-Self' + - $ref: '#/components/schemas/Links-Pagination' + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '429': + $ref: '#/components/responses/TooManyRequests' + '500': + $ref: '#/components/responses/InternalServerError' + /bookings: + get: + operationId: get-bookings + summary: List existing bookings + description: Returns a list of all trip bookings by the authenticated user. + tags: + - Bookings + parameters: + - $ref: '#/components/parameters/page' + - $ref: '#/components/parameters/limit' + responses: + '200': + description: A list of bookings + headers: + Cache-Control: + $ref: '#/components/headers/Cache-Control' + RateLimit: + $ref: '#/components/headers/RateLimit' + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/Wrapper-Collection' + - properties: + data: + type: array + items: + $ref: '#/components/schemas/Booking' + - properties: + links: + allOf: + - $ref: '#/components/schemas/Links-Self' + - $ref: '#/components/schemas/Links-Pagination' + example: + data: + - id: efdbb9d1-02c2-4bc3-afb7-6788d8782b1e + trip_id: efdbb9d1-02c2-4bc3-afb7-6788d8782b1e + passenger_name: John Doe + has_bicycle: true + has_dog: true + - id: b2e783e1-c824-4d63-b37a-d8d698862f1d + trip_id: b2e783e1-c824-4d63-b37a-d8d698862f1d + passenger_name: Jane Smith + has_bicycle: false + has_dog: false + links: + self: https://api.example.com/bookings + next: https://api.example.com/bookings?page=2 + application/xml: + schema: + allOf: + - $ref: '#/components/schemas/Wrapper-Collection' + - properties: + data: + type: array + xml: + name: bookings + wrapped: true + items: + $ref: '#/components/schemas/Booking' + - properties: + links: + allOf: + - $ref: '#/components/schemas/Links-Self' + - $ref: '#/components/schemas/Links-Pagination' + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '429': + $ref: '#/components/responses/TooManyRequests' + '500': + $ref: '#/components/responses/InternalServerError' + + post: + operationId: create-booking + summary: Create a booking + description: A booking is a temporary hold on a trip. It is not confirmed until the payment is processed. + tags: + - Bookings + security: + - OAuth2: + - write + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/Booking' + application/xml: + schema: + $ref: '#/components/schemas/Booking' + responses: + '201': + description: Booking successful + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/Booking' + - properties: + links: + $ref: '#/components/schemas/Links-Self' + + example: + id: efdbb9d1-02c2-4bc3-afb7-6788d8782b1e + trip_id: efdbb9d1-02c2-4bc3-afb7-6788d8782b1e + passenger_name: John Doe + has_bicycle: true + has_dog: true + links: + self: https://api.example.com/bookings/efdbb9d1-02c2-4bc3-afb7-6788d8782b1e + application/xml: + schema: + allOf: + - $ref: '#/components/schemas/Booking' + - properties: + links: + $ref: '#/components/schemas/Links-Self' + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '404': + $ref: '#/components/responses/NotFound' + '409': + $ref: '#/components/responses/Conflict' + '429': + $ref: '#/components/responses/TooManyRequests' + '500': + $ref: '#/components/responses/InternalServerError' + + /bookings/{bookingId}: + parameters: + - name: bookingId + in: path + required: true + description: The ID of the booking to retrieve. + schema: + type: string + format: uuid + example: 1725ff48-ab45-4bb5-9d02-88745177dedb + get: + summary: Get a booking + description: Returns the details of a specific booking. + operationId: get-booking + tags: + - Bookings + responses: + '200': + description: The booking details + headers: + Cache-Control: + $ref: '#/components/headers/Cache-Control' + RateLimit: + $ref: '#/components/headers/RateLimit' + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/Booking' + - properties: + links: + $ref: '#/components/schemas/Links-Self' + example: + id: efdbb9d1-02c2-4bc3-afb7-6788d8782b1e + trip_id: efdbb9d1-02c2-4bc3-afb7-6788d8782b1e + passenger_name: John Doe + has_bicycle: true + has_dog: true + links: + self: https://api.example.com/bookings/1725ff48-ab45-4bb5-9d02-88745177dedb + application/xml: + schema: + allOf: + - $ref: '#/components/schemas/Booking' + - properties: + links: + $ref: '#/components/schemas/Links-Self' + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '404': + $ref: '#/components/responses/NotFound' + '429': + $ref: '#/components/responses/TooManyRequests' + '500': + $ref: '#/components/responses/InternalServerError' + delete: + summary: Delete a booking + description: Deletes a booking, cancelling the hold on the trip. + operationId: delete-booking + security: + - OAuth2: + - write + tags: + - Bookings + responses: + '204': + description: Booking deleted + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '404': + $ref: '#/components/responses/NotFound' + '429': + $ref: '#/components/responses/TooManyRequests' + '500': + $ref: '#/components/responses/InternalServerError' + + /bookings/{bookingId}/payment: + parameters: + - name: bookingId + in: path + required: true + description: The ID of the booking to pay for. + schema: + type: string + format: uuid + example: 1725ff48-ab45-4bb5-9d02-88745177dedb + post: + summary: Pay for a Booking + description: A payment is an attempt to pay for the booking, which will confirm the booking for the user and enable them to get their tickets. + operationId: create-booking-payment + tags: + - Payments + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/BookingPayment' + examples: + Card: + summary: Card Payment + value: + amount: 49.99 + currency: gbp + source: + object: card + name: J. Doe + number: '4242424242424242' + cvc: 123 + exp_month: 12 + exp_year: 2025 + address_line1: 123 Fake Street + address_line2: 4th Floor + address_city: London + address_country: gb + address_post_code: N12 9XX + Bank: + summary: Bank Account Payment + value: + amount: 100.5 + currency: gbp + source: + object: bank_account + name: J. Doe + number: '00012345' + sort_code: '000123' + account_type: individual + bank_name: Starling Bank + country: gb + responses: + '200': + description: Payment successful + headers: + Cache-Control: + $ref: '#/components/headers/Cache-Control' + RateLimit: + $ref: '#/components/headers/RateLimit' + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/BookingPayment' + - properties: + links: + $ref: '#/components/schemas/Links-Booking' + examples: + Card: + summary: Card Payment + value: + id: 2e3b4f5a-6b7c-8d9e-0f1a-2b3c4d5e6f7a + amount: 49.99 + currency: gbp + source: + object: card + name: J. Doe + number: '************4242' + cvc: 123 + exp_month: 12 + exp_year: 2025 + address_country: gb + address_post_code: N12 9XX + status: succeeded + links: + booking: https://api.example.com/bookings/1725ff48-ab45-4bb5-9d02-88745177dedb/payment + Bank: + summary: Bank Account Payment + value: + id: 2e3b4f5a-6b7c-8d9e-0f1a-2b3c4d5e6f7a + amount: 100.5 + currency: gbp + source: + object: bank_account + name: J. Doe + account_type: individual + number: '*********2345' + sort_code: '000123' + bank_name: Starling Bank + country: gb + status: succeeded + links: + booking: https://api.example.com/bookings/1725ff48-ab45-4bb5-9d02-88745177dedb + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '429': + $ref: '#/components/responses/TooManyRequests' + '500': + $ref: '#/components/responses/InternalServerError' +webhooks: + newBooking: + post: + operationId: new-booking + summary: New Booking + description: | + Subscribe to new bookings being created, to update integrations for your users. Related data is available via the links provided in the request. + tags: + - Bookings + requestBody: + required: true + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/Booking' + - properties: + links: + allOf: + - $ref: '#/components/schemas/Links-Self' + - $ref: '#/components/schemas/Links-Pagination' + example: + id: efdbb9d1-02c2-4bc3-afb7-6788d8782b1e + trip_id: efdbb9d1-02c2-4bc3-afb7-6788d8782b1e + passenger_name: John Doe + has_bicycle: true + has_dog: true + links: + self: https://api.example.com/bookings/1725ff48-ab45-4bb5-9d02-88745177dedb + responses: + '200': + description: Return a 200 status to indicate that the data was received successfully. + +components: + parameters: + page: + name: page + in: query + description: The page number to return + required: false + schema: + type: integer + minimum: 1 + default: 1 + example: 1 + + limit: + name: limit + in: query + description: The number of items to return per page + required: false + schema: + type: integer + minimum: 1 + maximum: 100 + default: 10 + example: 10 + + securitySchemes: + OAuth2: + type: oauth2 + description: OAuth 2.0 authorization code following RFC8725 best practices. + flows: + authorizationCode: + authorizationUrl: https://example.com/oauth/authorize + tokenUrl: https://example.com/oauth/token + scopes: + read: Read access + write: Write access + schemas: + Station: + type: object + xml: + name: station + required: + - id + - name + - address + - country_code + properties: + id: + type: string + format: uuid + description: Unique identifier for the station. + examples: + - efdbb9d1-02c2-4bc3-afb7-6788d8782b1e + - b2e783e1-c824-4d63-b37a-d8d698862f1d + name: + type: string + description: The name of the station + examples: + - Berlin Hauptbahnhof + - Paris Gare du Nord + address: + type: string + description: The address of the station. + examples: + - Invalidenstraße 10557 Berlin, Germany + - 18 Rue de Dunkerque 75010 Paris, France + country_code: + type: string + description: The country code of the station. + format: iso-country-code + examples: + - DE + - FR + timezone: + type: string + description: The timezone of the station in the [IANA Time Zone Database format](https://www.iana.org/time-zones). + examples: + - Europe/Berlin + - Europe/Paris + Links-Self: + type: object + properties: + self: + type: string + format: uri + Links-Pagination: + type: object + properties: + next: + type: string + format: uri + prev: + type: string + format: uri + Problem: + type: object + xml: + name: problem + namespace: urn:ietf:rfc:7807 + properties: + type: + type: string + description: A URI reference that identifies the problem type + examples: + - https://example.com/probs/out-of-credit + title: + type: string + description: A short, human-readable summary of the problem type + examples: + - You do not have enough credit. + detail: + type: string + description: A human-readable explanation specific to this occurrence of the problem + examples: + - Your current balance is 30, but that costs 50. + instance: + type: string + description: A URI reference that identifies the specific occurrence of the problem + examples: + - /account/12345/msgs/abc + status: + type: integer + description: The HTTP status code + examples: + - 400 + Trip: + type: object + xml: + name: trip + properties: + id: + type: string + format: uuid + description: Unique identifier for the trip + examples: + - 4f4e4e1-c824-4d63-b37a-d8d698862f1d + origin: + type: string + description: The starting station of the trip + examples: + - efdbb9d1-02c2-4bc3-afb7-6788d8782b1e + - b2e783e1-c824-4d63-b37a-d8d698862f1d + destination: + type: string + description: The destination station of the trip + examples: + - b2e783e1-c824-4d63-b37a-d8d698862f1d + - efdbb9d1-02c2-4bc3-afb7-6788d8782b1e + departure_time: + type: string + format: date-time + description: The date and time when the trip departs + examples: + - '2024-02-01T10:00:00Z' + arrival_time: + type: string + format: date-time + description: The date and time when the trip arrives + examples: + - '2024-02-01T16:00:00Z' + operator: + type: string + description: The name of the operator of the trip + examples: + - Deutsche Bahn + - SNCF + price: + type: number + description: The cost of the trip + examples: + - 50 + bicycles_allowed: + type: boolean + description: Indicates whether bicycles are allowed on the trip + dogs_allowed: + type: boolean + description: Indicates whether dogs are allowed on the trip + Booking: + type: object + xml: + name: booking + properties: + id: + type: string + format: uuid + description: Unique identifier for the booking + readOnly: true + examples: + - 3f3e3e1-c824-4d63-b37a-d8d698862f1d + trip_id: + type: string + format: uuid + description: Identifier of the booked trip + examples: + - 4f4e4e1-c824-4d63-b37a-d8d698862f1d + passenger_name: + type: string + description: Name of the passenger + examples: + - John Doe + has_bicycle: + type: boolean + description: Indicates whether the passenger has a bicycle. + has_dog: + type: boolean + description: Indicates whether the passenger has a dog. + Wrapper-Collection: + description: This is a generic request/response wrapper which contains both data and links which serve as hypermedia controls (HATEOAS). + type: object + properties: + data: + description: The wrapper for a collection is an array of objects. + type: array + items: + type: object + links: + description: A set of hypermedia links which serve as controls for the client. + type: object + readOnly: true + xml: + name: data + BookingPayment: + type: object + properties: + id: + description: Unique identifier for the payment. This will be a unique identifier for the payment, and is used to reference the payment in other objects. + type: string + format: uuid + readOnly: true + amount: + description: Amount intended to be collected by this payment. A positive decimal figure describing the amount to be collected. + type: number + exclusiveMinimum: 0 + examples: + - 49.99 + currency: + description: Three-letter [ISO currency code](https://www.iso.org/iso-4217-currency-codes.html), in lowercase. + type: string + enum: + - bam + - bgn + - chf + - eur + - gbp + - nok + - sek + - try + source: + unevaluatedProperties: false + description: The payment source to take the payment from. This can be a card or a bank account. Some of these properties will be hidden on read to protect PII leaking. + oneOf: + - title: Card + description: A card (debit or credit) to take payment from. + type: object + properties: + object: + type: string + const: card + name: + type: string + description: Cardholder's full name as it appears on the card. + examples: + - Francis Bourgeois + number: + type: string + description: The card number, as a string without any separators. On read all but the last four digits will be masked for security. + examples: + - '4242424242424242' + cvc: + type: integer + description: Card security code, 3 or 4 digits usually found on the back of the card. + minLength: 3 + maxLength: 4 + writeOnly: true + + example: 123 + exp_month: + type: integer + format: int64 + description: Two-digit number representing the card's expiration month. + examples: + - 12 + exp_year: + type: integer + format: int64 + description: Four-digit number representing the card's expiration year. + examples: + - 2025 + address_line1: + type: string + writeOnly: true + address_line2: + type: string + writeOnly: true + address_city: + type: string + address_country: + type: string + address_post_code: + type: string + required: + - name + - number + - cvc + - exp_month + - exp_year + - address_country + - title: Bank Account + description: A bank account to take payment from. Must be able to make payments in the currency specified in the payment. + type: object + properties: + object: + const: bank_account + type: string + name: + type: string + number: + type: string + description: The account number for the bank account, in string form. Must be a current account. + sort_code: + type: string + description: The sort code for the bank account, in string form. Must be a six-digit number. + account_type: + enum: + - individual + - company + type: string + description: The type of entity that holds the account. This can be either `individual` or `company`. + bank_name: + type: string + description: The name of the bank associated with the routing number. + examples: + - Starling Bank + country: + type: string + description: Two-letter country code (ISO 3166-1 alpha-2). + required: + - name + - number + - account_type + - bank_name + - country + status: + description: The status of the payment, one of `pending`, `succeeded`, or `failed`. + type: string + enum: + - pending + - succeeded + - failed + readOnly: true + Links-Booking: + type: object + properties: + booking: + type: string + format: uri + examples: + - https://api.example.com/bookings/1725ff48-ab45-4bb5-9d02-88745177dedb + headers: + Cache-Control: + description: | + The Cache-Control header communicates directives for caching mechanisms in both requests and responses. + It is used to specify the caching directives in responses to prevent caches from storing sensitive information. + schema: + type: string + description: A comma-separated list of directives as defined in [RFC 9111](https://www.rfc-editor.org/rfc/rfc9111.html). + examples: + - max-age=3600 + - max-age=604800, public + - no-store + - no-cache + - private + + RateLimit: + description: | + The RateLimit header communicates quota policies. It contains a `limit` to + convey the expiring limit, `remaining` to convey the remaining quota units, + and `reset` to convey the time window reset time. + schema: + type: string + examples: + - limit=10, remaining=0, reset=10 + + Retry-After: + description: | + The Retry-After header indicates how long the user agent should wait before making a follow-up request. + The value is in seconds and can be an integer or a date in the future. + If the value is an integer, it indicates the number of seconds to wait. + If the value is a date, it indicates the time at which the user agent should make a follow-up request. + schema: + type: string + examples: + integer: + value: '120' + summary: Retry after 120 seconds + date: + value: 'Fri, 31 Dec 2021 23:59:59 GMT' + summary: Retry after the specified date + responses: + BadRequest: + description: Bad Request + headers: + RateLimit: + $ref: '#/components/headers/RateLimit' + content: + application/problem+json: + schema: + $ref: '#/components/schemas/Problem' + example: + type: https://example.com/errors/bad-request + title: Bad Request + status: 400 + detail: The request is invalid or missing required parameters. + application/problem+xml: + schema: + $ref: '#/components/schemas/Problem' + example: + type: https://example.com/errors/bad-request + title: Bad Request + status: 400 + detail: The request is invalid or missing required parameters. + + Conflict: + description: Conflict + headers: + RateLimit: + $ref: '#/components/headers/RateLimit' + content: + application/problem+json: + schema: + $ref: '#/components/schemas/Problem' + example: + type: https://example.com/errors/conflict + title: Conflict + status: 409 + detail: There is a conflict with an existing resource. + application/problem+xml: + schema: + $ref: '#/components/schemas/Problem' + example: + type: https://example.com/errors/conflict + title: Conflict + status: 409 + detail: There is a conflict with an existing resource. + + Forbidden: + description: Forbidden + headers: + RateLimit: + $ref: '#/components/headers/RateLimit' + content: + application/problem+json: + schema: + $ref: '#/components/schemas/Problem' + example: + type: https://example.com/errors/forbidden + title: Forbidden + status: 403 + detail: Access is forbidden with the provided credentials. + application/problem+xml: + schema: + $ref: '#/components/schemas/Problem' + example: + type: https://example.com/errors/forbidden + title: Forbidden + status: 403 + detail: Access is forbidden with the provided credentials. + + InternalServerError: + description: Internal Server Error + headers: + RateLimit: + $ref: '#/components/headers/RateLimit' + content: + application/problem+json: + schema: + $ref: '#/components/schemas/Problem' + example: + type: https://example.com/errors/internal-server-error + title: Internal Server Error + status: 500 + detail: An unexpected error occurred. + application/problem+xml: + schema: + $ref: '#/components/schemas/Problem' + example: + type: https://example.com/errors/internal-server-error + title: Internal Server Error + status: 500 + detail: An unexpected error occurred. + + NotFound: + description: Not Found + headers: + RateLimit: + $ref: '#/components/headers/RateLimit' + content: + application/problem+json: + schema: + $ref: '#/components/schemas/Problem' + example: + type: https://example.com/errors/not-found + title: Not Found + status: 404 + detail: The requested resource was not found. + application/problem+xml: + schema: + $ref: '#/components/schemas/Problem' + example: + type: https://example.com/errors/not-found + title: Not Found + status: 404 + detail: The requested resource was not found. + + TooManyRequests: + description: Too Many Requests + headers: + RateLimit: + $ref: '#/components/headers/RateLimit' + Retry-After: + $ref: '#/components/headers/Retry-After' + content: + application/problem+json: + schema: + $ref: '#/components/schemas/Problem' + example: + type: https://example.com/errors/too-many-requests + title: Too Many Requests + status: 429 + detail: You have exceeded the rate limit. + application/problem+xml: + schema: + $ref: '#/components/schemas/Problem' + example: + type: https://example.com/errors/too-many-requests + title: Too Many Requests + status: 429 + detail: You have exceeded the rate limit. + + Unauthorized: + description: Unauthorized + headers: + RateLimit: + $ref: '#/components/headers/RateLimit' + content: + application/problem+json: + schema: + $ref: '#/components/schemas/Problem' + example: + type: https://example.com/errors/unauthorized + title: Unauthorized + status: 401 + detail: You do not have the necessary permissions. + application/problem+xml: + schema: + $ref: '#/components/schemas/Problem' + example: + type: https://example.com/errors/unauthorized + title: Unauthorized + status: 401 + detail: You do not have the necessary permissions. diff --git a/packages/openapi-code-generator/src/core/loaders/generic.loader.ts b/packages/openapi-code-generator/src/core/loaders/generic.loader.ts index 9e4aa0947..56ed21a56 100644 --- a/packages/openapi-code-generator/src/core/loaders/generic.loader.ts +++ b/packages/openapi-code-generator/src/core/loaders/generic.loader.ts @@ -4,7 +4,7 @@ import yaml from "js-yaml" import json5 from "json5" import type {IFsAdaptor} from "../file-system/fs-adaptor" import {logger} from "../logger" -import {isRemote} from "./utils" +import {isJsonFile, isRemote, isTextFile, isYamlFile} from "./utils" export type GenericLoaderRequestHeaders = { [uri: string]: {name: string; value: string}[] @@ -64,8 +64,7 @@ export class GenericLoader { private async parseFile(raw: string, filepath: string): Promise { let result: unknown | undefined - // TODO: sniff format from raw text - if (filepath.endsWith(".json")) { + if (isJsonFile(filepath)) { try { result = json5.parse(raw) } catch (err: unknown) { @@ -74,7 +73,7 @@ export class GenericLoader { } } - if (filepath.endsWith(".yaml") || filepath.endsWith(".yml")) { + if (isYamlFile(filepath)) { try { result = yaml.load(raw) } catch (err: unknown) { @@ -83,6 +82,10 @@ export class GenericLoader { } } + if (isTextFile(filepath)) { + result = raw + } + if (!result) { throw new Error(`failed to parse '${filepath}'`) } diff --git a/packages/openapi-code-generator/src/core/loaders/utils.ts b/packages/openapi-code-generator/src/core/loaders/utils.ts index 93ffb3b61..9785576e5 100644 --- a/packages/openapi-code-generator/src/core/loaders/utils.ts +++ b/packages/openapi-code-generator/src/core/loaders/utils.ts @@ -1,3 +1,11 @@ export function isRemote(location: string): boolean { return location.startsWith("http://") || location.startsWith("https://") } + +export const isJsonFile = (filepath: string) => filepath.endsWith(".json") + +export const isYamlFile = (filepath: string) => + filepath.endsWith(".yaml") || filepath.endsWith(".yml") + +export const isTextFile = (filepath: string) => + filepath.endsWith(".txt") || filepath.endsWith(".md") diff --git a/packages/openapi-code-generator/src/core/openapi-loader.ts b/packages/openapi-code-generator/src/core/openapi-loader.ts index 95a1fc012..294072b90 100644 --- a/packages/openapi-code-generator/src/core/openapi-loader.ts +++ b/packages/openapi-code-generator/src/core/openapi-loader.ts @@ -6,7 +6,7 @@ import {load} from "js-yaml" import {VirtualDefinition, generationLib} from "./generation-lib" import type {GenericLoader} from "./loaders/generic.loader" import type {TypespecLoader} from "./loaders/typespec.loader" -import {isRemote} from "./loaders/utils" +import {isRemote, isTextFile} from "./loaders/utils" import type { OpenapiDocument, Operation, @@ -166,7 +166,13 @@ export class OpenapiLoader { ) { // biome-ignore lint/suspicious/noAssignInExpressions: const $ref = (obj[key] = normalizeRef(obj[key], loadedFrom)) - await this.loadFile(pathFromRef($ref)) + + // In-line plain text files rather than trying to load as OpenAPI documents. + if (isTextFile($ref)) { + obj[key] = this.loadFile($ref) + } else { + await this.loadFile(pathFromRef($ref)) + } } else if (typeof obj[key] === "object" && !!obj[key]) { await this.normalizeRefs(loadedFrom, obj[key]) } diff --git a/scripts/generate.mjs b/scripts/generate.mjs index 475ed4177..950107835 100644 --- a/scripts/generate.mjs +++ b/scripts/generate.mjs @@ -8,7 +8,9 @@ const templates = execSync( .split("\n") .map((it) => it.trim()) .filter(Boolean) -const definitions = execSync("find ./integration-tests-definitions -type f") +const definitions = execSync( + "find ./integration-tests-definitions -type f -name '*.yaml'", +) .toString("utf-8") .split("\n") .map((it) => it.trim()) diff --git a/scripts/refresh-data.sh b/scripts/refresh-data.sh index 9e64a85d7..9aa67d267 100755 --- a/scripts/refresh-data.sh +++ b/scripts/refresh-data.sh @@ -14,6 +14,7 @@ curl -L https://raw.githubusercontent.com/OAI/OpenAPI-Specification/refs/heads/m curl -L https://raw.githubusercontent.com/stripe/openapi/master/openapi/spec3.yaml -o ./integration-tests-definitions/stripe.yaml curl -L https://github.com/okta/okta-management-openapi-spec/raw/master/dist/current/idp-minimal.yaml -o ./integration-tests-definitions/okta.idp.yaml + # spec is currently invalid (https://github.com/okta/okta-management-openapi-spec/issues/180) #curl -L https://github.com/okta/okta-management-openapi-spec/raw/master/dist/current/management-minimal.yaml -o ./integration-tests-definitions/okta.management.yaml curl -L https://github.com/okta/okta-management-openapi-spec/raw/master/dist/current/oauth-minimal.yaml -o ./integration-tests-definitions/okta.oauth.yaml @@ -21,3 +22,5 @@ curl -L https://github.com/okta/okta-management-openapi-spec/raw/master/dist/cur # typespec samples curl -L https://raw.githubusercontent.com/Azure/typespec-azure/main/packages/typespec-azure-playground-website/samples/arm.tsp -o ./integration-tests-definitions/azure-resource-manager.tsp curl -L https://raw.githubusercontent.com/Azure/typespec-azure/main/packages/typespec-azure-playground-website/samples/azure-core.tsp -o ./integration-tests-definitions/azure-core-data-plane-service.tsp + +curl -L https://raw.githubusercontent.com/bump-sh-examples/train-travel-api/main/openapi.yaml -o ./integration-tests-definitions/train-travel.yaml From 1c43a43082cab28b9a0133336fd9c0a4dd38ce2f Mon Sep 17 00:00:00 2001 From: Michael Nahkies Date: Sat, 20 Apr 2024 12:12:16 +0100 Subject: [PATCH 2/2] chore: regenerate --- .../generated/train-travel.yaml/api.module.ts | 16 + .../train-travel.yaml/client.service.ts | 368 ++++++++ .../src/generated/train-travel.yaml/models.ts | 91 ++ .../src/generated/train-travel.yaml/client.ts | 282 ++++++ .../src/generated/train-travel.yaml/models.ts | 98 ++ .../src/generated/train-travel.yaml/client.ts | 297 ++++++ .../src/generated/train-travel.yaml/models.ts | 91 ++ .../generated/train-travel.yaml/generated.ts | 860 ++++++++++++++++++ .../src/generated/train-travel.yaml/models.ts | 164 ++++ .../generated/train-travel.yaml/schemas.ts | 99 ++ 10 files changed, 2366 insertions(+) create mode 100644 integration-tests/typescript-angular/src/generated/train-travel.yaml/api.module.ts create mode 100644 integration-tests/typescript-angular/src/generated/train-travel.yaml/client.service.ts create mode 100644 integration-tests/typescript-angular/src/generated/train-travel.yaml/models.ts create mode 100644 integration-tests/typescript-axios/src/generated/train-travel.yaml/client.ts create mode 100644 integration-tests/typescript-axios/src/generated/train-travel.yaml/models.ts create mode 100644 integration-tests/typescript-fetch/src/generated/train-travel.yaml/client.ts create mode 100644 integration-tests/typescript-fetch/src/generated/train-travel.yaml/models.ts create mode 100644 integration-tests/typescript-koa/src/generated/train-travel.yaml/generated.ts create mode 100644 integration-tests/typescript-koa/src/generated/train-travel.yaml/models.ts create mode 100644 integration-tests/typescript-koa/src/generated/train-travel.yaml/schemas.ts diff --git a/integration-tests/typescript-angular/src/generated/train-travel.yaml/api.module.ts b/integration-tests/typescript-angular/src/generated/train-travel.yaml/api.module.ts new file mode 100644 index 000000000..374d29407 --- /dev/null +++ b/integration-tests/typescript-angular/src/generated/train-travel.yaml/api.module.ts @@ -0,0 +1,16 @@ +/** AUTOGENERATED - DO NOT EDIT **/ +/* tslint:disable */ +/* eslint-disable */ + +import { TrainTravelApiService } from "./client.service" +import { NgModule } from "@angular/core" + +@NgModule({ + imports: [], + declarations: [], + exports: [], + providers: [TrainTravelApiService], +}) +export class TrainTravelApiModule {} + +export { TrainTravelApiModule as ApiModule } diff --git a/integration-tests/typescript-angular/src/generated/train-travel.yaml/client.service.ts b/integration-tests/typescript-angular/src/generated/train-travel.yaml/client.service.ts new file mode 100644 index 000000000..98d63b555 --- /dev/null +++ b/integration-tests/typescript-angular/src/generated/train-travel.yaml/client.service.ts @@ -0,0 +1,368 @@ +/** AUTOGENERATED - DO NOT EDIT **/ +/* tslint:disable */ +/* eslint-disable */ + +import { + t_Booking, + t_BookingPayment, + t_Links_Booking, + t_Links_Pagination, + t_Links_Self, + t_Problem, + t_Station, + t_Trip, + t_Wrapper_Collection, +} from "./models" +import { HttpClient, HttpParams, HttpResponse } from "@angular/common/http" +import { Injectable } from "@angular/core" +import { Observable } from "rxjs" + +export class TrainTravelApiServiceServers { + static default(): Server<"TrainTravelApiService"> { + return TrainTravelApiServiceServers.server().build() + } + + static server(url?: "https://try.microcks.io/rest/Train+Travel+API/1.0.0"): { + build: () => Server<"TrainTravelApiService"> + } + static server(url?: "https://api.example.com"): { + build: () => Server<"TrainTravelApiService"> + } + static server( + url: string = "https://try.microcks.io/rest/Train+Travel+API/1.0.0", + ): unknown { + switch (url) { + case "https://try.microcks.io/rest/Train+Travel+API/1.0.0": + return { + build(): Server<"TrainTravelApiService"> { + return "https://try.microcks.io/rest/Train+Travel+API/1.0.0" as Server<"TrainTravelApiService"> + }, + } + + case "https://api.example.com": + return { + build(): Server<"TrainTravelApiService"> { + return "https://api.example.com" as Server<"TrainTravelApiService"> + }, + } + + default: + throw new Error(`no matching server for url '${url}'`) + } + } +} + +export class TrainTravelApiServiceConfig { + basePath: Server<"TrainTravelApiService"> | string = + TrainTravelApiServiceServers.default() + defaultHeaders: Record = {} +} + +// from https://stackoverflow.com/questions/39494689/is-it-possible-to-restrict-number-to-a-certain-range +type Enumerate< + N extends number, + Acc extends number[] = [], +> = Acc["length"] extends N + ? Acc[number] + : Enumerate + +type IntRange = F extends T + ? F + : Exclude, Enumerate> extends never + ? never + : Exclude, Enumerate> | T + +export type StatusCode1xx = IntRange<100, 199> +export type StatusCode2xx = IntRange<200, 299> +export type StatusCode3xx = IntRange<300, 399> +export type StatusCode4xx = IntRange<400, 499> +export type StatusCode5xx = IntRange<500, 599> +export type StatusCode = + | StatusCode1xx + | StatusCode2xx + | StatusCode3xx + | StatusCode4xx + | StatusCode5xx + +export type QueryParams = { + [name: string]: + | string + | number + | boolean + | string[] + | undefined + | null + | QueryParams + | QueryParams[] +} + +export type Server = string & { __server__: T } + +@Injectable({ + providedIn: "root", +}) +export class TrainTravelApiService { + constructor( + private readonly httpClient: HttpClient, + private readonly config: TrainTravelApiServiceConfig, + ) {} + + private _headers( + headers: Record, + ): Record { + return Object.fromEntries( + Object.entries({ ...this.config.defaultHeaders, ...headers }).filter( + (it): it is [string, string] => it[1] !== undefined, + ), + ) + } + + private _queryParams(queryParams: QueryParams): HttpParams { + return Object.entries(queryParams).reduce((result, [name, value]) => { + if ( + typeof value === "string" || + typeof value === "boolean" || + typeof value === "number" + ) { + return result.set(name, value) + } else if (value === null || value === undefined) { + return result + } + throw new Error( + `query parameter '${name}' with value '${value}' is not yet supported`, + ) + }, new HttpParams()) + } + + getStations( + p: { + page?: number + limit?: number + coordinates?: string + search?: string + country?: string + } = {}, + ): Observable< + | (HttpResponse< + t_Wrapper_Collection & { + data?: t_Station[] + } & { + links?: t_Links_Self & t_Links_Pagination + } + > & { status: 200 }) + | (HttpResponse & { status: 400 }) + | (HttpResponse & { status: 401 }) + | (HttpResponse & { status: 403 }) + | (HttpResponse & { status: 429 }) + | (HttpResponse & { status: 500 }) + | HttpResponse + > { + const params = this._queryParams({ + page: p["page"], + limit: p["limit"], + coordinates: p["coordinates"], + search: p["search"], + country: p["country"], + }) + + return this.httpClient.request( + "GET", + this.config.basePath + `/stations`, + { + params, + observe: "response", + reportProgress: false, + }, + ) + } + + getTrips(p: { + page?: number + limit?: number + origin: string + destination: string + date: string + bicycles?: boolean + dogs?: boolean + }): Observable< + | (HttpResponse< + t_Wrapper_Collection & { + data?: t_Trip[] + } & { + links?: t_Links_Self & t_Links_Pagination + } + > & { status: 200 }) + | (HttpResponse & { status: 400 }) + | (HttpResponse & { status: 401 }) + | (HttpResponse & { status: 403 }) + | (HttpResponse & { status: 429 }) + | (HttpResponse & { status: 500 }) + | HttpResponse + > { + const params = this._queryParams({ + page: p["page"], + limit: p["limit"], + origin: p["origin"], + destination: p["destination"], + date: p["date"], + bicycles: p["bicycles"], + dogs: p["dogs"], + }) + + return this.httpClient.request( + "GET", + this.config.basePath + `/trips`, + { + params, + observe: "response", + reportProgress: false, + }, + ) + } + + getBookings( + p: { + page?: number + limit?: number + } = {}, + ): Observable< + | (HttpResponse< + t_Wrapper_Collection & { + data?: t_Booking[] + } & { + links?: t_Links_Self & t_Links_Pagination + } + > & { status: 200 }) + | (HttpResponse & { status: 400 }) + | (HttpResponse & { status: 401 }) + | (HttpResponse & { status: 403 }) + | (HttpResponse & { status: 429 }) + | (HttpResponse & { status: 500 }) + | HttpResponse + > { + const params = this._queryParams({ page: p["page"], limit: p["limit"] }) + + return this.httpClient.request( + "GET", + this.config.basePath + `/bookings`, + { + params, + observe: "response", + reportProgress: false, + }, + ) + } + + createBooking(p: { + requestBody: t_Booking + }): Observable< + | (HttpResponse< + t_Booking & { + links?: t_Links_Self + } + > & { status: 201 }) + | (HttpResponse & { status: 400 }) + | (HttpResponse & { status: 401 }) + | (HttpResponse & { status: 404 }) + | (HttpResponse & { status: 409 }) + | (HttpResponse & { status: 429 }) + | (HttpResponse & { status: 500 }) + | HttpResponse + > { + const headers = this._headers({ "Content-Type": "application/json" }) + const body = p["requestBody"] + + return this.httpClient.request( + "POST", + this.config.basePath + `/bookings`, + { + headers, + body, + observe: "response", + reportProgress: false, + }, + ) + } + + getBooking(p: { + bookingId: string + }): Observable< + | (HttpResponse< + t_Booking & { + links?: t_Links_Self + } + > & { status: 200 }) + | (HttpResponse & { status: 400 }) + | (HttpResponse & { status: 401 }) + | (HttpResponse & { status: 403 }) + | (HttpResponse & { status: 404 }) + | (HttpResponse & { status: 429 }) + | (HttpResponse & { status: 500 }) + | HttpResponse + > { + return this.httpClient.request( + "GET", + this.config.basePath + `/bookings/${p["bookingId"]}`, + { + observe: "response", + reportProgress: false, + }, + ) + } + + deleteBooking(p: { + bookingId: string + }): Observable< + | (HttpResponse & { status: 204 }) + | (HttpResponse & { status: 400 }) + | (HttpResponse & { status: 401 }) + | (HttpResponse & { status: 403 }) + | (HttpResponse & { status: 404 }) + | (HttpResponse & { status: 429 }) + | (HttpResponse & { status: 500 }) + | HttpResponse + > { + return this.httpClient.request( + "DELETE", + this.config.basePath + `/bookings/${p["bookingId"]}`, + { + observe: "response", + reportProgress: false, + }, + ) + } + + createBookingPayment(p: { + bookingId: string + requestBody: t_BookingPayment + }): Observable< + | (HttpResponse< + t_BookingPayment & { + links?: t_Links_Booking + } + > & { status: 200 }) + | (HttpResponse & { status: 400 }) + | (HttpResponse & { status: 401 }) + | (HttpResponse & { status: 403 }) + | (HttpResponse & { status: 429 }) + | (HttpResponse & { status: 500 }) + | HttpResponse + > { + const headers = this._headers({ "Content-Type": "application/json" }) + const body = p["requestBody"] + + return this.httpClient.request( + "POST", + this.config.basePath + `/bookings/${p["bookingId"]}/payment`, + { + headers, + body, + observe: "response", + reportProgress: false, + }, + ) + } +} + +export { TrainTravelApiService as ApiClient } +export { TrainTravelApiServiceConfig as ApiClientConfig } diff --git a/integration-tests/typescript-angular/src/generated/train-travel.yaml/models.ts b/integration-tests/typescript-angular/src/generated/train-travel.yaml/models.ts new file mode 100644 index 000000000..23f9a3dab --- /dev/null +++ b/integration-tests/typescript-angular/src/generated/train-travel.yaml/models.ts @@ -0,0 +1,91 @@ +/** AUTOGENERATED - DO NOT EDIT **/ +/* tslint:disable */ +/* eslint-disable */ + +export type EmptyObject = { [key: string]: never } + +export type t_Booking = { + has_bicycle?: boolean + has_dog?: boolean + readonly id?: string + passenger_name?: string + trip_id?: string +} + +export type t_BookingPayment = { + amount?: number + currency?: "bam" | "bgn" | "chf" | "eur" | "gbp" | "nok" | "sek" | "try" + readonly id?: string + source?: + | { + address_city?: string + address_country: string + address_line1?: string + address_line2?: string + address_post_code?: string + cvc: number + exp_month: number + exp_year: number + name: string + number: string + object?: string + } + | { + account_type: "individual" | "company" + bank_name: string + country: string + name: string + number: string + object?: string + sort_code?: string + } + readonly status?: "pending" | "succeeded" | "failed" +} + +export type t_Links_Booking = { + booking?: string +} + +export type t_Links_Pagination = { + next?: string + prev?: string +} + +export type t_Links_Self = { + self?: string +} + +export type t_Problem = { + detail?: string + instance?: string + status?: number + title?: string + type?: string +} + +export type t_Station = { + address: string + country_code: string + id: string + name: string + timezone?: string +} + +export type t_Trip = { + arrival_time?: string + bicycles_allowed?: boolean + departure_time?: string + destination?: string + dogs_allowed?: boolean + id?: string + operator?: string + origin?: string + price?: number +} + +export type t_Wrapper_Collection = { + data?: { + [key: string]: unknown | undefined + }[] + readonly links?: EmptyObject +} diff --git a/integration-tests/typescript-axios/src/generated/train-travel.yaml/client.ts b/integration-tests/typescript-axios/src/generated/train-travel.yaml/client.ts new file mode 100644 index 000000000..e4d433285 --- /dev/null +++ b/integration-tests/typescript-axios/src/generated/train-travel.yaml/client.ts @@ -0,0 +1,282 @@ +/** AUTOGENERATED - DO NOT EDIT **/ +/* tslint:disable */ +/* eslint-disable */ + +import { + t_Booking, + t_BookingPayment, + t_Links_Booking, + t_Links_Pagination, + t_Links_Self, + t_Station, + t_Trip, + t_Wrapper_Collection, +} from "./models" +import { + AbstractAxiosClient, + AbstractAxiosConfig, + Server, +} from "@nahkies/typescript-axios-runtime/main" +import { AxiosRequestConfig, AxiosResponse } from "axios" + +export class TrainTravelApiServers { + static default(): Server<"TrainTravelApi"> { + return TrainTravelApiServers.server().build() + } + + static server(url?: "https://try.microcks.io/rest/Train+Travel+API/1.0.0"): { + build: () => Server<"TrainTravelApi"> + } + static server(url?: "https://api.example.com"): { + build: () => Server<"TrainTravelApi"> + } + static server( + url: string = "https://try.microcks.io/rest/Train+Travel+API/1.0.0", + ): unknown { + switch (url) { + case "https://try.microcks.io/rest/Train+Travel+API/1.0.0": + return { + build(): Server<"TrainTravelApi"> { + return "https://try.microcks.io/rest/Train+Travel+API/1.0.0" as Server<"TrainTravelApi"> + }, + } + + case "https://api.example.com": + return { + build(): Server<"TrainTravelApi"> { + return "https://api.example.com" as Server<"TrainTravelApi"> + }, + } + + default: + throw new Error(`no matching server for url '${url}'`) + } + } +} + +export interface TrainTravelApiConfig extends AbstractAxiosConfig { + basePath: Server<"TrainTravelApi"> | string +} + +export class TrainTravelApi extends AbstractAxiosClient { + constructor(config: TrainTravelApiConfig) { + super(config) + } + + async getStations( + p: { + page?: number + limit?: number + coordinates?: string + search?: string + country?: string + } = {}, + timeout?: number, + opts: AxiosRequestConfig = {}, + ): Promise< + AxiosResponse< + t_Wrapper_Collection & { + data?: t_Station[] | undefined + } & { + links?: (t_Links_Self & t_Links_Pagination) | undefined + } + > + > { + const url = `/stations` + const headers = this._headers({}, opts.headers) + const query = this._query({ + page: p["page"], + limit: p["limit"], + coordinates: p["coordinates"], + search: p["search"], + country: p["country"], + }) + + return this._request({ + url: url + query, + method: "GET", + ...(timeout ? { timeout } : {}), + ...opts, + headers, + }) + } + + async getTrips( + p: { + page?: number + limit?: number + origin: string + destination: string + date: string + bicycles?: boolean + dogs?: boolean + }, + timeout?: number, + opts: AxiosRequestConfig = {}, + ): Promise< + AxiosResponse< + t_Wrapper_Collection & { + data?: t_Trip[] | undefined + } & { + links?: (t_Links_Self & t_Links_Pagination) | undefined + } + > + > { + const url = `/trips` + const headers = this._headers({}, opts.headers) + const query = this._query({ + page: p["page"], + limit: p["limit"], + origin: p["origin"], + destination: p["destination"], + date: p["date"], + bicycles: p["bicycles"], + dogs: p["dogs"], + }) + + return this._request({ + url: url + query, + method: "GET", + ...(timeout ? { timeout } : {}), + ...opts, + headers, + }) + } + + async getBookings( + p: { + page?: number + limit?: number + } = {}, + timeout?: number, + opts: AxiosRequestConfig = {}, + ): Promise< + AxiosResponse< + t_Wrapper_Collection & { + data?: t_Booking[] | undefined + } & { + links?: (t_Links_Self & t_Links_Pagination) | undefined + } + > + > { + const url = `/bookings` + const headers = this._headers({}, opts.headers) + const query = this._query({ page: p["page"], limit: p["limit"] }) + + return this._request({ + url: url + query, + method: "GET", + ...(timeout ? { timeout } : {}), + ...opts, + headers, + }) + } + + async createBooking( + p: { + requestBody: t_Booking + }, + timeout?: number, + opts: AxiosRequestConfig = {}, + ): Promise< + AxiosResponse< + t_Booking & { + links?: t_Links_Self | undefined + } + > + > { + const url = `/bookings` + const headers = this._headers( + { "Content-Type": "application/json" }, + opts.headers, + ) + const body = JSON.stringify(p.requestBody) + + return this._request({ + url: url, + method: "POST", + data: body, + ...(timeout ? { timeout } : {}), + ...opts, + headers, + }) + } + + async getBooking( + p: { + bookingId: string + }, + timeout?: number, + opts: AxiosRequestConfig = {}, + ): Promise< + AxiosResponse< + t_Booking & { + links?: t_Links_Self | undefined + } + > + > { + const url = `/bookings/${p["bookingId"]}` + const headers = this._headers({}, opts.headers) + + return this._request({ + url: url, + method: "GET", + ...(timeout ? { timeout } : {}), + ...opts, + headers, + }) + } + + async deleteBooking( + p: { + bookingId: string + }, + timeout?: number, + opts: AxiosRequestConfig = {}, + ): Promise> { + const url = `/bookings/${p["bookingId"]}` + const headers = this._headers({}, opts.headers) + + return this._request({ + url: url, + method: "DELETE", + ...(timeout ? { timeout } : {}), + ...opts, + headers, + }) + } + + async createBookingPayment( + p: { + bookingId: string + requestBody: t_BookingPayment + }, + timeout?: number, + opts: AxiosRequestConfig = {}, + ): Promise< + AxiosResponse< + t_BookingPayment & { + links?: t_Links_Booking | undefined + } + > + > { + const url = `/bookings/${p["bookingId"]}/payment` + const headers = this._headers( + { "Content-Type": "application/json" }, + opts.headers, + ) + const body = JSON.stringify(p.requestBody) + + return this._request({ + url: url, + method: "POST", + data: body, + ...(timeout ? { timeout } : {}), + ...opts, + headers, + }) + } +} + +export { TrainTravelApi as ApiClient } +export type { TrainTravelApiConfig as ApiClientConfig } diff --git a/integration-tests/typescript-axios/src/generated/train-travel.yaml/models.ts b/integration-tests/typescript-axios/src/generated/train-travel.yaml/models.ts new file mode 100644 index 000000000..c675146ab --- /dev/null +++ b/integration-tests/typescript-axios/src/generated/train-travel.yaml/models.ts @@ -0,0 +1,98 @@ +/** AUTOGENERATED - DO NOT EDIT **/ +/* tslint:disable */ +/* eslint-disable */ + +export type EmptyObject = { [key: string]: never } + +export type t_Booking = { + has_bicycle?: boolean | undefined + has_dog?: boolean | undefined + readonly id?: string | undefined + passenger_name?: string | undefined + trip_id?: string | undefined +} + +export type t_BookingPayment = { + amount?: number | undefined + currency?: + | ("bam" | "bgn" | "chf" | "eur" | "gbp" | "nok" | "sek" | "try") + | undefined + readonly id?: string | undefined + source?: + | ( + | { + address_city?: string | undefined + address_country: string + address_line1?: string | undefined + address_line2?: string | undefined + address_post_code?: string | undefined + cvc: number + exp_month: number + exp_year: number + name: string + number: string + object?: string | undefined + } + | { + account_type: "individual" | "company" + bank_name: string + country: string + name: string + number: string + object?: string | undefined + sort_code?: string | undefined + } + ) + | undefined + readonly status?: ("pending" | "succeeded" | "failed") | undefined +} + +export type t_Links_Booking = { + booking?: string | undefined +} + +export type t_Links_Pagination = { + next?: string | undefined + prev?: string | undefined +} + +export type t_Links_Self = { + self?: string | undefined +} + +export type t_Problem = { + detail?: string | undefined + instance?: string | undefined + status?: number | undefined + title?: string | undefined + type?: string | undefined +} + +export type t_Station = { + address: string + country_code: string + id: string + name: string + timezone?: string | undefined +} + +export type t_Trip = { + arrival_time?: string | undefined + bicycles_allowed?: boolean | undefined + departure_time?: string | undefined + destination?: string | undefined + dogs_allowed?: boolean | undefined + id?: string | undefined + operator?: string | undefined + origin?: string | undefined + price?: number | undefined +} + +export type t_Wrapper_Collection = { + data?: + | { + [key: string]: unknown | undefined + }[] + | undefined + readonly links?: EmptyObject | undefined +} diff --git a/integration-tests/typescript-fetch/src/generated/train-travel.yaml/client.ts b/integration-tests/typescript-fetch/src/generated/train-travel.yaml/client.ts new file mode 100644 index 000000000..dd2a248b1 --- /dev/null +++ b/integration-tests/typescript-fetch/src/generated/train-travel.yaml/client.ts @@ -0,0 +1,297 @@ +/** AUTOGENERATED - DO NOT EDIT **/ +/* tslint:disable */ +/* eslint-disable */ + +import { + t_Booking, + t_BookingPayment, + t_Links_Booking, + t_Links_Pagination, + t_Links_Self, + t_Problem, + t_Station, + t_Trip, + t_Wrapper_Collection, +} from "./models" +import { + AbstractFetchClient, + AbstractFetchClientConfig, + Res, + Server, +} from "@nahkies/typescript-fetch-runtime/main" + +export class TrainTravelApiServers { + static default(): Server<"TrainTravelApi"> { + return TrainTravelApiServers.server().build() + } + + static server(url?: "https://try.microcks.io/rest/Train+Travel+API/1.0.0"): { + build: () => Server<"TrainTravelApi"> + } + static server(url?: "https://api.example.com"): { + build: () => Server<"TrainTravelApi"> + } + static server( + url: string = "https://try.microcks.io/rest/Train+Travel+API/1.0.0", + ): unknown { + switch (url) { + case "https://try.microcks.io/rest/Train+Travel+API/1.0.0": + return { + build(): Server<"TrainTravelApi"> { + return "https://try.microcks.io/rest/Train+Travel+API/1.0.0" as Server<"TrainTravelApi"> + }, + } + + case "https://api.example.com": + return { + build(): Server<"TrainTravelApi"> { + return "https://api.example.com" as Server<"TrainTravelApi"> + }, + } + + default: + throw new Error(`no matching server for url '${url}'`) + } + } +} + +export interface TrainTravelApiConfig extends AbstractFetchClientConfig { + basePath: Server<"TrainTravelApi"> | string +} + +export class TrainTravelApi extends AbstractFetchClient { + constructor(config: TrainTravelApiConfig) { + super(config) + } + + async getStations( + p: { + page?: number + limit?: number + coordinates?: string + search?: string + country?: string + } = {}, + timeout?: number, + opts: RequestInit = {}, + ): Promise< + | Res< + 200, + t_Wrapper_Collection & { + data?: t_Station[] + } & { + links?: t_Links_Self & t_Links_Pagination + } + > + | Res<400, t_Problem> + | Res<401, t_Problem> + | Res<403, t_Problem> + | Res<429, t_Problem> + | Res<500, t_Problem> + > { + const url = this.basePath + `/stations` + const headers = this._headers({}, opts.headers) + const query = this._query({ + page: p["page"], + limit: p["limit"], + coordinates: p["coordinates"], + search: p["search"], + country: p["country"], + }) + + return this._fetch( + url + query, + { method: "GET", ...opts, headers }, + timeout, + ) + } + + async getTrips( + p: { + page?: number + limit?: number + origin: string + destination: string + date: string + bicycles?: boolean + dogs?: boolean + }, + timeout?: number, + opts: RequestInit = {}, + ): Promise< + | Res< + 200, + t_Wrapper_Collection & { + data?: t_Trip[] + } & { + links?: t_Links_Self & t_Links_Pagination + } + > + | Res<400, t_Problem> + | Res<401, t_Problem> + | Res<403, t_Problem> + | Res<429, t_Problem> + | Res<500, t_Problem> + > { + const url = this.basePath + `/trips` + const headers = this._headers({}, opts.headers) + const query = this._query({ + page: p["page"], + limit: p["limit"], + origin: p["origin"], + destination: p["destination"], + date: p["date"], + bicycles: p["bicycles"], + dogs: p["dogs"], + }) + + return this._fetch( + url + query, + { method: "GET", ...opts, headers }, + timeout, + ) + } + + async getBookings( + p: { + page?: number + limit?: number + } = {}, + timeout?: number, + opts: RequestInit = {}, + ): Promise< + | Res< + 200, + t_Wrapper_Collection & { + data?: t_Booking[] + } & { + links?: t_Links_Self & t_Links_Pagination + } + > + | Res<400, t_Problem> + | Res<401, t_Problem> + | Res<403, t_Problem> + | Res<429, t_Problem> + | Res<500, t_Problem> + > { + const url = this.basePath + `/bookings` + const headers = this._headers({}, opts.headers) + const query = this._query({ page: p["page"], limit: p["limit"] }) + + return this._fetch( + url + query, + { method: "GET", ...opts, headers }, + timeout, + ) + } + + async createBooking( + p: { + requestBody: t_Booking + }, + timeout?: number, + opts: RequestInit = {}, + ): Promise< + | Res< + 201, + t_Booking & { + links?: t_Links_Self + } + > + | Res<400, t_Problem> + | Res<401, t_Problem> + | Res<404, t_Problem> + | Res<409, t_Problem> + | Res<429, t_Problem> + | Res<500, t_Problem> + > { + const url = this.basePath + `/bookings` + const headers = this._headers( + { "Content-Type": "application/json" }, + opts.headers, + ) + const body = JSON.stringify(p.requestBody) + + return this._fetch(url, { method: "POST", body, ...opts, headers }, timeout) + } + + async getBooking( + p: { + bookingId: string + }, + timeout?: number, + opts: RequestInit = {}, + ): Promise< + | Res< + 200, + t_Booking & { + links?: t_Links_Self + } + > + | Res<400, t_Problem> + | Res<401, t_Problem> + | Res<403, t_Problem> + | Res<404, t_Problem> + | Res<429, t_Problem> + | Res<500, t_Problem> + > { + const url = this.basePath + `/bookings/${p["bookingId"]}` + const headers = this._headers({}, opts.headers) + + return this._fetch(url, { method: "GET", ...opts, headers }, timeout) + } + + async deleteBooking( + p: { + bookingId: string + }, + timeout?: number, + opts: RequestInit = {}, + ): Promise< + | Res<204, void> + | Res<400, t_Problem> + | Res<401, t_Problem> + | Res<403, t_Problem> + | Res<404, t_Problem> + | Res<429, t_Problem> + | Res<500, t_Problem> + > { + const url = this.basePath + `/bookings/${p["bookingId"]}` + const headers = this._headers({}, opts.headers) + + return this._fetch(url, { method: "DELETE", ...opts, headers }, timeout) + } + + async createBookingPayment( + p: { + bookingId: string + requestBody: t_BookingPayment + }, + timeout?: number, + opts: RequestInit = {}, + ): Promise< + | Res< + 200, + t_BookingPayment & { + links?: t_Links_Booking + } + > + | Res<400, t_Problem> + | Res<401, t_Problem> + | Res<403, t_Problem> + | Res<429, t_Problem> + | Res<500, t_Problem> + > { + const url = this.basePath + `/bookings/${p["bookingId"]}/payment` + const headers = this._headers( + { "Content-Type": "application/json" }, + opts.headers, + ) + const body = JSON.stringify(p.requestBody) + + return this._fetch(url, { method: "POST", body, ...opts, headers }, timeout) + } +} + +export { TrainTravelApi as ApiClient } +export type { TrainTravelApiConfig as ApiClientConfig } diff --git a/integration-tests/typescript-fetch/src/generated/train-travel.yaml/models.ts b/integration-tests/typescript-fetch/src/generated/train-travel.yaml/models.ts new file mode 100644 index 000000000..23f9a3dab --- /dev/null +++ b/integration-tests/typescript-fetch/src/generated/train-travel.yaml/models.ts @@ -0,0 +1,91 @@ +/** AUTOGENERATED - DO NOT EDIT **/ +/* tslint:disable */ +/* eslint-disable */ + +export type EmptyObject = { [key: string]: never } + +export type t_Booking = { + has_bicycle?: boolean + has_dog?: boolean + readonly id?: string + passenger_name?: string + trip_id?: string +} + +export type t_BookingPayment = { + amount?: number + currency?: "bam" | "bgn" | "chf" | "eur" | "gbp" | "nok" | "sek" | "try" + readonly id?: string + source?: + | { + address_city?: string + address_country: string + address_line1?: string + address_line2?: string + address_post_code?: string + cvc: number + exp_month: number + exp_year: number + name: string + number: string + object?: string + } + | { + account_type: "individual" | "company" + bank_name: string + country: string + name: string + number: string + object?: string + sort_code?: string + } + readonly status?: "pending" | "succeeded" | "failed" +} + +export type t_Links_Booking = { + booking?: string +} + +export type t_Links_Pagination = { + next?: string + prev?: string +} + +export type t_Links_Self = { + self?: string +} + +export type t_Problem = { + detail?: string + instance?: string + status?: number + title?: string + type?: string +} + +export type t_Station = { + address: string + country_code: string + id: string + name: string + timezone?: string +} + +export type t_Trip = { + arrival_time?: string + bicycles_allowed?: boolean + departure_time?: string + destination?: string + dogs_allowed?: boolean + id?: string + operator?: string + origin?: string + price?: number +} + +export type t_Wrapper_Collection = { + data?: { + [key: string]: unknown | undefined + }[] + readonly links?: EmptyObject +} diff --git a/integration-tests/typescript-koa/src/generated/train-travel.yaml/generated.ts b/integration-tests/typescript-koa/src/generated/train-travel.yaml/generated.ts new file mode 100644 index 000000000..5339a2d88 --- /dev/null +++ b/integration-tests/typescript-koa/src/generated/train-travel.yaml/generated.ts @@ -0,0 +1,860 @@ +/** AUTOGENERATED - DO NOT EDIT **/ +/* tslint:disable */ +/* eslint-disable */ + +import { + t_Booking, + t_BookingPayment, + t_CreateBookingBodySchema, + t_CreateBookingPaymentBodySchema, + t_CreateBookingPaymentParamSchema, + t_DeleteBookingParamSchema, + t_GetBookingParamSchema, + t_GetBookingsQuerySchema, + t_GetStationsQuerySchema, + t_GetTripsQuerySchema, + t_Links_Booking, + t_Links_Pagination, + t_Links_Self, + t_Problem, + t_Station, + t_Trip, + t_Wrapper_Collection, +} from "./models" +import { + PermissiveBoolean, + s_Booking, + s_BookingPayment, + s_Links_Booking, + s_Links_Pagination, + s_Links_Self, + s_Problem, + s_Station, + s_Trip, + s_Wrapper_Collection, +} from "./schemas" +import KoaRouter, { RouterContext } from "@koa/router" +import { + KoaRuntimeError, + RequestInputType, +} from "@nahkies/typescript-koa-runtime/errors" +import { + KoaRuntimeResponder, + KoaRuntimeResponse, + Response, + ServerConfig, + StatusCode, + startServer, +} from "@nahkies/typescript-koa-runtime/server" +import { + Params, + parseRequestInput, + responseValidationFactory, +} from "@nahkies/typescript-koa-runtime/zod" +import { z } from "zod" + +export type GetStationsResponder = { + with200(): KoaRuntimeResponse< + t_Wrapper_Collection & { + data?: t_Station[] + } & { + links?: t_Links_Self & t_Links_Pagination + } + > + with400(): KoaRuntimeResponse + with401(): KoaRuntimeResponse + with403(): KoaRuntimeResponse + with429(): KoaRuntimeResponse + with500(): KoaRuntimeResponse +} & KoaRuntimeResponder + +export type GetStations = ( + params: Params, + respond: GetStationsResponder, + ctx: RouterContext, +) => Promise< + | KoaRuntimeResponse + | Response< + 200, + t_Wrapper_Collection & { + data?: t_Station[] + } & { + links?: t_Links_Self & t_Links_Pagination + } + > + | Response<400, t_Problem> + | Response<401, t_Problem> + | Response<403, t_Problem> + | Response<429, t_Problem> + | Response<500, t_Problem> +> + +export type GetTripsResponder = { + with200(): KoaRuntimeResponse< + t_Wrapper_Collection & { + data?: t_Trip[] + } & { + links?: t_Links_Self & t_Links_Pagination + } + > + with400(): KoaRuntimeResponse + with401(): KoaRuntimeResponse + with403(): KoaRuntimeResponse + with429(): KoaRuntimeResponse + with500(): KoaRuntimeResponse +} & KoaRuntimeResponder + +export type GetTrips = ( + params: Params, + respond: GetTripsResponder, + ctx: RouterContext, +) => Promise< + | KoaRuntimeResponse + | Response< + 200, + t_Wrapper_Collection & { + data?: t_Trip[] + } & { + links?: t_Links_Self & t_Links_Pagination + } + > + | Response<400, t_Problem> + | Response<401, t_Problem> + | Response<403, t_Problem> + | Response<429, t_Problem> + | Response<500, t_Problem> +> + +export type GetBookingsResponder = { + with200(): KoaRuntimeResponse< + t_Wrapper_Collection & { + data?: t_Booking[] + } & { + links?: t_Links_Self & t_Links_Pagination + } + > + with400(): KoaRuntimeResponse + with401(): KoaRuntimeResponse + with403(): KoaRuntimeResponse + with429(): KoaRuntimeResponse + with500(): KoaRuntimeResponse +} & KoaRuntimeResponder + +export type GetBookings = ( + params: Params, + respond: GetBookingsResponder, + ctx: RouterContext, +) => Promise< + | KoaRuntimeResponse + | Response< + 200, + t_Wrapper_Collection & { + data?: t_Booking[] + } & { + links?: t_Links_Self & t_Links_Pagination + } + > + | Response<400, t_Problem> + | Response<401, t_Problem> + | Response<403, t_Problem> + | Response<429, t_Problem> + | Response<500, t_Problem> +> + +export type CreateBookingResponder = { + with201(): KoaRuntimeResponse< + t_Booking & { + links?: t_Links_Self + } + > + with400(): KoaRuntimeResponse + with401(): KoaRuntimeResponse + with404(): KoaRuntimeResponse + with409(): KoaRuntimeResponse + with429(): KoaRuntimeResponse + with500(): KoaRuntimeResponse +} & KoaRuntimeResponder + +export type CreateBooking = ( + params: Params, + respond: CreateBookingResponder, + ctx: RouterContext, +) => Promise< + | KoaRuntimeResponse + | Response< + 201, + t_Booking & { + links?: t_Links_Self + } + > + | Response<400, t_Problem> + | Response<401, t_Problem> + | Response<404, t_Problem> + | Response<409, t_Problem> + | Response<429, t_Problem> + | Response<500, t_Problem> +> + +export type GetBookingResponder = { + with200(): KoaRuntimeResponse< + t_Booking & { + links?: t_Links_Self + } + > + with400(): KoaRuntimeResponse + with401(): KoaRuntimeResponse + with403(): KoaRuntimeResponse + with404(): KoaRuntimeResponse + with429(): KoaRuntimeResponse + with500(): KoaRuntimeResponse +} & KoaRuntimeResponder + +export type GetBooking = ( + params: Params, + respond: GetBookingResponder, + ctx: RouterContext, +) => Promise< + | KoaRuntimeResponse + | Response< + 200, + t_Booking & { + links?: t_Links_Self + } + > + | Response<400, t_Problem> + | Response<401, t_Problem> + | Response<403, t_Problem> + | Response<404, t_Problem> + | Response<429, t_Problem> + | Response<500, t_Problem> +> + +export type DeleteBookingResponder = { + with204(): KoaRuntimeResponse + with400(): KoaRuntimeResponse + with401(): KoaRuntimeResponse + with403(): KoaRuntimeResponse + with404(): KoaRuntimeResponse + with429(): KoaRuntimeResponse + with500(): KoaRuntimeResponse +} & KoaRuntimeResponder + +export type DeleteBooking = ( + params: Params, + respond: DeleteBookingResponder, + ctx: RouterContext, +) => Promise< + | KoaRuntimeResponse + | Response<204, void> + | Response<400, t_Problem> + | Response<401, t_Problem> + | Response<403, t_Problem> + | Response<404, t_Problem> + | Response<429, t_Problem> + | Response<500, t_Problem> +> + +export type CreateBookingPaymentResponder = { + with200(): KoaRuntimeResponse< + t_BookingPayment & { + links?: t_Links_Booking + } + > + with400(): KoaRuntimeResponse + with401(): KoaRuntimeResponse + with403(): KoaRuntimeResponse + with429(): KoaRuntimeResponse + with500(): KoaRuntimeResponse +} & KoaRuntimeResponder + +export type CreateBookingPayment = ( + params: Params< + t_CreateBookingPaymentParamSchema, + void, + t_CreateBookingPaymentBodySchema, + void + >, + respond: CreateBookingPaymentResponder, + ctx: RouterContext, +) => Promise< + | KoaRuntimeResponse + | Response< + 200, + t_BookingPayment & { + links?: t_Links_Booking + } + > + | Response<400, t_Problem> + | Response<401, t_Problem> + | Response<403, t_Problem> + | Response<429, t_Problem> + | Response<500, t_Problem> +> + +export type Implementation = { + getStations: GetStations + getTrips: GetTrips + getBookings: GetBookings + createBooking: CreateBooking + getBooking: GetBooking + deleteBooking: DeleteBooking + createBookingPayment: CreateBookingPayment +} + +export function createRouter(implementation: Implementation): KoaRouter { + const router = new KoaRouter() + + const getStationsQuerySchema = z.object({ + page: z.coerce.number().min(1).optional().default(1), + limit: z.coerce.number().min(1).max(100).optional().default(10), + coordinates: z.string().optional(), + search: z.string().optional(), + country: z.string().optional(), + }) + + const getStationsResponseValidator = responseValidationFactory( + [ + [ + "200", + s_Wrapper_Collection + .merge(z.object({ data: z.array(s_Station).optional() })) + .merge( + z.object({ + links: s_Links_Self.merge(s_Links_Pagination).optional(), + }), + ), + ], + ["400", s_Problem], + ["401", s_Problem], + ["403", s_Problem], + ["429", s_Problem], + ["500", s_Problem], + ], + undefined, + ) + + router.get("getStations", "/stations", async (ctx, next) => { + const input = { + params: undefined, + query: parseRequestInput( + getStationsQuerySchema, + ctx.query, + RequestInputType.QueryString, + ), + body: undefined, + headers: undefined, + } + + const responder = { + with200() { + return new KoaRuntimeResponse< + t_Wrapper_Collection & { + data?: t_Station[] + } & { + links?: t_Links_Self & t_Links_Pagination + } + >(200) + }, + with400() { + return new KoaRuntimeResponse(400) + }, + with401() { + return new KoaRuntimeResponse(401) + }, + with403() { + return new KoaRuntimeResponse(403) + }, + with429() { + return new KoaRuntimeResponse(429) + }, + with500() { + return new KoaRuntimeResponse(500) + }, + withStatus(status: StatusCode) { + return new KoaRuntimeResponse(status) + }, + } + + const response = await implementation + .getStations(input, responder, ctx) + .catch((err) => { + throw KoaRuntimeError.HandlerError(err) + }) + + const { status, body } = + response instanceof KoaRuntimeResponse ? response.unpack() : response + + ctx.body = getStationsResponseValidator(status, body) + ctx.status = status + return next() + }) + + const getTripsQuerySchema = z.object({ + page: z.coerce.number().min(1).optional().default(1), + limit: z.coerce.number().min(1).max(100).optional().default(10), + origin: z.string(), + destination: z.string(), + date: z.string().datetime({ offset: true }), + bicycles: PermissiveBoolean.optional().default(false), + dogs: PermissiveBoolean.optional().default(false), + }) + + const getTripsResponseValidator = responseValidationFactory( + [ + [ + "200", + s_Wrapper_Collection + .merge(z.object({ data: z.array(s_Trip).optional() })) + .merge( + z.object({ + links: s_Links_Self.merge(s_Links_Pagination).optional(), + }), + ), + ], + ["400", s_Problem], + ["401", s_Problem], + ["403", s_Problem], + ["429", s_Problem], + ["500", s_Problem], + ], + undefined, + ) + + router.get("getTrips", "/trips", async (ctx, next) => { + const input = { + params: undefined, + query: parseRequestInput( + getTripsQuerySchema, + ctx.query, + RequestInputType.QueryString, + ), + body: undefined, + headers: undefined, + } + + const responder = { + with200() { + return new KoaRuntimeResponse< + t_Wrapper_Collection & { + data?: t_Trip[] + } & { + links?: t_Links_Self & t_Links_Pagination + } + >(200) + }, + with400() { + return new KoaRuntimeResponse(400) + }, + with401() { + return new KoaRuntimeResponse(401) + }, + with403() { + return new KoaRuntimeResponse(403) + }, + with429() { + return new KoaRuntimeResponse(429) + }, + with500() { + return new KoaRuntimeResponse(500) + }, + withStatus(status: StatusCode) { + return new KoaRuntimeResponse(status) + }, + } + + const response = await implementation + .getTrips(input, responder, ctx) + .catch((err) => { + throw KoaRuntimeError.HandlerError(err) + }) + + const { status, body } = + response instanceof KoaRuntimeResponse ? response.unpack() : response + + ctx.body = getTripsResponseValidator(status, body) + ctx.status = status + return next() + }) + + const getBookingsQuerySchema = z.object({ + page: z.coerce.number().min(1).optional().default(1), + limit: z.coerce.number().min(1).max(100).optional().default(10), + }) + + const getBookingsResponseValidator = responseValidationFactory( + [ + [ + "200", + s_Wrapper_Collection + .merge(z.object({ data: z.array(s_Booking).optional() })) + .merge( + z.object({ + links: s_Links_Self.merge(s_Links_Pagination).optional(), + }), + ), + ], + ["400", s_Problem], + ["401", s_Problem], + ["403", s_Problem], + ["429", s_Problem], + ["500", s_Problem], + ], + undefined, + ) + + router.get("getBookings", "/bookings", async (ctx, next) => { + const input = { + params: undefined, + query: parseRequestInput( + getBookingsQuerySchema, + ctx.query, + RequestInputType.QueryString, + ), + body: undefined, + headers: undefined, + } + + const responder = { + with200() { + return new KoaRuntimeResponse< + t_Wrapper_Collection & { + data?: t_Booking[] + } & { + links?: t_Links_Self & t_Links_Pagination + } + >(200) + }, + with400() { + return new KoaRuntimeResponse(400) + }, + with401() { + return new KoaRuntimeResponse(401) + }, + with403() { + return new KoaRuntimeResponse(403) + }, + with429() { + return new KoaRuntimeResponse(429) + }, + with500() { + return new KoaRuntimeResponse(500) + }, + withStatus(status: StatusCode) { + return new KoaRuntimeResponse(status) + }, + } + + const response = await implementation + .getBookings(input, responder, ctx) + .catch((err) => { + throw KoaRuntimeError.HandlerError(err) + }) + + const { status, body } = + response instanceof KoaRuntimeResponse ? response.unpack() : response + + ctx.body = getBookingsResponseValidator(status, body) + ctx.status = status + return next() + }) + + const createBookingBodySchema = s_Booking + + const createBookingResponseValidator = responseValidationFactory( + [ + ["201", s_Booking.merge(z.object({ links: s_Links_Self.optional() }))], + ["400", s_Problem], + ["401", s_Problem], + ["404", s_Problem], + ["409", s_Problem], + ["429", s_Problem], + ["500", s_Problem], + ], + undefined, + ) + + router.post("createBooking", "/bookings", async (ctx, next) => { + const input = { + params: undefined, + query: undefined, + body: parseRequestInput( + createBookingBodySchema, + Reflect.get(ctx.request, "body"), + RequestInputType.RequestBody, + ), + headers: undefined, + } + + const responder = { + with201() { + return new KoaRuntimeResponse< + t_Booking & { + links?: t_Links_Self + } + >(201) + }, + with400() { + return new KoaRuntimeResponse(400) + }, + with401() { + return new KoaRuntimeResponse(401) + }, + with404() { + return new KoaRuntimeResponse(404) + }, + with409() { + return new KoaRuntimeResponse(409) + }, + with429() { + return new KoaRuntimeResponse(429) + }, + with500() { + return new KoaRuntimeResponse(500) + }, + withStatus(status: StatusCode) { + return new KoaRuntimeResponse(status) + }, + } + + const response = await implementation + .createBooking(input, responder, ctx) + .catch((err) => { + throw KoaRuntimeError.HandlerError(err) + }) + + const { status, body } = + response instanceof KoaRuntimeResponse ? response.unpack() : response + + ctx.body = createBookingResponseValidator(status, body) + ctx.status = status + return next() + }) + + const getBookingParamSchema = z.object({ bookingId: z.string() }) + + const getBookingResponseValidator = responseValidationFactory( + [ + ["200", s_Booking.merge(z.object({ links: s_Links_Self.optional() }))], + ["400", s_Problem], + ["401", s_Problem], + ["403", s_Problem], + ["404", s_Problem], + ["429", s_Problem], + ["500", s_Problem], + ], + undefined, + ) + + router.get("getBooking", "/bookings/:bookingId", async (ctx, next) => { + const input = { + params: parseRequestInput( + getBookingParamSchema, + ctx.params, + RequestInputType.RouteParam, + ), + query: undefined, + body: undefined, + headers: undefined, + } + + const responder = { + with200() { + return new KoaRuntimeResponse< + t_Booking & { + links?: t_Links_Self + } + >(200) + }, + with400() { + return new KoaRuntimeResponse(400) + }, + with401() { + return new KoaRuntimeResponse(401) + }, + with403() { + return new KoaRuntimeResponse(403) + }, + with404() { + return new KoaRuntimeResponse(404) + }, + with429() { + return new KoaRuntimeResponse(429) + }, + with500() { + return new KoaRuntimeResponse(500) + }, + withStatus(status: StatusCode) { + return new KoaRuntimeResponse(status) + }, + } + + const response = await implementation + .getBooking(input, responder, ctx) + .catch((err) => { + throw KoaRuntimeError.HandlerError(err) + }) + + const { status, body } = + response instanceof KoaRuntimeResponse ? response.unpack() : response + + ctx.body = getBookingResponseValidator(status, body) + ctx.status = status + return next() + }) + + const deleteBookingParamSchema = z.object({ bookingId: z.string() }) + + const deleteBookingResponseValidator = responseValidationFactory( + [ + ["204", z.undefined()], + ["400", s_Problem], + ["401", s_Problem], + ["403", s_Problem], + ["404", s_Problem], + ["429", s_Problem], + ["500", s_Problem], + ], + undefined, + ) + + router.delete("deleteBooking", "/bookings/:bookingId", async (ctx, next) => { + const input = { + params: parseRequestInput( + deleteBookingParamSchema, + ctx.params, + RequestInputType.RouteParam, + ), + query: undefined, + body: undefined, + headers: undefined, + } + + const responder = { + with204() { + return new KoaRuntimeResponse(204) + }, + with400() { + return new KoaRuntimeResponse(400) + }, + with401() { + return new KoaRuntimeResponse(401) + }, + with403() { + return new KoaRuntimeResponse(403) + }, + with404() { + return new KoaRuntimeResponse(404) + }, + with429() { + return new KoaRuntimeResponse(429) + }, + with500() { + return new KoaRuntimeResponse(500) + }, + withStatus(status: StatusCode) { + return new KoaRuntimeResponse(status) + }, + } + + const response = await implementation + .deleteBooking(input, responder, ctx) + .catch((err) => { + throw KoaRuntimeError.HandlerError(err) + }) + + const { status, body } = + response instanceof KoaRuntimeResponse ? response.unpack() : response + + ctx.body = deleteBookingResponseValidator(status, body) + ctx.status = status + return next() + }) + + const createBookingPaymentParamSchema = z.object({ bookingId: z.string() }) + + const createBookingPaymentBodySchema = s_BookingPayment + + const createBookingPaymentResponseValidator = responseValidationFactory( + [ + [ + "200", + s_BookingPayment.merge(z.object({ links: s_Links_Booking.optional() })), + ], + ["400", s_Problem], + ["401", s_Problem], + ["403", s_Problem], + ["429", s_Problem], + ["500", s_Problem], + ], + undefined, + ) + + router.post( + "createBookingPayment", + "/bookings/:bookingId/payment", + async (ctx, next) => { + const input = { + params: parseRequestInput( + createBookingPaymentParamSchema, + ctx.params, + RequestInputType.RouteParam, + ), + query: undefined, + body: parseRequestInput( + createBookingPaymentBodySchema, + Reflect.get(ctx.request, "body"), + RequestInputType.RequestBody, + ), + headers: undefined, + } + + const responder = { + with200() { + return new KoaRuntimeResponse< + t_BookingPayment & { + links?: t_Links_Booking + } + >(200) + }, + with400() { + return new KoaRuntimeResponse(400) + }, + with401() { + return new KoaRuntimeResponse(401) + }, + with403() { + return new KoaRuntimeResponse(403) + }, + with429() { + return new KoaRuntimeResponse(429) + }, + with500() { + return new KoaRuntimeResponse(500) + }, + withStatus(status: StatusCode) { + return new KoaRuntimeResponse(status) + }, + } + + const response = await implementation + .createBookingPayment(input, responder, ctx) + .catch((err) => { + throw KoaRuntimeError.HandlerError(err) + }) + + const { status, body } = + response instanceof KoaRuntimeResponse ? response.unpack() : response + + ctx.body = createBookingPaymentResponseValidator(status, body) + ctx.status = status + return next() + }, + ) + + return router +} + +export async function bootstrap(config: ServerConfig) { + // Train Travel API + return startServer(config) +} diff --git a/integration-tests/typescript-koa/src/generated/train-travel.yaml/models.ts b/integration-tests/typescript-koa/src/generated/train-travel.yaml/models.ts new file mode 100644 index 000000000..6e3f6a6b6 --- /dev/null +++ b/integration-tests/typescript-koa/src/generated/train-travel.yaml/models.ts @@ -0,0 +1,164 @@ +/** AUTOGENERATED - DO NOT EDIT **/ +/* tslint:disable */ +/* eslint-disable */ + +export type EmptyObject = { [key: string]: never } + +export type t_Booking = { + has_bicycle?: boolean + has_dog?: boolean + readonly id?: string + passenger_name?: string + trip_id?: string +} + +export type t_BookingPayment = { + amount?: number + currency?: "bam" | "bgn" | "chf" | "eur" | "gbp" | "nok" | "sek" | "try" + readonly id?: string + source?: + | { + address_city?: string + address_country: string + address_line1?: string + address_line2?: string + address_post_code?: string + cvc: number + exp_month: number + exp_year: number + name: string + number: string + object?: string + } + | { + account_type: "individual" | "company" + bank_name: string + country: string + name: string + number: string + object?: string + sort_code?: string + } + readonly status?: "pending" | "succeeded" | "failed" +} + +export type t_Links_Booking = { + booking?: string +} + +export type t_Links_Pagination = { + next?: string + prev?: string +} + +export type t_Links_Self = { + self?: string +} + +export type t_Problem = { + detail?: string + instance?: string + status?: number + title?: string + type?: string +} + +export type t_Station = { + address: string + country_code: string + id: string + name: string + timezone?: string +} + +export type t_Trip = { + arrival_time?: string + bicycles_allowed?: boolean + departure_time?: string + destination?: string + dogs_allowed?: boolean + id?: string + operator?: string + origin?: string + price?: number +} + +export type t_Wrapper_Collection = { + data?: { + [key: string]: unknown | undefined + }[] + readonly links?: EmptyObject +} + +export type t_CreateBookingBodySchema = { + has_bicycle?: boolean + has_dog?: boolean + readonly id?: string + passenger_name?: string + trip_id?: string +} + +export type t_CreateBookingPaymentBodySchema = { + amount?: number + currency?: "bam" | "bgn" | "chf" | "eur" | "gbp" | "nok" | "sek" | "try" + readonly id?: string + source?: + | { + address_city?: string + address_country: string + address_line1?: string + address_line2?: string + address_post_code?: string + cvc: number + exp_month: number + exp_year: number + name: string + number: string + object?: string + } + | { + account_type: "individual" | "company" + bank_name: string + country: string + name: string + number: string + object?: string + sort_code?: string + } + readonly status?: "pending" | "succeeded" | "failed" +} + +export type t_CreateBookingPaymentParamSchema = { + bookingId: string +} + +export type t_DeleteBookingParamSchema = { + bookingId: string +} + +export type t_GetBookingParamSchema = { + bookingId: string +} + +export type t_GetBookingsQuerySchema = { + limit?: number + page?: number +} + +export type t_GetStationsQuerySchema = { + coordinates?: string + country?: string + limit?: number + page?: number + search?: string +} + +export type t_GetTripsQuerySchema = { + bicycles?: boolean + date: string + destination: string + dogs?: boolean + limit?: number + origin: string + page?: number +} diff --git a/integration-tests/typescript-koa/src/generated/train-travel.yaml/schemas.ts b/integration-tests/typescript-koa/src/generated/train-travel.yaml/schemas.ts new file mode 100644 index 000000000..685fff4e0 --- /dev/null +++ b/integration-tests/typescript-koa/src/generated/train-travel.yaml/schemas.ts @@ -0,0 +1,99 @@ +/** AUTOGENERATED - DO NOT EDIT **/ +/* tslint:disable */ +/* eslint-disable */ + +import { z } from "zod" + +export const PermissiveBoolean = z.preprocess((value) => { + if (typeof value === "string" && (value === "true" || value === "false")) { + return value === "true" + } else if (typeof value === "number" && (value === 1 || value === 0)) { + return value === 1 + } + return value +}, z.boolean()) + +export const s_Booking = z.object({ + id: z.string().optional(), + trip_id: z.string().optional(), + passenger_name: z.string().optional(), + has_bicycle: PermissiveBoolean.optional(), + has_dog: PermissiveBoolean.optional(), +}) + +export const s_BookingPayment = z.object({ + id: z.string().optional(), + amount: z.coerce.number().gt(0).optional(), + currency: z + .enum(["bam", "bgn", "chf", "eur", "gbp", "nok", "sek", "try"]) + .optional(), + source: z + .union([ + z.object({ + object: z.string().optional(), + name: z.string(), + number: z.string(), + cvc: z.coerce.number(), + exp_month: z.coerce.number(), + exp_year: z.coerce.number(), + address_line1: z.string().optional(), + address_line2: z.string().optional(), + address_city: z.string().optional(), + address_country: z.string(), + address_post_code: z.string().optional(), + }), + z.object({ + object: z.string().optional(), + name: z.string(), + number: z.string(), + sort_code: z.string().optional(), + account_type: z.enum(["individual", "company"]), + bank_name: z.string(), + country: z.string(), + }), + ]) + .optional(), + status: z.enum(["pending", "succeeded", "failed"]).optional(), +}) + +export const s_Links_Booking = z.object({ booking: z.string().optional() }) + +export const s_Links_Pagination = z.object({ + next: z.string().optional(), + prev: z.string().optional(), +}) + +export const s_Links_Self = z.object({ self: z.string().optional() }) + +export const s_Problem = z.object({ + type: z.string().optional(), + title: z.string().optional(), + detail: z.string().optional(), + instance: z.string().optional(), + status: z.coerce.number().optional(), +}) + +export const s_Station = z.object({ + id: z.string(), + name: z.string(), + address: z.string(), + country_code: z.string(), + timezone: z.string().optional(), +}) + +export const s_Trip = z.object({ + id: z.string().optional(), + origin: z.string().optional(), + destination: z.string().optional(), + departure_time: z.string().datetime({ offset: true }).optional(), + arrival_time: z.string().datetime({ offset: true }).optional(), + operator: z.string().optional(), + price: z.coerce.number().optional(), + bicycles_allowed: PermissiveBoolean.optional(), + dogs_allowed: PermissiveBoolean.optional(), +}) + +export const s_Wrapper_Collection = z.object({ + data: z.array(z.record(z.unknown())).optional(), + links: z.object({}).optional(), +})