diff --git a/packages/cli/cli/src/commands/upgrade/updateApiSpec.ts b/packages/cli/cli/src/commands/upgrade/updateApiSpec.ts index e5788ebd846..2ae7af198dd 100644 --- a/packages/cli/cli/src/commands/upgrade/updateApiSpec.ts +++ b/packages/cli/cli/src/commands/upgrade/updateApiSpec.ts @@ -1,10 +1,10 @@ import { generatorsYml, getFernDirectory } from "@fern-api/configuration"; +import { join, RelativeFilePath } from "@fern-api/fs-utils"; import { Logger } from "@fern-api/logger"; import { Project } from "@fern-api/project-loader"; import * as fs from "fs"; import { readFile, writeFile } from "fs/promises"; import yaml from "js-yaml"; -import path from "path"; import { Readable } from "stream"; import { finished } from "stream/promises"; import { ReadableStream } from "stream/web"; @@ -63,7 +63,11 @@ export async function updateApiSpec({ for (const api of apis) { if (typeof api !== "string" && api.origin != null) { cliContext.logger.info(`Origin found, fetching spec from ${api.origin}`); - await fetchAndWriteFile(api.origin, path.join(fernDirectory, api.path), cliContext.logger); + await fetchAndWriteFile( + api.origin, + join(workspace.absoluteFilepath, RelativeFilePath.of(api.path)), + cliContext.logger + ); } } } else if (generatorConfig[generatorsYml.ASYNC_API_LOCATION_KEY] != null) { @@ -74,7 +78,11 @@ export async function updateApiSpec({ const origin = generatorConfig[generatorsYml.API_ORIGIN_LOCATION_KEY]; const location = generatorConfig[generatorsYml.ASYNC_API_LOCATION_KEY]; if (origin != null && location != null) { - await fetchAndWriteFile(origin, path.join(fernDirectory, location), cliContext.logger); + await fetchAndWriteFile( + origin, + join(workspace.absoluteFilepath, RelativeFilePath.of(location)), + cliContext.logger + ); } } } else if (generatorConfig[generatorsYml.OPENAPI_LOCATION_KEY] != null) { @@ -89,7 +97,11 @@ export async function updateApiSpec({ if (apiOrigin != null && apiOutput != null) { origin = apiOrigin; cliContext.logger.info(`Origin found, fetching spec from ${apiOrigin}`); - await fetchAndWriteFile(apiOrigin, path.join(fernDirectory, apiOutput), cliContext.logger); + await fetchAndWriteFile( + apiOrigin, + join(workspace.absoluteFilepath, RelativeFilePath.of(apiOutput)), + cliContext.logger + ); } } return; diff --git a/packages/cli/ete-tests/src/tests/update-api-unioned/__snapshots__/update-api.test.ts.snap b/packages/cli/ete-tests/src/tests/update-api-unioned/__snapshots__/update-api.test.ts.snap new file mode 100644 index 00000000000..a8806ec4bf0 --- /dev/null +++ b/packages/cli/ete-tests/src/tests/update-api-unioned/__snapshots__/update-api.test.ts.snap @@ -0,0 +1,797 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`fern api update unioned fern api update unioned 1`] = ` +[ + { + "contents": "default-group: local +api: + path: ./openapi.json + origin: https://bump.sh/bump-examples/doc/train-travel-api.json +", + "name": "generators.yml", + "type": "file", + }, + { + "contents": "{ + "info": { + "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" + } + }, + "servers": [ + { + "url": "https://api.example.com", + "description": "Production" + } + ], + "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\\nand local timezone.\\n" + }, + { + "name": "Train Tracks", + "description": "Find and filter all the different rail roads available across Europe, including their location\\nand local timezone.\\n" + }, + { + "name": "Trips", + "description": "Timetables and routes for train trips between stations, including pricing\\nand availability.\\n" + }, + { + "name": "Bookings", + "description": "Create and manage bookings for train trips, including passenger details\\nand optional extras.\\n" + }, + { + "name": "Payments", + "description": "Pay for bookings using a card or bank account, and view payment\\nstatus and history.\\n\\n> warn\\n> Bookings usually expire within 1 hour so you'll need to make your payment\\n> before the expiry date \\n" + } + ], + "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.\\n", + "tags": [ + "Bookings" + ], + "requestBody": { + "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": { + "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" + } + } + }, + "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": [ + "Berlin Hauptbahnhof", + "Paris Gare du Nord" + ] + }, + "destination": { + "type": "string", + "description": "The destination station of the trip", + "examples": [ + "Paris Gare du Nord", + "Berlin Hauptbahnhof" + ] + }, + "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.", + "anyOf": [ + { + "title": "Card", + "description": "A card (debit or credit) to take payment from.", + "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": { + "RateLimit": { + "description": "The RateLimit header communicates quota policies. It contains a \`limit\` to\\nconvey the expiring limit, \`remaining\` to convey the remaining quota units,\\nand \`reset\` to convey the time window reset time.\\n", + "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. \\nThe value is in seconds and can be an integer or a date in the future. \\nIf the value is an integer, it indicates the number of seconds to wait. \\nIf the value is a date, it indicates the time at which the user agent should make a follow-up request. \\n", + "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." + } + } + } + } + } + } +}", + "name": "openapi.json", + "type": "file", + }, +] +`; diff --git a/packages/cli/ete-tests/src/tests/update-api-unioned/fixtures/fern/fern.config.json b/packages/cli/ete-tests/src/tests/update-api-unioned/fixtures/fern/fern.config.json new file mode 100644 index 00000000000..2e3e1df85fd --- /dev/null +++ b/packages/cli/ete-tests/src/tests/update-api-unioned/fixtures/fern/fern.config.json @@ -0,0 +1,4 @@ +{ + "version": "*", + "organization": "fern" +} \ No newline at end of file diff --git a/packages/cli/ete-tests/src/tests/update-api-unioned/fixtures/fern/spec1/generators.yml b/packages/cli/ete-tests/src/tests/update-api-unioned/fixtures/fern/spec1/generators.yml new file mode 100644 index 00000000000..cf0562dda16 --- /dev/null +++ b/packages/cli/ete-tests/src/tests/update-api-unioned/fixtures/fern/spec1/generators.yml @@ -0,0 +1,4 @@ +default-group: local +api: + path: ./openapi.json + origin: https://bump.sh/bump-examples/doc/train-travel-api.json diff --git a/packages/cli/ete-tests/src/tests/update-api-unioned/fixtures/fern/spec1/openapi.json b/packages/cli/ete-tests/src/tests/update-api-unioned/fixtures/fern/spec1/openapi.json new file mode 100644 index 00000000000..3a835787863 --- /dev/null +++ b/packages/cli/ete-tests/src/tests/update-api-unioned/fixtures/fern/spec1/openapi.json @@ -0,0 +1,778 @@ +{ + "info": { + "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" + } + }, + "servers": [ + { + "url": "https://api.example.com", + "description": "Production" + } + ], + "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\nand local timezone.\n" + }, + { + "name": "Train Tracks", + "description": "Find and filter all the different rail roads available across Europe, including their location\nand local timezone.\n" + }, + { + "name": "Trips", + "description": "Timetables and routes for train trips between stations, including pricing\nand availability.\n" + }, + { + "name": "Bookings", + "description": "Create and manage bookings for train trips, including passenger details\nand optional extras.\n" + }, + { + "name": "Payments", + "description": "Pay for bookings using a card or bank account, and view payment\nstatus and history.\n\n> warn\n> Bookings usually expire within 1 hour so you'll need to make your payment\n> before the expiry date \n" + } + ], + "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.\n", + "tags": [ + "Bookings" + ], + "requestBody": { + "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": { + "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" + } + } + }, + "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": [ + "Berlin Hauptbahnhof", + "Paris Gare du Nord" + ] + }, + "destination": { + "type": "string", + "description": "The destination station of the trip", + "examples": [ + "Paris Gare du Nord", + "Berlin Hauptbahnhof" + ] + }, + "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.", + "anyOf": [ + { + "title": "Card", + "description": "A card (debit or credit) to take payment from.", + "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": { + "RateLimit": { + "description": "The RateLimit header communicates quota policies. It contains a `limit` to\nconvey the expiring limit, `remaining` to convey the remaining quota units,\nand `reset` to convey the time window reset time.\n", + "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. \nThe value is in seconds and can be an integer or a date in the future. \nIf the value is an integer, it indicates the number of seconds to wait. \nIf the value is a date, it indicates the time at which the user agent should make a follow-up request. \n", + "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." + } + } + } + } + } + } +} \ No newline at end of file diff --git a/packages/cli/ete-tests/src/tests/update-api-unioned/fixtures/fern/spec2/generators.yml b/packages/cli/ete-tests/src/tests/update-api-unioned/fixtures/fern/spec2/generators.yml new file mode 100644 index 00000000000..cf0562dda16 --- /dev/null +++ b/packages/cli/ete-tests/src/tests/update-api-unioned/fixtures/fern/spec2/generators.yml @@ -0,0 +1,4 @@ +default-group: local +api: + path: ./openapi.json + origin: https://bump.sh/bump-examples/doc/train-travel-api.json diff --git a/packages/cli/ete-tests/src/tests/update-api-unioned/fixtures/fern/spec2/openapi.json b/packages/cli/ete-tests/src/tests/update-api-unioned/fixtures/fern/spec2/openapi.json new file mode 100644 index 00000000000..a7705520202 --- /dev/null +++ b/packages/cli/ete-tests/src/tests/update-api-unioned/fixtures/fern/spec2/openapi.json @@ -0,0 +1,1435 @@ +{ + "info": { + "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" + } + }, + "servers": [ + { + "url": "https://api.example.com", + "description": "Production" + } + ], + "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\nand local timezone.\n" + }, + { + "name": "Train Tracks", + "description": "Find and filter all the different rail roads available across Europe, including their location\nand local timezone.\n" + }, + { + "name": "Trips", + "description": "Timetables and routes for train trips between stations, including pricing\nand availability.\n" + }, + { + "name": "Bookings", + "description": "Create and manage bookings for train trips, including passenger details\nand optional extras.\n" + }, + { + "name": "Payments", + "description": "Pay for bookings using a card or bank account, and view payment\nstatus and history.\n\n> warn\n> Bookings usually expire within 1 hour so you'll need to make your payment\n> before the expiry date \n" + } + ], + "paths": { + "/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": [ + { + "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": { + "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" + ], + "responses": { + "200": { + "description": "A list of bookings", + "headers": { + "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": { + "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": { + "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": { + "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" + } + } + } + } + }, + "components": { + "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" + } + } + }, + "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": [ + "Berlin Hauptbahnhof", + "Paris Gare du Nord" + ] + }, + "destination": { + "type": "string", + "description": "The destination station of the trip", + "examples": [ + "Paris Gare du Nord", + "Berlin Hauptbahnhof" + ] + }, + "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.", + "anyOf": [ + { + "title": "Card", + "description": "A card (debit or credit) to take payment from.", + "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": { + "RateLimit": { + "description": "The RateLimit header communicates quota policies. It contains a `limit` to\nconvey the expiring limit, `remaining` to convey the remaining quota units,\nand `reset` to convey the time window reset time.\n", + "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. \nThe value is in seconds and can be an integer or a date in the future. \nIf the value is an integer, it indicates the number of seconds to wait. \nIf the value is a date, it indicates the time at which the user agent should make a follow-up request. \n", + "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." + } + } + } + } + } + } +} \ No newline at end of file diff --git a/packages/cli/ete-tests/src/tests/update-api-unioned/fixtures/fern/unioned/api.yml b/packages/cli/ete-tests/src/tests/update-api-unioned/fixtures/fern/unioned/api.yml new file mode 100644 index 00000000000..a939faab6cf --- /dev/null +++ b/packages/cli/ete-tests/src/tests/update-api-unioned/fixtures/fern/unioned/api.yml @@ -0,0 +1 @@ +name: api \ No newline at end of file diff --git a/packages/cli/ete-tests/src/tests/update-api-unioned/fixtures/fern/unioned/definition/spec1/__package__.yml b/packages/cli/ete-tests/src/tests/update-api-unioned/fixtures/fern/unioned/definition/spec1/__package__.yml new file mode 100644 index 00000000000..7e022f0d628 --- /dev/null +++ b/packages/cli/ete-tests/src/tests/update-api-unioned/fixtures/fern/unioned/definition/spec1/__package__.yml @@ -0,0 +1 @@ +export: spec1 \ No newline at end of file diff --git a/packages/cli/ete-tests/src/tests/update-api-unioned/fixtures/fern/unioned/definition/spec2/__package__.yml b/packages/cli/ete-tests/src/tests/update-api-unioned/fixtures/fern/unioned/definition/spec2/__package__.yml new file mode 100644 index 00000000000..aad3c796b2f --- /dev/null +++ b/packages/cli/ete-tests/src/tests/update-api-unioned/fixtures/fern/unioned/definition/spec2/__package__.yml @@ -0,0 +1 @@ +export: spec2 \ No newline at end of file diff --git a/packages/cli/ete-tests/src/tests/update-api-unioned/fixtures/fern/unioned/dependencies.yml b/packages/cli/ete-tests/src/tests/update-api-unioned/fixtures/fern/unioned/dependencies.yml new file mode 100644 index 00000000000..45e0bfea97e --- /dev/null +++ b/packages/cli/ete-tests/src/tests/update-api-unioned/fixtures/fern/unioned/dependencies.yml @@ -0,0 +1,3 @@ +dependencies: + spec1: ../spec1 + spec2: ../spec2 diff --git a/packages/cli/ete-tests/src/tests/update-api-unioned/fixtures/fern/unioned/generators.yml b/packages/cli/ete-tests/src/tests/update-api-unioned/fixtures/fern/unioned/generators.yml new file mode 100644 index 00000000000..c8ae78dfffa --- /dev/null +++ b/packages/cli/ete-tests/src/tests/update-api-unioned/fixtures/fern/unioned/generators.yml @@ -0,0 +1,9 @@ +default-group: local +groups: + local: + generators: + - name: fernapi/fern-typescript-node-sdk + version: 0.9.5 + output: + location: local-file-system + path: ../sdks/typescript diff --git a/packages/cli/ete-tests/src/tests/update-api-unioned/update-api.test.ts b/packages/cli/ete-tests/src/tests/update-api-unioned/update-api.test.ts new file mode 100644 index 00000000000..235d4f3497f --- /dev/null +++ b/packages/cli/ete-tests/src/tests/update-api-unioned/update-api.test.ts @@ -0,0 +1,25 @@ +import { AbsoluteFilePath, getDirectoryContents } from "@fern-api/fs-utils"; +import { cp } from "fs/promises"; +import path from "path"; +import tmp from "tmp-promise"; +import { runFernCli } from "../../utils/runFernCli"; + +const FIXTURES_DIR = path.join(__dirname, "fixtures"); + +describe("fern api update unioned", () => { + it("fern api update unioned", async () => { + // Create tmpdir and copy contents + const tmpDir = await tmp.dir(); + const directory = AbsoluteFilePath.of(tmpDir.path); + + await cp(FIXTURES_DIR, directory, { recursive: true }); + + const outputPath = AbsoluteFilePath.of(path.join(directory, "fern", "spec1")); + + await runFernCli(["api", "update"], { + cwd: directory + }); + + expect(await getDirectoryContents(outputPath)).toMatchSnapshot(); + }, 60_000); +});