diff --git a/packages/parsers/package.json b/packages/parsers/package.json index fd9fc05d98..5829c82d9c 100644 --- a/packages/parsers/package.json +++ b/packages/parsers/package.json @@ -1,6 +1,6 @@ { "name": "@fern-api/docs-parsers", - "version": "0.0.20", + "version": "0.0.22", "repository": { "type": "git", "url": "https://github.com/fern-api/fern-platform.git", diff --git a/packages/parsers/src/__test__/__snapshots__/openapi/cohere.json b/packages/parsers/src/__test__/__snapshots__/openapi/cohere.json index f904d90b95..5968d256f7 100644 --- a/packages/parsers/src/__test__/__snapshots__/openapi/cohere.json +++ b/packages/parsers/src/__test__/__snapshots__/openapi/cohere.json @@ -603,7 +603,30 @@ } } ], - "responses": [], + "responses": [ + { + "statusCode": 200, + "body": { + "type": "alias", + "value": { + "type": "id", + "id": "NonStreamedChatResponse" + } + }, + "description": "OK" + }, + { + "statusCode": 200, + "body": { + "type": "alias", + "value": { + "type": "id", + "id": "StreamedChatResponse" + } + }, + "description": "OK" + } + ], "errors": [ { "statusCode": 400, @@ -1623,6 +1646,28 @@ } ] } + }, + { + "path": "/v1/chat", + "responseStatusCode": 200, + "responseBody": { + "type": "json", + "value": { + "text": "string" + } + }, + "snippets": {} + }, + { + "path": "/v1/chat", + "responseStatusCode": 200, + "responseBody": { + "type": "json", + "value": { + "text": "string" + } + }, + "snippets": {} } ] }, @@ -1768,8 +1813,13 @@ "value": { "type": "list", "itemShape": { - "type": "undiscriminatedUnion", - "variants": [] + "type": "alias", + "value": { + "type": "primitive", + "value": { + "type": "string" + } + } } } } @@ -2022,18 +2072,15 @@ } ] } - } - ], - "responses": [], - "errors": [ + }, { - "statusCode": 400, - "shape": { + "contentType": "application/json", + "body": { "type": "object", "extends": [], "properties": [ { - "key": "data", + "key": "model", "valueShape": { "type": "alias", "value": { @@ -2042,168 +2089,535 @@ "type": "string" } } - } - } - ] - }, - "name": "Bad Request", - "examples": [ - { - "responseBody": { - "type": "json", - "value": { - "data": "string" - } - } - } - ] - }, - { - "statusCode": 401, - "shape": { - "type": "object", - "extends": [], - "properties": [ + }, + "description": "The name of a compatible [Cohere model](https://docs.cohere.com/v2/docs/models) (such as command-r or command-r-plus) or the ID of a [fine-tuned](https://docs.cohere.com/v2/docs/chat-fine-tuning) model." + }, { - "key": "data", + "key": "messages", "valueShape": { "type": "alias", "value": { - "type": "primitive", - "value": { - "type": "string" - } + "type": "id", + "id": "ChatMessages" } } - } - ] - }, - "name": "Unauthorized", - "examples": [ - { - "responseBody": { - "type": "json", - "value": { - "data": "string" - } - } - } - ] - }, - { - "statusCode": 403, - "shape": { - "type": "object", - "extends": [], - "properties": [ + }, { - "key": "data", + "key": "tools", "valueShape": { "type": "alias", "value": { - "type": "primitive", - "value": { - "type": "string" + "type": "optional", + "shape": { + "type": "alias", + "value": { + "type": "list", + "itemShape": { + "type": "alias", + "value": { + "type": "id", + "id": "ToolV2" + } + } + } } } - } - } - ] - }, - "name": "Forbidden", - "examples": [ - { - "responseBody": { - "type": "json", - "value": { - "data": "string" - } - } - } - ] - }, - { - "statusCode": 404, - "shape": { - "type": "object", - "extends": [], - "properties": [ + }, + "description": "A list of available tools (functions) that the model may suggest invoking before producing a text response.\n\nWhen `tools` is passed (without `tool_results`), the `text` content in the response will be empty and the `tool_calls` field in the response will be populated with a list of tool calls that need to be made. If no calls need to be made, the `tool_calls` array will be empty.\n" + }, { - "key": "data", + "key": "strict_tools", "valueShape": { "type": "alias", "value": { - "type": "primitive", - "value": { - "type": "string" + "type": "optional", + "shape": { + "type": "alias", + "value": { + "type": "primitive", + "value": { + "type": "boolean" + } + } } } - } - } - ] - }, - "name": "Not Found", - "examples": [ - { - "responseBody": { - "type": "json", - "value": { - "data": "string" - } - } - } - ] - }, - { - "statusCode": 422, - "shape": { - "type": "object", - "extends": [], - "properties": [ + }, + "description": "When set to `true`, tool calls in the Assistant message will be forced to follow the tool definition strictly. Learn more in the [Strict Tools guide](https://docs.cohere.com/docs/structured-outputs-json#structured-outputs-tools).\n\n**Note**: The first few requests with a new set of tools will take longer to process.\n", + "availability": "Beta" + }, { - "key": "data", + "key": "documents", "valueShape": { "type": "alias", "value": { - "type": "primitive", - "value": { - "type": "string" + "type": "optional", + "shape": { + "type": "alias", + "value": { + "type": "list", + "itemShape": { + "type": "alias", + "value": { + "type": "id", + "id": "Document" + } + } + } } } - } - } - ] - }, - "name": "Unprocessable Entity", - "examples": [ - { - "responseBody": { - "type": "json", - "value": { - "data": "string" - } - } - } - ] - }, - { - "statusCode": 429, - "shape": { - "type": "object", - "extends": [], - "properties": [ + }, + "description": "A list of relevant documents that the model can cite to generate a more accurate reply. Each document is either a string or document object with content and metadata.\n" + }, { - "key": "data", + "key": "citation_options", "valueShape": { "type": "alias", "value": { - "type": "primitive", - "value": { - "type": "string" + "type": "optional", + "shape": { + "type": "alias", + "value": { + "type": "id", + "id": "CitationOptions" + } } } } - } + }, + { + "key": "response_format", + "valueShape": { + "type": "alias", + "value": { + "type": "optional", + "shape": { + "type": "alias", + "value": { + "type": "id", + "id": "ResponseFormatV2" + } + } + } + } + }, + { + "key": "safety_mode", + "valueShape": { + "type": "alias", + "value": { + "type": "optional", + "shape": { + "type": "enum", + "values": [ + { + "value": "CONTEXTUAL" + }, + { + "value": "STRICT" + }, + { + "value": "OFF" + } + ] + } + } + }, + "description": "Used to select the [safety instruction](https://docs.cohere.com/v2/docs/safety-modes) inserted into the prompt. Defaults to `CONTEXTUAL`.\nWhen `OFF` is specified, the safety instruction will be omitted.\n\nSafety modes are not yet configurable in combination with `tools`, `tool_results` and `documents` parameters.\n\n**Note**: This parameter is only compatible with models [Command R 08-2024](https://docs.cohere.com/v2/docs/command-r#august-2024-release), [Command R+ 08-2024](https://docs.cohere.com/v2/docs/command-r-plus#august-2024-release) and newer.\n" + }, + { + "key": "max_tokens", + "valueShape": { + "type": "alias", + "value": { + "type": "optional", + "shape": { + "type": "alias", + "value": { + "type": "primitive", + "value": { + "type": "integer" + } + } + } + } + }, + "description": "The maximum number of tokens the model will generate as part of the response.\n\n**Note**: Setting a low value may result in incomplete generations.\n" + }, + { + "key": "stop_sequences", + "valueShape": { + "type": "alias", + "value": { + "type": "optional", + "shape": { + "type": "alias", + "value": { + "type": "list", + "itemShape": { + "type": "alias", + "value": { + "type": "primitive", + "value": { + "type": "string" + } + } + } + } + } + } + }, + "description": "A list of up to 5 strings that the model will use to stop generation. If the model generates a string that matches any of the strings in the list, it will stop generating tokens and return the generated text up to that point not including the stop sequence.\n" + }, + { + "key": "temperature", + "valueShape": { + "type": "alias", + "value": { + "type": "optional", + "shape": { + "type": "alias", + "value": { + "type": "primitive", + "value": { + "type": "double", + "minimum": 0, + "maximum": 1 + } + } + } + } + }, + "description": "Defaults to `0.3`.\n\nA non-negative float that tunes the degree of randomness in generation. Lower temperatures mean less random generations, and higher temperatures mean more random generations.\n\nRandomness can be further maximized by increasing the value of the `p` parameter.\n" + }, + { + "key": "seed", + "valueShape": { + "type": "alias", + "value": { + "type": "optional", + "shape": { + "type": "alias", + "value": { + "type": "primitive", + "value": { + "type": "integer", + "minimum": 0, + "maximum": 18446744073709552000 + } + } + } + } + }, + "description": "If specified, the backend will make a best effort to sample tokens\ndeterministically, such that repeated requests with the same\nseed and parameters should return the same result. However,\ndeterminism cannot be totally guaranteed.\n" + }, + { + "key": "frequency_penalty", + "valueShape": { + "type": "alias", + "value": { + "type": "optional", + "shape": { + "type": "alias", + "value": { + "type": "primitive", + "value": { + "type": "double" + } + } + } + } + }, + "description": "Defaults to `0.0`, min value of `0.0`, max value of `1.0`.\nUsed to reduce repetitiveness of generated tokens. The higher the value, the stronger a penalty is applied to previously present tokens, proportional to how many times they have already appeared in the prompt or prior generation.\n" + }, + { + "key": "presence_penalty", + "valueShape": { + "type": "alias", + "value": { + "type": "optional", + "shape": { + "type": "alias", + "value": { + "type": "primitive", + "value": { + "type": "double" + } + } + } + } + }, + "description": "Defaults to `0.0`, min value of `0.0`, max value of `1.0`.\nUsed to reduce repetitiveness of generated tokens. Similar to `frequency_penalty`, except that this penalty is applied equally to all tokens that have already appeared, regardless of their exact frequencies.\n" + }, + { + "key": "k", + "valueShape": { + "type": "alias", + "value": { + "type": "optional", + "shape": { + "type": "alias", + "value": { + "type": "primitive", + "value": { + "type": "double", + "minimum": 0, + "maximum": 500, + "default": 0 + } + } + } + } + }, + "description": "Ensures that only the top `k` most likely tokens are considered for generation at each step. When `k` is set to `0`, k-sampling is disabled.\nDefaults to `0`, min value of `0`, max value of `500`.\n" + }, + { + "key": "p", + "valueShape": { + "type": "alias", + "value": { + "type": "optional", + "shape": { + "type": "alias", + "value": { + "type": "primitive", + "value": { + "type": "double", + "minimum": 0.01, + "maximum": 0.99, + "default": 0.75 + } + } + } + } + }, + "description": "Ensures that only the most likely tokens, with total probability mass of `p`, are considered for generation at each step. If both `k` and `p` are enabled, `p` acts after `k`.\nDefaults to `0.75`. min value of `0.01`, max value of `0.99`.\n" + }, + { + "key": "logprobs", + "valueShape": { + "type": "alias", + "value": { + "type": "optional", + "shape": { + "type": "alias", + "value": { + "type": "primitive", + "value": { + "type": "boolean" + } + } + } + } + }, + "description": "Defaults to `false`. When set to `true`, the log probabilities of the generated tokens will be included in the response.\n" + } + ] + } + } + ], + "responses": [ + { + "statusCode": 200, + "body": { + "type": "alias", + "value": { + "type": "id", + "id": "ChatResponse" + } + }, + "description": "OK" + }, + { + "statusCode": 200, + "body": { + "type": "alias", + "value": { + "type": "id", + "id": "StreamedChatResponseV2" + } + }, + "description": "OK" + } + ], + "errors": [ + { + "statusCode": 400, + "shape": { + "type": "object", + "extends": [], + "properties": [ + { + "key": "data", + "valueShape": { + "type": "alias", + "value": { + "type": "primitive", + "value": { + "type": "string" + } + } + } + } + ] + }, + "name": "Bad Request", + "examples": [ + { + "responseBody": { + "type": "json", + "value": { + "data": "string" + } + } + } + ] + }, + { + "statusCode": 401, + "shape": { + "type": "object", + "extends": [], + "properties": [ + { + "key": "data", + "valueShape": { + "type": "alias", + "value": { + "type": "primitive", + "value": { + "type": "string" + } + } + } + } + ] + }, + "name": "Unauthorized", + "examples": [ + { + "responseBody": { + "type": "json", + "value": { + "data": "string" + } + } + } + ] + }, + { + "statusCode": 403, + "shape": { + "type": "object", + "extends": [], + "properties": [ + { + "key": "data", + "valueShape": { + "type": "alias", + "value": { + "type": "primitive", + "value": { + "type": "string" + } + } + } + } + ] + }, + "name": "Forbidden", + "examples": [ + { + "responseBody": { + "type": "json", + "value": { + "data": "string" + } + } + } + ] + }, + { + "statusCode": 404, + "shape": { + "type": "object", + "extends": [], + "properties": [ + { + "key": "data", + "valueShape": { + "type": "alias", + "value": { + "type": "primitive", + "value": { + "type": "string" + } + } + } + } + ] + }, + "name": "Not Found", + "examples": [ + { + "responseBody": { + "type": "json", + "value": { + "data": "string" + } + } + } + ] + }, + { + "statusCode": 422, + "shape": { + "type": "object", + "extends": [], + "properties": [ + { + "key": "data", + "valueShape": { + "type": "alias", + "value": { + "type": "primitive", + "value": { + "type": "string" + } + } + } + } + ] + }, + "name": "Unprocessable Entity", + "examples": [ + { + "responseBody": { + "type": "json", + "value": { + "data": "string" + } + } + } + ] + }, + { + "statusCode": 429, + "shape": { + "type": "object", + "extends": [], + "properties": [ + { + "key": "data", + "valueShape": { + "type": "alias", + "value": { + "type": "primitive", + "value": { + "type": "string" + } + } + } + } ] }, "name": "Too Many Requests", @@ -2942,6 +3356,36 @@ } ] } + }, + { + "path": "/v2/chat", + "responseStatusCode": 200, + "responseBody": { + "type": "json", + "value": { + "id": "string", + "finish_reason": "string", + "message": { + "role": "string" + } + } + }, + "snippets": {} + }, + { + "path": "/v2/chat", + "responseStatusCode": 200, + "responseBody": { + "type": "json", + "value": { + "id": "string", + "finish_reason": "string", + "message": { + "role": "string" + } + } + }, + "snippets": {} } ] }, @@ -3388,6 +3832,17 @@ } }, "description": "OK" + }, + { + "statusCode": 200, + "body": { + "type": "alias", + "value": { + "type": "id", + "id": "GenerateStreamedResponse" + } + }, + "description": "OK" } ], "errors": [ @@ -3939,6 +4394,51 @@ } }, "snippets": {} + }, + { + "path": "/v1/generate", + "responseStatusCode": 200, + "responseBody": { + "type": "json", + "value": { + "text": "string", + "index": 0, + "is_finished": false, + "event_type": "string" + } + }, + "snippets": {} + }, + { + "path": "/v1/generate", + "responseStatusCode": 200, + "responseBody": { + "type": "json", + "value": { + "id": "string", + "generations": [ + { + "text": "string", + "id": "string" + } + ] + } + }, + "snippets": {} + }, + { + "path": "/v1/generate", + "responseStatusCode": 200, + "responseBody": { + "type": "json", + "value": { + "text": "string", + "index": 0, + "is_finished": false, + "event_type": "string" + } + }, + "snippets": {} } ] }, @@ -13820,8 +14320,156 @@ "value": { "type": "list", "itemShape": { - "type": "undiscriminatedUnion", - "variants": [] + "type": "alias", + "value": { + "type": "primitive", + "value": { + "type": "string" + } + } + } + } + }, + "description": "A list of document objects or strings to rerank.\nIf a document is provided the text fields is required and all other fields will be preserved in the response.\n\nThe total max chunks (length of documents * max_chunks_per_doc) must be less than 10000.\n\nWe recommend a maximum of 1,000 documents for optimal endpoint performance." + }, + { + "key": "top_n", + "valueShape": { + "type": "alias", + "value": { + "type": "optional", + "shape": { + "type": "alias", + "value": { + "type": "primitive", + "value": { + "type": "integer", + "minimum": 1 + } + } + } + } + }, + "description": "The number of most relevant documents or indices to return, defaults to the length of the documents" + }, + { + "key": "rank_fields", + "valueShape": { + "type": "alias", + "value": { + "type": "optional", + "shape": { + "type": "alias", + "value": { + "type": "list", + "itemShape": { + "type": "alias", + "value": { + "type": "primitive", + "value": { + "type": "string" + } + } + } + } + } + } + }, + "description": "If a JSON object is provided, you can specify which keys you would like to have considered for reranking. The model will rerank based on order of the fields passed in (i.e. rank_fields=['title','author','text'] will rerank using the values in title, author, text sequentially. If the length of title, author, and text exceeds the context length of the model, the chunking will not re-consider earlier fields). If not provided, the model will use the default text field for ranking." + }, + { + "key": "return_documents", + "valueShape": { + "type": "alias", + "value": { + "type": "optional", + "shape": { + "type": "alias", + "value": { + "type": "primitive", + "value": { + "type": "boolean", + "default": false + } + } + } + } + }, + "description": "- If false, returns results without the doc text - the api will return a list of {index, relevance score} where index is inferred from the list passed into the request.\n- If true, returns results with the doc text passed in - the api will return an ordered list of {index, text, relevance score} where index + text refers to the list passed into the request." + }, + { + "key": "max_chunks_per_doc", + "valueShape": { + "type": "alias", + "value": { + "type": "optional", + "shape": { + "type": "alias", + "value": { + "type": "primitive", + "value": { + "type": "integer", + "default": 10 + } + } + } + } + }, + "description": "The maximum number of chunks to produce internally from a document" + } + ] + } + }, + { + "contentType": "application/json", + "body": { + "type": "object", + "extends": [], + "properties": [ + { + "key": "model", + "valueShape": { + "type": "alias", + "value": { + "type": "optional", + "shape": { + "type": "alias", + "value": { + "type": "primitive", + "value": { + "type": "string" + } + } + } + } + }, + "description": "The identifier of the model to use, one of : `rerank-english-v3.0`, `rerank-multilingual-v3.0`, `rerank-english-v2.0`, `rerank-multilingual-v2.0`" + }, + { + "key": "query", + "valueShape": { + "type": "alias", + "value": { + "type": "primitive", + "value": { + "type": "string" + } + } + }, + "description": "The search query" + }, + { + "key": "documents", + "valueShape": { + "type": "alias", + "value": { + "type": "list", + "itemShape": { + "type": "alias", + "value": { + "type": "id", + "id": "RerankDocument" + } } } }, @@ -15614,15 +16262,6 @@ }, "description": "An array containing the confidence scores of all the predictions in the same order" }, - { - "key": "labels", - "valueShape": { - "type": "object", - "extends": [], - "properties": [] - }, - "description": "A map containing each label and its confidence score according to the classifier. All the confidence scores add up to 1 for single-label classification. For multi-label classification the label confidences are independent of each other, so they don't have to sum up to 1." - }, { "key": "classification_type", "valueShape": { @@ -28634,21 +29273,6 @@ } }, "description": "The description of what the tool does, the model uses the description to choose when and how to call the function.\n" - }, - { - "key": "parameter_definitions", - "valueShape": { - "type": "alias", - "value": { - "type": "optional", - "shape": { - "type": "object", - "extends": [], - "properties": [] - } - } - }, - "description": "The input parameters of the tool. Accepts a dictionary where the key is the name of the parameter and the value is the parameter spec. Valid parameter names contain only the characters `a-z`, `A-Z`, `0-9`, `_` and must not begin with a digit.\n```\n{\n \"my_param\": {\n \"description\": ,\n \"type\": , // any python data type, such as 'str', 'bool'\n \"required\": \n }\n}\n```\n" } ] } @@ -28730,6 +29354,7 @@ "variants": [ { "discriminantValue": "text", + "displayName": "Text Response", "type": "object", "extends": [], "properties": [ @@ -28747,6 +29372,7 @@ }, { "discriminantValue": "json_object", + "displayName": "JSON Object Response", "type": "object", "extends": [], "properties": [ @@ -30240,8 +30866,13 @@ { "key": "content", "valueShape": { - "type": "undiscriminatedUnion", - "variants": [] + "type": "alias", + "value": { + "type": "primitive", + "value": { + "type": "string" + } + } }, "description": "The content of the message. This can be a string or a list of content blocks.\nIf a string is provided, it will be treated as a text content block.\n" } @@ -30384,6 +31015,7 @@ "variants": [ { "discriminantValue": "tool", + "displayName": "Tool Output", "type": "object", "extends": [], "properties": [ @@ -30573,8 +31205,13 @@ "value": { "type": "optional", "shape": { - "type": "undiscriminatedUnion", - "variants": [] + "type": "alias", + "value": { + "type": "primitive", + "value": { + "type": "string" + } + } } } } @@ -30625,8 +31262,13 @@ { "key": "content", "valueShape": { - "type": "undiscriminatedUnion", - "variants": [] + "type": "alias", + "value": { + "type": "primitive", + "value": { + "type": "string" + } + } } } ] @@ -30811,8 +31453,13 @@ { "key": "content", "valueShape": { - "type": "undiscriminatedUnion", - "variants": [] + "type": "alias", + "value": { + "type": "primitive", + "value": { + "type": "string" + } + } }, "description": "Outputs from a tool. The content should formatted as a JSON object string, or a list of tool content blocks" } @@ -30827,8 +31474,79 @@ "discriminant": "role", "variants": [ { - "discriminantValue": "user", - "description": "A message from the user.", + "discriminantValue": "user", + "displayName": "User Message", + "description": "A message from the user.", + "type": "object", + "extends": [], + "properties": [ + { + "key": "role", + "valueShape": { + "type": "enum", + "values": [ + { + "value": "user" + } + ] + } + }, + { + "key": "content", + "valueShape": { + "type": "alias", + "value": { + "type": "primitive", + "value": { + "type": "string" + } + } + }, + "description": "The content of the message. This can be a string or a list of content blocks.\nIf a string is provided, it will be treated as a text content block.\n" + } + ] + }, + { + "discriminantValue": "user", + "displayName": "User Message", + "description": "A message from the user.", + "type": "object", + "extends": [], + "properties": [ + { + "key": "role", + "valueShape": { + "type": "enum", + "values": [ + { + "value": "user" + } + ] + } + }, + { + "key": "content", + "valueShape": { + "type": "alias", + "value": { + "type": "list", + "itemShape": { + "type": "alias", + "value": { + "type": "id", + "id": "Content" + } + } + } + }, + "description": "The content of the message. This can be a string or a list of content blocks.\nIf a string is provided, it will be treated as a text content block.\n" + } + ] + }, + { + "discriminantValue": "assistant", + "displayName": "Assistant Message", + "description": "A message from the assistant role can contain text and tool call information.", "type": "object", "extends": [], "properties": [ @@ -30838,23 +31556,97 @@ "type": "enum", "values": [ { - "value": "user" + "value": "assistant" } ] } }, { - "key": "content", + "key": "tool_calls", + "valueShape": { + "type": "alias", + "value": { + "type": "optional", + "shape": { + "type": "alias", + "value": { + "type": "list", + "itemShape": { + "type": "alias", + "value": { + "type": "id", + "id": "ToolCallV2" + } + } + } + } + } + } + }, + { + "key": "tool_plan", "valueShape": { - "type": "undiscriminatedUnion", - "variants": [] + "type": "alias", + "value": { + "type": "optional", + "shape": { + "type": "alias", + "value": { + "type": "primitive", + "value": { + "type": "string" + } + } + } + } }, - "description": "The content of the message. This can be a string or a list of content blocks.\nIf a string is provided, it will be treated as a text content block.\n" + "description": "A chain-of-thought style reflection and plan that the model generates when working with Tools." + }, + { + "key": "content", + "valueShape": { + "type": "alias", + "value": { + "type": "optional", + "shape": { + "type": "alias", + "value": { + "type": "primitive", + "value": { + "type": "string" + } + } + } + } + } + }, + { + "key": "citations", + "valueShape": { + "type": "alias", + "value": { + "type": "optional", + "shape": { + "type": "alias", + "value": { + "type": "list", + "itemShape": { + "type": "alias", + "value": { + "type": "id", + "id": "Citation" + } + } + } + } + } + } } ] }, { "discriminantValue": "assistant", + "displayName": "Assistant Message", "description": "A message from the assistant role can contain text and tool call information.", "type": "object", "extends": [], @@ -30918,8 +31710,47 @@ "value": { "type": "optional", "shape": { - "type": "undiscriminatedUnion", - "variants": [] + "type": "alias", + "value": { + "type": "list", + "itemShape": { + "type": "discriminatedUnion", + "discriminant": "type", + "variants": [ + { + "discriminantValue": "text", + "description": "Text content of the message.", + "type": "object", + "extends": [], + "properties": [ + { + "key": "type", + "valueShape": { + "type": "enum", + "values": [ + { + "value": "text" + } + ] + } + }, + { + "key": "text", + "valueShape": { + "type": "alias", + "value": { + "type": "primitive", + "value": { + "type": "string" + } + } + } + } + ] + } + ] + } + } } } } @@ -30950,6 +31781,39 @@ }, { "discriminantValue": "system", + "displayName": "System Message", + "description": "A message from the system.", + "type": "object", + "extends": [], + "properties": [ + { + "key": "role", + "valueShape": { + "type": "enum", + "values": [ + { + "value": "system" + } + ] + } + }, + { + "key": "content", + "valueShape": { + "type": "alias", + "value": { + "type": "primitive", + "value": { + "type": "string" + } + } + } + } + ] + }, + { + "discriminantValue": "system", + "displayName": "System Message", "description": "A message from the system.", "type": "object", "extends": [], @@ -30968,14 +31832,100 @@ { "key": "content", "valueShape": { - "type": "undiscriminatedUnion", - "variants": [] + "type": "alias", + "value": { + "type": "list", + "itemShape": { + "type": "discriminatedUnion", + "discriminant": "type", + "variants": [ + { + "discriminantValue": "text", + "description": "Text content of the message.", + "type": "object", + "extends": [], + "properties": [ + { + "key": "type", + "valueShape": { + "type": "enum", + "values": [ + { + "value": "text" + } + ] + } + }, + { + "key": "text", + "valueShape": { + "type": "alias", + "value": { + "type": "primitive", + "value": { + "type": "string" + } + } + } + } + ] + } + ] + } + } + } + } + ] + }, + { + "discriminantValue": "tool", + "displayName": "Tool Message", + "description": "A message with Tool outputs.", + "type": "object", + "extends": [], + "properties": [ + { + "key": "role", + "valueShape": { + "type": "enum", + "values": [ + { + "value": "tool" + } + ] } + }, + { + "key": "tool_call_id", + "valueShape": { + "type": "alias", + "value": { + "type": "primitive", + "value": { + "type": "string" + } + } + }, + "description": "The id of the associated tool call that has provided the given content" + }, + { + "key": "content", + "valueShape": { + "type": "alias", + "value": { + "type": "primitive", + "value": { + "type": "string" + } + } + }, + "description": "Outputs from a tool. The content should formatted as a JSON object string, or a list of tool content blocks" } ] }, { "discriminantValue": "tool", + "displayName": "Tool Message", "description": "A message with Tool outputs.", "type": "object", "extends": [], @@ -31007,8 +31957,17 @@ { "key": "content", "valueShape": { - "type": "undiscriminatedUnion", - "variants": [] + "type": "alias", + "value": { + "type": "list", + "itemShape": { + "type": "alias", + "value": { + "type": "id", + "id": "ToolContent" + } + } + } }, "description": "Outputs from a tool. The content should formatted as a JSON object string, or a list of tool content blocks" } diff --git a/packages/parsers/src/__test__/__snapshots__/openapi/deeptune.json b/packages/parsers/src/__test__/__snapshots__/openapi/deeptune.json index a80e38f9ee..5af287268a 100644 --- a/packages/parsers/src/__test__/__snapshots__/openapi/deeptune.json +++ b/packages/parsers/src/__test__/__snapshots__/openapi/deeptune.json @@ -47,9 +47,33 @@ } } ], - "responses": [], + "responses": [ + { + "statusCode": 200, + "body": { + "type": "alias", + "value": { + "type": "id", + "id": "TextToSpeechResponse" + } + }, + "description": "Successful response" + } + ], "errors": [], - "examples": [] + "examples": [ + { + "path": "/v1/text-to-speech", + "responseStatusCode": 200, + "responseBody": { + "type": "json", + "value": { + "file": "string" + } + }, + "snippets": {} + } + ] }, "endpoint_textToSpeech.generateFromPrompt": { "id": "endpoint_textToSpeech.generateFromPrompt", @@ -105,9 +129,33 @@ } } ], - "responses": [], + "responses": [ + { + "statusCode": 200, + "body": { + "type": "alias", + "value": { + "type": "id", + "id": "TextToSpeechFromPromptResponse" + } + }, + "description": "Successful response" + } + ], "errors": [], - "examples": [] + "examples": [ + { + "path": "/v1/text-to-speech/from-prompt", + "responseStatusCode": 200, + "responseBody": { + "type": "json", + "value": { + "file": "string" + } + }, + "snippets": {} + } + ] }, "endpoint_voices.list": { "id": "endpoint_voices.list", diff --git a/packages/parsers/src/__test__/__snapshots__/openapi/petstore.json b/packages/parsers/src/__test__/__snapshots__/openapi/petstore.json index 01766baf03..f01907a6e1 100644 --- a/packages/parsers/src/__test__/__snapshots__/openapi/petstore.json +++ b/packages/parsers/src/__test__/__snapshots__/openapi/petstore.json @@ -51,6 +51,17 @@ } ], "responses": [ + { + "statusCode": 200, + "body": { + "type": "alias", + "value": { + "type": "id", + "id": "Pet" + } + }, + "description": "Successful operation" + }, { "statusCode": 200, "body": { @@ -65,6 +76,48 @@ ], "errors": [], "examples": [ + { + "path": "/pet", + "responseStatusCode": 200, + "responseBody": { + "type": "json", + "value": { + "name": "doggie", + "photoUrls": [ + "string" + ] + } + }, + "snippets": {} + }, + { + "path": "/pet", + "responseStatusCode": 200, + "responseBody": { + "type": "json", + "value": { + "name": "doggie", + "photoUrls": [ + "string" + ] + } + }, + "snippets": {} + }, + { + "path": "/pet", + "responseStatusCode": 200, + "responseBody": { + "type": "json", + "value": { + "name": "doggie", + "photoUrls": [ + "string" + ] + } + }, + "snippets": {} + }, { "path": "/pet", "responseStatusCode": 200, @@ -131,6 +184,17 @@ } ], "responses": [ + { + "statusCode": 200, + "body": { + "type": "alias", + "value": { + "type": "id", + "id": "Pet" + } + }, + "description": "Successful operation" + }, { "statusCode": 200, "body": { @@ -145,6 +209,48 @@ ], "errors": [], "examples": [ + { + "path": "/pet", + "responseStatusCode": 200, + "responseBody": { + "type": "json", + "value": { + "name": "doggie", + "photoUrls": [ + "string" + ] + } + }, + "snippets": {} + }, + { + "path": "/pet", + "responseStatusCode": 200, + "responseBody": { + "type": "json", + "value": { + "name": "doggie", + "photoUrls": [ + "string" + ] + } + }, + "snippets": {} + }, + { + "path": "/pet", + "responseStatusCode": 200, + "responseBody": { + "type": "json", + "value": { + "name": "doggie", + "photoUrls": [ + "string" + ] + } + }, + "snippets": {} + }, { "path": "/pet", "responseStatusCode": 200, @@ -212,6 +318,17 @@ } ], "responses": [ + { + "statusCode": null, + "body": { + "type": "alias", + "value": { + "type": "id", + "id": "Pet" + } + }, + "description": "The pet" + }, { "statusCode": null, "body": { @@ -250,6 +367,56 @@ } ] }, + { + "statusCode": 400, + "shape": { + "type": "alias", + "value": { + "type": "id", + "id": "Pet" + } + }, + "description": "Invalid ID supplied", + "name": "Bad Request", + "examples": [ + { + "responseBody": { + "type": "json", + "value": { + "name": "doggie", + "photoUrls": [ + "string" + ] + } + } + } + ] + }, + { + "statusCode": 404, + "shape": { + "type": "alias", + "value": { + "type": "id", + "id": "Pet" + } + }, + "description": "Pet not found", + "name": "Not Found", + "examples": [ + { + "responseBody": { + "type": "json", + "value": { + "name": "doggie", + "photoUrls": [ + "string" + ] + } + } + } + ] + }, { "statusCode": 404, "shape": { @@ -277,6 +444,48 @@ } ], "examples": [ + { + "path": "/pet/{petId}", + "responseStatusCode": null, + "responseBody": { + "type": "json", + "value": { + "name": "doggie", + "photoUrls": [ + "string" + ] + } + }, + "snippets": {} + }, + { + "path": "/pet/{petId}", + "responseStatusCode": null, + "responseBody": { + "type": "json", + "value": { + "name": "doggie", + "photoUrls": [ + "string" + ] + } + }, + "snippets": {} + }, + { + "path": "/pet/{petId}", + "responseStatusCode": null, + "responseBody": { + "type": "json", + "value": { + "name": "doggie", + "photoUrls": [ + "string" + ] + } + }, + "snippets": {} + }, { "path": "/pet/{petId}", "responseStatusCode": null, diff --git a/packages/parsers/src/__test__/__snapshots__/openapi/uploadcare.json b/packages/parsers/src/__test__/__snapshots__/openapi/uploadcare.json index ee9c3ca3c7..6f663fc341 100644 --- a/packages/parsers/src/__test__/__snapshots__/openapi/uploadcare.json +++ b/packages/parsers/src/__test__/__snapshots__/openapi/uploadcare.json @@ -10656,6 +10656,28 @@ } } ] + }, + { + "statusCode": 503, + "shape": { + "type": "alias", + "value": { + "type": "primitive", + "value": { + "type": "string" + } + } + }, + "description": "Conversion service error.", + "name": "Service Unavailable", + "examples": [ + { + "responseBody": { + "type": "json", + "value": "string" + } + } + ] } ], "examples": [ @@ -11382,6 +11404,28 @@ } } ] + }, + { + "statusCode": 503, + "shape": { + "type": "alias", + "value": { + "type": "primitive", + "value": { + "type": "string" + } + } + }, + "description": "Conversion service error.", + "name": "Service Unavailable", + "examples": [ + { + "responseBody": { + "type": "json", + "value": "string" + } + } + ] } ], "examples": [ @@ -12175,6 +12219,28 @@ } } ] + }, + { + "statusCode": 503, + "shape": { + "type": "alias", + "value": { + "type": "primitive", + "value": { + "type": "string" + } + } + }, + "description": "Conversion service error.", + "name": "Service Unavailable", + "examples": [ + { + "responseBody": { + "type": "json", + "value": "string" + } + } + ] } ], "examples": [ diff --git a/packages/parsers/src/openapi/3.1/paths/ExampleObjectConverter.node.ts b/packages/parsers/src/openapi/3.1/paths/ExampleObjectConverter.node.ts index 490c03c3b6..5c9f1f863e 100644 --- a/packages/parsers/src/openapi/3.1/paths/ExampleObjectConverter.node.ts +++ b/packages/parsers/src/openapi/3.1/paths/ExampleObjectConverter.node.ts @@ -199,7 +199,7 @@ export class ExampleObjectConverterNode extends BaseOpenApiV3_1ConverterNode< break; default: new UnreachableCaseError(this.responseBody.contentType); - return undefined; + return; } } } @@ -383,6 +383,11 @@ export class ExampleObjectConverterNode extends BaseOpenApiV3_1ConverterNode< }; break; case undefined: + responseBody = { + type: "json", + value: + this.resolvedResponseInput?.value ?? this.resolvedResponseInput, + }; break; default: new UnreachableCaseError(this.responseBody.contentType); diff --git a/packages/parsers/src/openapi/3.1/paths/OperationObjectConverter.node.ts b/packages/parsers/src/openapi/3.1/paths/OperationObjectConverter.node.ts index 7c524cdffa..e76c7f0bd9 100644 --- a/packages/parsers/src/openapi/3.1/paths/OperationObjectConverter.node.ts +++ b/packages/parsers/src/openapi/3.1/paths/OperationObjectConverter.node.ts @@ -340,7 +340,7 @@ export class OperationObjectConverterNode extends BaseOpenApiV3_1ConverterNode< path: this.convertPathToPathParts()?.map((part) => part.value.toString()) ?? [], - headers: convertOperationObjectProperties(this.requestHeaders), + headers: convertOperationObjectProperties(this.requestHeaders)?.flat(), // TODO: figure out what this looks like to be able to parse payload: undefined, examples: undefined, @@ -394,9 +394,15 @@ export class OperationObjectConverterNode extends BaseOpenApiV3_1ConverterNode< auth: authIds?.map((id) => FernRegistry.api.latest.AuthSchemeId(id)), defaultEnvironment: environments?.[0]?.id, environments, - pathParameters: convertOperationObjectProperties(this.pathParameters), - queryParameters: convertOperationObjectProperties(this.queryParameters), - requestHeaders: convertOperationObjectProperties(this.requestHeaders), + pathParameters: convertOperationObjectProperties( + this.pathParameters + )?.flat(), + queryParameters: convertOperationObjectProperties( + this.queryParameters + )?.flat(), + requestHeaders: convertOperationObjectProperties( + this.requestHeaders + )?.flat(), responseHeaders: responses?.[0]?.headers, // TODO: revisit fdr shape to suport multiple requests requests: this.requests?.convert(), diff --git a/packages/parsers/src/openapi/3.1/paths/parameters/ParameterBaseObjectConverter.node.ts b/packages/parsers/src/openapi/3.1/paths/parameters/ParameterBaseObjectConverter.node.ts index 74eb08d57e..369a794fab 100644 --- a/packages/parsers/src/openapi/3.1/paths/parameters/ParameterBaseObjectConverter.node.ts +++ b/packages/parsers/src/openapi/3.1/paths/parameters/ParameterBaseObjectConverter.node.ts @@ -12,7 +12,7 @@ import { SchemaConverterNode } from "../../schemas/SchemaConverter.node"; export function convertOperationObjectProperties( properties: Record | undefined -): FernRegistry.api.latest.ObjectProperty[] | undefined { +): FernRegistry.api.latest.ObjectProperty[][] | undefined { if (properties == null) { return undefined; } @@ -27,7 +27,7 @@ export function convertOperationObjectProperties( export class ParameterBaseObjectConverterNode extends BaseOpenApiV3_1ConverterNode< OpenAPIV3_1.ParameterBaseObject | OpenAPIV3_1.ReferenceObject, - FernRegistry.api.latest.TypeShape + FernRegistry.api.latest.TypeShape | FernRegistry.api.latest.TypeShape[] > { availability: AvailabilityConverterNode | undefined; required: boolean | undefined; @@ -78,7 +78,10 @@ export class ParameterBaseObjectConverterNode extends BaseOpenApiV3_1ConverterNo }); } - convert(): FernRegistry.api.latest.TypeShape | undefined { + convert(): + | FernRegistry.api.latest.TypeShape + | FernRegistry.api.latest.TypeShape[] + | undefined { return this.schema?.convert(); } } diff --git a/packages/parsers/src/openapi/3.1/paths/request/RequestBodyObjectConverter.node.ts b/packages/parsers/src/openapi/3.1/paths/request/RequestBodyObjectConverter.node.ts index 7325018c7b..5281515aa0 100644 --- a/packages/parsers/src/openapi/3.1/paths/request/RequestBodyObjectConverter.node.ts +++ b/packages/parsers/src/openapi/3.1/paths/request/RequestBodyObjectConverter.node.ts @@ -63,18 +63,22 @@ export class RequestBodyObjectConverterNode extends BaseOpenApiV3_1ConverterNode convert(): FernRegistry.api.latest.HttpRequest[] { return Object.entries(this.requestBodiesByContentType ?? {}) - .map(([contentType, mediaTypeObject]) => { - const body = mediaTypeObject.convert(); + .flatMap(([contentType, mediaTypeObject]) => { + let maybeBodies = mediaTypeObject.convert(); - if (body == null) { + if (maybeBodies == null) { return undefined; } - return { + if (!Array.isArray(maybeBodies)) { + maybeBodies = [maybeBodies]; + } + + return maybeBodies.map((body) => ({ description: this.description, contentType, body, - }; + })); }) .filter(isNonNullish); } diff --git a/packages/parsers/src/openapi/3.1/paths/request/RequestMediaTypeObjectConverter.node.ts b/packages/parsers/src/openapi/3.1/paths/request/RequestMediaTypeObjectConverter.node.ts index 34e376ccd9..3f4bc53d79 100644 --- a/packages/parsers/src/openapi/3.1/paths/request/RequestMediaTypeObjectConverter.node.ts +++ b/packages/parsers/src/openapi/3.1/paths/request/RequestMediaTypeObjectConverter.node.ts @@ -26,7 +26,8 @@ export type RequestContentType = ConstArrayToType< export class RequestMediaTypeObjectConverterNode extends BaseOpenApiV3_1ConverterNode< OpenAPIV3_1.MediaTypeObject, - FernRegistry.api.latest.HttpRequestBodyShape + | FernRegistry.api.latest.HttpRequestBodyShape + | FernRegistry.api.latest.HttpRequestBodyShape[] > { description: string | undefined; @@ -146,7 +147,10 @@ export class RequestMediaTypeObjectConverterNode extends BaseOpenApiV3_1Converte } } - convert(): FernRegistry.api.latest.HttpRequestBodyShape | undefined { + convert(): + | FernRegistry.api.latest.HttpRequestBodyShape + | FernRegistry.api.latest.HttpRequestBodyShape[] + | undefined { switch (this.contentType) { case "json": return this.schema?.convert(); @@ -156,55 +160,79 @@ export class RequestMediaTypeObjectConverterNode extends BaseOpenApiV3_1Converte isOptional: this.isOptional ?? false, contentType: this.contentType, }; - case "form-data": - return { - type: "formData", - fields: Object.entries(this.fields ?? {}) + case "form-data": { + const possibleFields: FernRegistry.api.latest.FormDataField[][] = + Object.entries(this.fields ?? {}) .map(([key, field]) => { switch (field.multipartType) { case "file": - return { - type: field.multipartType, - key: FernRegistry.PropertyKey(key), - isOptional: this.requiredFields?.includes(key) == null, - contentType: field.contentType, - description: field.description, - availability: field.availability?.convert(), - }; + return [ + { + type: field.multipartType, + key: FernRegistry.PropertyKey(key), + isOptional: this.requiredFields?.includes(key) == null, + contentType: field.contentType, + description: field.description, + availability: field.availability?.convert(), + }, + ]; case "files": - return { - type: field.multipartType, - key: FernRegistry.PropertyKey(key), - isOptional: this.requiredFields?.includes(key) == null, - contentType: field.contentType, - description: field.description, - availability: field.availability?.convert(), - }; + return [ + { + type: field.multipartType, + key: FernRegistry.PropertyKey(key), + isOptional: this.requiredFields?.includes(key) == null, + contentType: field.contentType, + description: field.description, + availability: field.availability?.convert(), + }, + ]; case "property": { - const valueShape = field.convert(); - if (valueShape == null) { - return undefined; + let maybeValueShapes = field.convert(); + const type = field.multipartType; + + if ( + !Array.isArray(maybeValueShapes) && + maybeValueShapes != null + ) { + maybeValueShapes = [maybeValueShapes]; } - return { - type: field.multipartType, - key: FernRegistry.PropertyKey(key), - contentType: field.contentType, - valueShape, - description: field.description, - availability: field.availability?.convert(), - }; + return maybeValueShapes?.map((valueShape) => { + return { + type, + key: FernRegistry.PropertyKey(key), + contentType: field.contentType, + valueShape, + description: field.description, + availability: field.availability?.convert(), + }; + }); } case undefined: - return undefined; + return []; default: new UnreachableCaseError(field.multipartType); - return undefined; + return []; } }) - .filter(isNonNullish), + .filter(isNonNullish); + const fieldPermutations = possibleFields.reduce< + FernRegistry.api.latest.FormDataField[][] + >( + (acc, curr) => { + return acc.flatMap((a) => + curr.length > 0 ? curr.map((b) => [...a, b]) : [[...a]] + ); + }, + [[]] + ); + return fieldPermutations.map((fields) => ({ + type: "formData", + fields, availability: this.availability?.convert(), description: this.description, - }; + })); + } case undefined: return this.schema?.convert(); default: diff --git a/packages/parsers/src/openapi/3.1/paths/response/ResponseMediaTypeObjectConverter.node.ts b/packages/parsers/src/openapi/3.1/paths/response/ResponseMediaTypeObjectConverter.node.ts index f9b71a9664..153fea4f70 100644 --- a/packages/parsers/src/openapi/3.1/paths/response/ResponseMediaTypeObjectConverter.node.ts +++ b/packages/parsers/src/openapi/3.1/paths/response/ResponseMediaTypeObjectConverter.node.ts @@ -1,3 +1,4 @@ +import { isNonNullish } from "@fern-api/ui-core-utils"; import { OpenAPIV3_1 } from "openapi-types"; import { UnreachableCaseError } from "ts-essentials"; import { FernRegistry } from "../../../../client/generated"; @@ -25,10 +26,12 @@ export type ResponseStreamingFormat = ConstArrayToType< export class ResponseMediaTypeObjectConverterNode extends BaseOpenApiV3_1ConverterNode< OpenAPIV3_1.MediaTypeObject, - FernRegistry.api.latest.HttpResponseBodyShape + | FernRegistry.api.latest.HttpResponseBodyShape + | FernRegistry.api.latest.HttpResponseBodyShape[] > { schema: SchemaConverterNode | undefined; contentType: ResponseContentType | undefined; + unsupportedContentType: string | undefined; contentSubtype: string | undefined; examples: ExampleObjectConverterNode[] | undefined; @@ -70,6 +73,22 @@ export class ResponseMediaTypeObjectConverterNode extends BaseOpenApiV3_1Convert this.input.schema, this.context.document )?.contentMediaType; + } else { + this.unsupportedContentType = contentType; + if (this.input.schema == null) { + this.context.errors.error({ + message: + "Expected schema for plain text response body. Received null", + path: this.accessPath, + }); + } else { + this.schema = new SchemaConverterNode({ + input: this.input.schema, + context: this.context, + accessPath: this.accessPath, + pathId: this.pathId, + }); + } } // TODO: This can all be moved upstream if there is a way to correlate the requests and responses (probably with ref-based config) @@ -101,7 +120,7 @@ export class ResponseMediaTypeObjectConverterNode extends BaseOpenApiV3_1Convert } ); - if (this.contentType != null) { + if (this.contentType != null || this.unsupportedContentType != null) { const resolvedSchema = resolveSchemaReference( this.input.schema, this.context.document @@ -201,19 +220,23 @@ export class ResponseMediaTypeObjectConverterNode extends BaseOpenApiV3_1Convert convertStreamingFormat(): | FernRegistry.api.latest.HttpResponseBodyShape + | FernRegistry.api.latest.HttpResponseBodyShape[] | undefined { switch (this.streamingFormat) { case "json": { - const shape = this.schema?.convert(); - if (shape == null) { + let maybeShapes = this.schema?.convert(); + if (maybeShapes == null) { return undefined; } - return { + if (!Array.isArray(maybeShapes)) { + maybeShapes = [maybeShapes]; + } + return maybeShapes.map((shape) => ({ type: "stream", // TODO: Parse terminator (probably extension) terminator: "[DATA]", shape, - }; + })); } case "sse": return { type: "streamingText" }; @@ -225,30 +248,70 @@ export class ResponseMediaTypeObjectConverterNode extends BaseOpenApiV3_1Convert } } - convert(): FernRegistry.api.latest.HttpResponseBodyShape | undefined { - switch (this.contentType) { - case "application/json": - if (this.streamingFormat == null) { - const shape = this.schema?.convert(); - if ( - shape == null || - (shape.type !== "object" && shape.type !== "alias") - ) { - return undefined; + convert(): + | FernRegistry.api.latest.HttpResponseBodyShape + | FernRegistry.api.latest.HttpResponseBodyShape[] + | undefined { + if (this.contentType != null) { + switch (this.contentType) { + case "application/json": + if (this.streamingFormat == null) { + let maybeShapes = this.schema?.convert(); + if (!Array.isArray(maybeShapes) && maybeShapes != null) { + maybeShapes = [maybeShapes]; + } + return maybeShapes + ?.map((shape) => { + if ( + shape == null || + (shape.type !== "object" && shape.type !== "alias") + ) { + return undefined; + } + return shape; + }) + .filter(isNonNullish); + } else { + return this.convertStreamingFormat(); } - return shape; - } else { + case "application/octet-stream": + return { type: "fileDownload", contentType: this.contentSubtype }; + case "text/event-stream": return this.convertStreamingFormat(); - } - case "application/octet-stream": - return { type: "fileDownload", contentType: this.contentSubtype }; - case "text/event-stream": - return this.convertStreamingFormat(); - case undefined: - return undefined; - default: - new UnreachableCaseError(this.contentType); + case undefined: + return undefined; + default: + new UnreachableCaseError(this.contentType); + return undefined; + } + } else if (this.unsupportedContentType != null) { + let maybeShapes = this.schema?.convert(); + if (maybeShapes == null) { return undefined; + } + if (!Array.isArray(maybeShapes)) { + maybeShapes = [maybeShapes]; + } + return maybeShapes + .map((shape) => { + const type = shape.type; + switch (type) { + case "alias": + return shape; + case "discriminatedUnion": + case "undiscriminatedUnion": + case "enum": + return undefined; + case "object": + return shape; + default: + new UnreachableCaseError(type); + return undefined; + } + }) + .filter(isNonNullish); + } else { + return undefined; } } } diff --git a/packages/parsers/src/openapi/3.1/paths/response/ResponseObjectConverter.node.ts b/packages/parsers/src/openapi/3.1/paths/response/ResponseObjectConverter.node.ts index 7dd9f75614..335fb4b33f 100644 --- a/packages/parsers/src/openapi/3.1/paths/response/ResponseObjectConverter.node.ts +++ b/packages/parsers/src/openapi/3.1/paths/response/ResponseObjectConverter.node.ts @@ -82,7 +82,7 @@ export class ResponseObjectConverterNode extends BaseOpenApiV3_1ConverterNode< convert(): FernRegistry.api.latest.HttpResponseBodyShape[] | undefined { return this.responses - ?.map((response) => response.convert()) + ?.flatMap((response) => response.convert()) .filter(isNonNullish); } } diff --git a/packages/parsers/src/openapi/3.1/paths/response/ResponsesObjectConverter.node.ts b/packages/parsers/src/openapi/3.1/paths/response/ResponsesObjectConverter.node.ts index 4b6dcb2894..c61239474b 100644 --- a/packages/parsers/src/openapi/3.1/paths/response/ResponsesObjectConverter.node.ts +++ b/packages/parsers/src/openapi/3.1/paths/response/ResponsesObjectConverter.node.ts @@ -86,19 +86,23 @@ export class ResponsesObjectConverterNode extends BaseOpenApiV3_1ConverterNode< if (bodies == null) { return undefined; } - return bodies?.map((body) => ({ - headers: convertOperationObjectProperties(response.headers), - response: { - statusCode: parseInt(statusCode), - body, - description: response.description, - }, - examples: (response.responses ?? []).flatMap((response) => - (response.examples ?? []) - .map((example) => example.convert()) - .filter(isNonNullish) - ), - })); + return ( + convertOperationObjectProperties(response.headers) ?? [undefined] + ).flatMap((headers) => + bodies?.map((body) => ({ + headers, + response: { + statusCode: parseInt(statusCode), + body, + description: response.description, + }, + examples: (response.responses ?? []).flatMap((response) => + (response.examples ?? []) + .map((example) => example.convert()) + .filter(isNonNullish) + ), + })) + ); }) .filter(isNonNullish); } @@ -107,40 +111,48 @@ export class ResponsesObjectConverterNode extends BaseOpenApiV3_1ConverterNode< return Object.entries(this.errorsByStatusCode ?? {}) .flatMap(([statusCode, response]) => { // TODO: resolve reference here, if not done already - return response.responses?.map((res) => { + return response.responses?.flatMap((res) => { const schema = res.schema; - const shape = schema?.convert(); + let maybeShapes = schema?.convert(); - if (shape == null || schema == null) { + if (maybeShapes == null || schema == null) { return undefined; } - return { - statusCode: parseInt(statusCode), - shape, - description: response.description ?? schema.description, - availability: schema.availability?.convert(), - name: - schema.name ?? - STATUS_CODE_MESSAGES[parseInt(statusCode)] ?? - "UNKNOWN ERROR", - examples: res.examples - ?.map((example) => { - const convertedExample = example.convert(); - if ( - convertedExample == null || - convertedExample.responseBody?.type !== "json" - ) { - return undefined; - } - return { - name: convertedExample.name, - description: convertedExample.description, - responseBody: convertedExample.responseBody, - }; - }) - .filter(isNonNullish), - }; + if (!Array.isArray(maybeShapes)) { + maybeShapes = [maybeShapes]; + } + + return maybeShapes + .map((shape) => ({ + statusCode: parseInt(statusCode), + shape, + description: response.description ?? schema.description, + availability: schema.availability?.convert(), + name: + schema.name ?? + STATUS_CODE_MESSAGES[parseInt(statusCode)] ?? + "UNKNOWN ERROR", + examples: res.examples + ?.map((example) => { + const convertedExample = example.convert(); + if (convertedExample == null) { + return undefined; + } + + if (convertedExample.responseBody?.type !== "json") { + return undefined; + } + + return { + name: convertedExample.name, + description: convertedExample.description, + responseBody: convertedExample.responseBody, + }; + }) + .filter(isNonNullish), + })) + .filter(isNonNullish); }); }) .filter(isNonNullish); diff --git a/packages/parsers/src/openapi/3.1/schemas/ArrayConverter.node.ts b/packages/parsers/src/openapi/3.1/schemas/ArrayConverter.node.ts index 25bcb4985d..80438de129 100644 --- a/packages/parsers/src/openapi/3.1/schemas/ArrayConverter.node.ts +++ b/packages/parsers/src/openapi/3.1/schemas/ArrayConverter.node.ts @@ -18,7 +18,7 @@ export declare namespace ArrayConverterNode { export class ArrayConverterNode extends BaseOpenApiV3_1ConverterNodeWithExample< ArrayConverterNode.Input, - ArrayConverterNode.Output | undefined + ArrayConverterNode.Output[] | undefined > { item: SchemaConverterNode | undefined; @@ -45,20 +45,24 @@ export class ArrayConverterNode extends BaseOpenApiV3_1ConverterNodeWithExample< } } - convert(): ArrayConverterNode.Output | undefined { - const itemShape = this.item?.convert(); + convert(): ArrayConverterNode.Output[] | undefined { + let maybeItemShapes = this.item?.convert(); - if (itemShape == null) { + if (maybeItemShapes == null) { return undefined; } - return { + if (!Array.isArray(maybeItemShapes)) { + maybeItemShapes = [maybeItemShapes]; + } + + return maybeItemShapes.map((itemShape) => ({ type: "alias", value: { type: "list", itemShape, }, - }; + })); } example(): unknown[] | undefined { diff --git a/packages/parsers/src/openapi/3.1/schemas/ComponentsConverter.node.ts b/packages/parsers/src/openapi/3.1/schemas/ComponentsConverter.node.ts index 51f87a3fb6..b2952a5d4f 100644 --- a/packages/parsers/src/openapi/3.1/schemas/ComponentsConverter.node.ts +++ b/packages/parsers/src/openapi/3.1/schemas/ComponentsConverter.node.ts @@ -52,17 +52,21 @@ export class ComponentsConverterNode extends BaseOpenApiV3_1ConverterNode< Object.entries(this.typeSchemas) .map(([key, value]) => { const name = value.name ?? key; - const shape = value.convert(); + let maybeShapes = value.convert(); - if (name == null || shape == null) { + if (maybeShapes == null) { return [key, undefined]; } + if (!Array.isArray(maybeShapes)) { + maybeShapes = [maybeShapes]; + } + return [ FernRegistry.TypeId(key), { name, - shape, + shape: maybeShapes[0], description: value.description, availability: undefined, }, diff --git a/packages/parsers/src/openapi/3.1/schemas/MixedSchemaConverter.node.ts b/packages/parsers/src/openapi/3.1/schemas/MixedSchemaConverter.node.ts index c584495252..6dce90dfb8 100644 --- a/packages/parsers/src/openapi/3.1/schemas/MixedSchemaConverter.node.ts +++ b/packages/parsers/src/openapi/3.1/schemas/MixedSchemaConverter.node.ts @@ -17,8 +17,8 @@ export declare namespace MixedSchemaConverterNode { export class MixedSchemaConverterNode extends BaseOpenApiV3_1ConverterNodeWithExample< MixedSchemaConverterNode.Input, - | FernRegistry.api.latest.TypeShape.UndiscriminatedUnion - | FernRegistry.api.latest.TypeShape.Alias + | FernRegistry.api.latest.TypeShape.UndiscriminatedUnion[] + | FernRegistry.api.latest.TypeShape.Alias[] > { typeNodes: SchemaConverterNode[] | undefined; nullable: boolean | undefined; @@ -49,43 +49,60 @@ export class MixedSchemaConverterNode extends BaseOpenApiV3_1ConverterNodeWithEx } public convert(): - | FernRegistry.api.latest.TypeShape.UndiscriminatedUnion - | FernRegistry.api.latest.TypeShape.Alias + | FernRegistry.api.latest.TypeShape.UndiscriminatedUnion[] + | FernRegistry.api.latest.TypeShape.Alias[] | undefined { if (this.typeNodes == null) { return undefined; } - const union = { - type: "undiscriminatedUnion", - variants: this.typeNodes + const concreteTypeNodes: FernRegistry.api.latest.UndiscriminatedUnionVariant[][] = + this.typeNodes .map((typeNode) => { - const shape = typeNode.convert(); - if (shape == null) { + let maybeShapes = typeNode.convert(); + if (maybeShapes == null) { return undefined; } - return { + if (!Array.isArray(maybeShapes)) { + maybeShapes = [maybeShapes]; + } + + return maybeShapes.map((shape) => ({ displayName: typeNode.name, shape, description: typeNode.description, availability: typeNode.availability?.convert(), - }; + })); }) - .filter(isNonNullish), - } as const; + .filter(isNonNullish); + + const concreteTypeNodePermutations = concreteTypeNodes.reduce< + FernRegistry.api.latest.UndiscriminatedUnionVariant[][] + >( + (acc, curr) => { + return acc.flatMap((acc) => + curr.length > 0 ? curr.map((c) => [...acc, c]) : [[...acc]] + ); + }, + [[]] + ); + const unions = concreteTypeNodePermutations.map((variants) => ({ + type: "undiscriminatedUnion" as const, + variants, + })); // TODO: right now, this is handled as an optional, but we should handle it as nullable return this.nullable - ? { - type: "alias", + ? unions.map((union) => ({ + type: "alias" as const, value: { - type: "optional", + type: "optional" as const, default: union.variants[0], shape: union, }, - } - : union; + })) + : unions; } example(): unknown | undefined { diff --git a/packages/parsers/src/openapi/3.1/schemas/ObjectConverter.node.ts b/packages/parsers/src/openapi/3.1/schemas/ObjectConverter.node.ts index 6d2ee4d16b..b025b87a54 100644 --- a/packages/parsers/src/openapi/3.1/schemas/ObjectConverter.node.ts +++ b/packages/parsers/src/openapi/3.1/schemas/ObjectConverter.node.ts @@ -19,7 +19,7 @@ export declare namespace ObjectConverterNode { export class ObjectConverterNode extends BaseOpenApiV3_1ConverterNodeWithExample< ObjectConverterNode.Input, - FernRegistry.api.latest.TypeShape.Object_ + FernRegistry.api.latest.TypeShape.Object_[] > { description: string | undefined; extends: string[] = []; @@ -101,7 +101,7 @@ export class ObjectConverterNode extends BaseOpenApiV3_1ConverterNodeWithExample this.description = this.input.description; } - convertProperties(): FernRegistry.api.latest.ObjectProperty[] | undefined { + convertProperties(): FernRegistry.api.latest.ObjectProperty[][] | undefined { if (this.properties == null) { return undefined; } @@ -109,39 +109,55 @@ export class ObjectConverterNode extends BaseOpenApiV3_1ConverterNodeWithExample return convertToObjectProperties(this.properties, this.requiredProperties); } - convertExtraProperties(): FernRegistry.api.latest.TypeReference | undefined { + convertExtraProperties(): + | FernRegistry.api.latest.TypeReference[] + | undefined { if (this.extraProperties == null) { return undefined; } if (typeof this.extraProperties === "string") { - return { - type: "unknown", - displayName: this.extraProperties, - }; + return [ + { + type: "unknown", + displayName: this.extraProperties, + }, + ]; } - const convertedShape = this.extraProperties.convert(); + let maybeConvertedShapes = this.extraProperties.convert(); + + if (!Array.isArray(maybeConvertedShapes) && maybeConvertedShapes != null) { + maybeConvertedShapes = [maybeConvertedShapes]; + } - if (convertedShape?.type === "alias") { - return convertedShape.value; + if (maybeConvertedShapes == null) { + return undefined; } - return undefined; + return maybeConvertedShapes + .map((shape) => (shape.type === "alias" ? shape.value : undefined)) + .filter(isNonNullish); } - convert(): FernRegistry.api.latest.TypeShape.Object_ | undefined { - const properties = this.convertProperties(); - if (properties == null) { + convert(): FernRegistry.api.latest.TypeShape.Object_[] | undefined { + const maybeMultipleObjectsProperties = this.convertProperties(); + if (maybeMultipleObjectsProperties == null) { return undefined; } - return { - type: "object", - extends: this.extends.map((id) => FernRegistry.TypeId(id)), - properties, - extraProperties: this.convertExtraProperties(), - }; + const maybeMultipleObjectsExtraProperties = this.convertExtraProperties(); + + return maybeMultipleObjectsProperties.flatMap((properties) => { + return (maybeMultipleObjectsExtraProperties ?? [undefined]).map( + (extraProperties) => ({ + type: "object", + extends: this.extends.map((id) => FernRegistry.TypeId(id)), + properties, + extraProperties, + }) + ); + }); } example(): Record | undefined { diff --git a/packages/parsers/src/openapi/3.1/schemas/OneOfConverter.node.ts b/packages/parsers/src/openapi/3.1/schemas/OneOfConverter.node.ts index 2348040d64..1d4e3e3f29 100644 --- a/packages/parsers/src/openapi/3.1/schemas/OneOfConverter.node.ts +++ b/packages/parsers/src/openapi/3.1/schemas/OneOfConverter.node.ts @@ -10,9 +10,11 @@ import { SchemaConverterNode } from "./SchemaConverter.node"; export class OneOfConverterNode extends BaseOpenApiV3_1ConverterNodeWithExample< OpenAPIV3_1.NonArraySchemaObject, - | FernRegistry.api.latest.TypeShape.DiscriminatedUnion - | FernRegistry.api.latest.TypeShape.UndiscriminatedUnion + | [FernRegistry.api.latest.TypeShape.DiscriminatedUnion] + | [FernRegistry.api.latest.TypeShape.UndiscriminatedUnion] + | FernRegistry.api.latest.TypeShape[] > { + isUnionOfObjects: boolean | undefined; discriminated: boolean | undefined; discriminant: string | undefined; discriminatedMapping: Record | undefined; @@ -28,6 +30,11 @@ export class OneOfConverterNode extends BaseOpenApiV3_1ConverterNodeWithExample< parse(): void { if (this.input.oneOf != null || this.input.anyOf != null) { + this.isUnionOfObjects = (this.input.oneOf ?? this.input.anyOf)?.every( + (schema) => + resolveSchemaReference(schema, this.context.document)?.type === + "object" + ); if (this.input.discriminator == null) { this.discriminated = false; this.undiscriminatedMapping = ( @@ -75,9 +82,16 @@ export class OneOfConverterNode extends BaseOpenApiV3_1ConverterNodeWithExample< } convert(): - | FernRegistry.api.latest.TypeShape.DiscriminatedUnion - | FernRegistry.api.latest.TypeShape.UndiscriminatedUnion + | [FernRegistry.api.latest.TypeShape.DiscriminatedUnion] + | [FernRegistry.api.latest.TypeShape.UndiscriminatedUnion] + | FernRegistry.api.latest.TypeShape[] | undefined { + if (!this.isUnionOfObjects && !this.discriminated) { + return this.undiscriminatedMapping + ?.flatMap((node) => node.convert()) + .filter(isNonNullish); + } + if ( // If no decision has been made, bail this.discriminated == null || @@ -92,49 +106,70 @@ export class OneOfConverterNode extends BaseOpenApiV3_1ConverterNodeWithExample< return this.discriminated && this.discriminant != null && this.discriminatedMapping != null - ? { - type: "discriminatedUnion", - discriminant: FernRegistry.PropertyKey(this.discriminant), - variants: Object.entries(this.discriminatedMapping) - .map(([key, node]) => { - const convertedShape = node.convert(); - if (convertedShape == null || convertedShape.type !== "object") { - return undefined; - } - return { - discriminantValue: key, - // TODO x-fern-display-name extension - displayName: undefined, - // TODO x-fern-availability extension - availability: undefined, - description: node.description, - ...convertedShape, - }; - }) - .filter(isNonNullish), - } - : this.undiscriminatedMapping != null - ? { - type: "undiscriminatedUnion", - variants: this.undiscriminatedMapping - .map((node) => { - const convertedShape = node.convert(); - if ( - convertedShape == null || - convertedShape.type !== "object" - ) { - return undefined; + ? [ + { + type: "discriminatedUnion", + discriminant: FernRegistry.PropertyKey(this.discriminant), + variants: Object.entries(this.discriminatedMapping) + .flatMap(([key, node]) => { + let convertedShape: + | FernRegistry.api.latest.TypeShape + | FernRegistry.api.latest.TypeShape[] + | undefined = node.convert(); + if (!Array.isArray(convertedShape) && convertedShape != null) { + convertedShape = [convertedShape]; } - return { - displayName: node.name, - shape: convertedShape, - description: node.description, - // TODO: handle availability - availability: undefined, - }; + return convertedShape + ?.map((shape) => { + if (shape == null || shape.type !== "object") { + return undefined; + } + return { + discriminantValue: key, + displayName: node.name, + availability: node.availability?.convert(), + description: node.description, + ...shape, + }; + }) + .filter(isNonNullish); }) .filter(isNonNullish), - } + }, + ] + : this.undiscriminatedMapping != null + ? [ + { + type: "undiscriminatedUnion", + variants: this.undiscriminatedMapping + .flatMap((node) => { + let convertedShape: + | FernRegistry.api.latest.TypeShape + | FernRegistry.api.latest.TypeShape[] + | undefined = node.convert(); + if ( + !Array.isArray(convertedShape) && + convertedShape != null + ) { + convertedShape = [convertedShape]; + } + return convertedShape + ?.map((shape) => { + if (shape == null || shape.type !== "object") { + return undefined; + } + return { + displayName: node.name, + shape, + description: node.description, + availability: node.availability?.convert(), + }; + }) + .filter(isNonNullish); + }) + .filter(isNonNullish), + }, + ] : undefined; } diff --git a/packages/parsers/src/openapi/3.1/schemas/ReferenceConverter.node.ts b/packages/parsers/src/openapi/3.1/schemas/ReferenceConverter.node.ts index 5a3fafa05f..5500361d94 100644 --- a/packages/parsers/src/openapi/3.1/schemas/ReferenceConverter.node.ts +++ b/packages/parsers/src/openapi/3.1/schemas/ReferenceConverter.node.ts @@ -53,6 +53,7 @@ export class ReferenceConverterNode extends BaseOpenApiV3_1ConverterNodeWithExam if (schema == null) { return undefined; } + return new SchemaConverterNode({ input: schema, context: this.context, diff --git a/packages/parsers/src/openapi/3.1/schemas/SchemaConverter.node.ts b/packages/parsers/src/openapi/3.1/schemas/SchemaConverter.node.ts index fbeab5229a..cab65547a5 100644 --- a/packages/parsers/src/openapi/3.1/schemas/SchemaConverter.node.ts +++ b/packages/parsers/src/openapi/3.1/schemas/SchemaConverter.node.ts @@ -37,12 +37,16 @@ export type PrimitiveType = export class SchemaConverterNode extends BaseOpenApiV3_1ConverterNodeWithExample< OpenAPIV3_1.SchemaObject | OpenAPIV3_1.ReferenceObject, - FernRegistry.api.latest.TypeShape | undefined + | FernRegistry.api.latest.TypeShape + | FernRegistry.api.latest.TypeShape[] + | undefined > { typeShapeNode: | BaseOpenApiV3_1ConverterNodeWithExample< unknown, - FernRegistry.api.latest.TypeShape | undefined + | FernRegistry.api.latest.TypeShape + | FernRegistry.api.latest.TypeShape[] + | undefined > | undefined; @@ -218,7 +222,10 @@ export class SchemaConverterNode extends BaseOpenApiV3_1ConverterNodeWithExample } } - convert(): FernRegistry.api.latest.TypeShape | undefined { + convert(): + | FernRegistry.api.latest.TypeShape + | FernRegistry.api.latest.TypeShape[] + | undefined { return this.typeShapeNode?.convert(); } diff --git a/packages/parsers/src/openapi/3.1/schemas/__test__/ArrayConverter.node.test.ts b/packages/parsers/src/openapi/3.1/schemas/__test__/ArrayConverter.node.test.ts index 06e95b8b90..6b301e988c 100644 --- a/packages/parsers/src/openapi/3.1/schemas/__test__/ArrayConverter.node.test.ts +++ b/packages/parsers/src/openapi/3.1/schemas/__test__/ArrayConverter.node.test.ts @@ -56,21 +56,23 @@ describe("ArrayConverterNode", () => { pathId: "test", }); const converted = node.convert(); - expect(converted).toEqual({ - type: "alias", - value: { - type: "list", - itemShape: { - type: "alias", - value: { - type: "primitive", + expect(converted).toEqual([ + { + type: "alias", + value: { + type: "list", + itemShape: { + type: "alias", value: { - type: "string", + type: "primitive", + value: { + type: "string", + }, }, }, }, }, - }); + ]); }); it("should return undefined if inner schema conversion fails", () => { diff --git a/packages/parsers/src/openapi/3.1/schemas/__test__/MixedSchemaConverter.node.test.ts b/packages/parsers/src/openapi/3.1/schemas/__test__/MixedSchemaConverter.node.test.ts index e447f5cceb..66135f37d6 100644 --- a/packages/parsers/src/openapi/3.1/schemas/__test__/MixedSchemaConverter.node.test.ts +++ b/packages/parsers/src/openapi/3.1/schemas/__test__/MixedSchemaConverter.node.test.ts @@ -59,7 +59,8 @@ describe("MixedSchemaConverterNode", () => { pathId: "test", }); - const result = node.convert(); + const result = node.convert()[0]; + console.log(JSON.stringify(result, null, 2)); expect(result?.type).toBe("undiscriminatedUnion"); expect( (result as FernRegistry.api.latest.TypeShape.UndiscriminatedUnion) @@ -80,7 +81,7 @@ describe("MixedSchemaConverterNode", () => { pathId: "test", }); - const result = node.convert(); + const result = node.convert()[0]; expect(result?.type).toBe("alias"); expect( (result as FernRegistry.api.latest.TypeShape.Alias)?.value?.type diff --git a/packages/parsers/src/openapi/3.1/schemas/__test__/ObjectConverter.node.test.ts b/packages/parsers/src/openapi/3.1/schemas/__test__/ObjectConverter.node.test.ts index e89f09a082..f997cda2de 100644 --- a/packages/parsers/src/openapi/3.1/schemas/__test__/ObjectConverter.node.test.ts +++ b/packages/parsers/src/openapi/3.1/schemas/__test__/ObjectConverter.node.test.ts @@ -57,7 +57,7 @@ describe("ObjectConverterNode", () => { pathId: "test", }); expect(node.extraProperties).toBeDefined(); - const converted = node.convert()?.extraProperties; + const converted = node.convert()[0]?.extraProperties; expect(converted).toEqual({ type: "unknown", displayName: "TestObject", @@ -108,19 +108,21 @@ describe("ObjectConverterNode", () => { pathId: "test", }); const converted = node.convert(); - expect(converted).toEqual({ - type: "object", - extends: [], - properties: [ - { - key: "name", - valueShape: expect.any(Object), - description: undefined, - availability: undefined, - }, - ], - extraProperties: undefined, - }); + expect(converted).toEqual([ + { + type: "object", + extends: [], + properties: [ + { + key: "name", + valueShape: expect.any(Object), + description: undefined, + availability: undefined, + }, + ], + extraProperties: undefined, + }, + ]); }); it("should convert object schema with extra properties", () => { @@ -136,15 +138,17 @@ describe("ObjectConverterNode", () => { pathId: "test", }); const converted = node.convert(); - expect(converted).toEqual({ - type: "object", - extends: [], - properties: [], - extraProperties: { - type: "unknown", - displayName: "TestObject", + expect(converted).toEqual([ + { + type: "object", + extends: [], + properties: [], + extraProperties: { + type: "unknown", + displayName: "TestObject", + }, }, - }); + ]); }); }); }); diff --git a/packages/parsers/src/openapi/3.1/schemas/__test__/OneOfConverter.node.test.ts b/packages/parsers/src/openapi/3.1/schemas/__test__/OneOfConverter.node.test.ts index 3986c68d66..3e46724b58 100644 --- a/packages/parsers/src/openapi/3.1/schemas/__test__/OneOfConverter.node.test.ts +++ b/packages/parsers/src/openapi/3.1/schemas/__test__/OneOfConverter.node.test.ts @@ -81,7 +81,7 @@ describe("OneOfConverterNode", () => { accessPath: [], pathId: "test", }); - const result = node.convert(); + const result = node.convert()[0]; expect(result?.type).toBe("discriminatedUnion"); expect(result?.variants.length).toEqual(1); }); @@ -100,7 +100,7 @@ describe("OneOfConverterNode", () => { accessPath: [], pathId: "test", }); - const result = node.convert(); + const result = node.convert()[0]; expect(result?.type).toBe("undiscriminatedUnion"); expect(result).toEqual({ type: "undiscriminatedUnion", diff --git a/packages/parsers/src/openapi/utils/3.1/convertToObjectProperties.ts b/packages/parsers/src/openapi/utils/3.1/convertToObjectProperties.ts index ba89b6eec8..6ca81b957b 100644 --- a/packages/parsers/src/openapi/utils/3.1/convertToObjectProperties.ts +++ b/packages/parsers/src/openapi/utils/3.1/convertToObjectProperties.ts @@ -10,36 +10,53 @@ export function convertToObjectProperties( | Record | undefined, requiredProperties: string[] | undefined -): FernRegistry.api.latest.ObjectProperty[] | undefined { +): FernRegistry.api.latest.ObjectProperty[][] | undefined { if (properties == null) { return undefined; } - return Object.entries(properties) + const rawProperties = Object.entries(properties) .map(([key, node]) => { - let valueShape = node.convert(); - if (valueShape == null) { + let maybeValueShapes = node.convert(); + if (maybeValueShapes == null) { return undefined; } - if (requiredProperties != null && !requiredProperties.includes(key)) { - valueShape = { - type: "alias", - value: { - type: "optional", - shape: valueShape, - default: - valueShape.type === "enum" ? valueShape.default : undefined, - }, - }; + if (!Array.isArray(maybeValueShapes)) { + maybeValueShapes = [maybeValueShapes]; } - return { - key: FernRegistry.PropertyKey(key), - valueShape, - description: node.description, - availability: node.availability?.convert(), - }; + return maybeValueShapes + .map((valueShape) => { + if (requiredProperties != null && !requiredProperties.includes(key)) { + valueShape = { + type: "alias", + value: { + type: "optional", + shape: valueShape, + default: + valueShape.type === "enum" ? valueShape.default : undefined, + }, + }; + } + + return { + key: FernRegistry.PropertyKey(key), + valueShape, + description: node.description, + availability: node.availability?.convert(), + }; + }) + .filter(isNonNullish); }) .filter(isNonNullish); + + return rawProperties.reduce( + (acc, curr) => { + return acc.flatMap((a) => + curr.length > 0 ? curr.map((b) => [...a, b]) : [[...a]] + ); + }, + [[]] + ); } diff --git a/packages/parsers/src/openapi/utils/__test__/3.1/convertToObjectProperties.test.ts b/packages/parsers/src/openapi/utils/__test__/3.1/convertToObjectProperties.test.ts index 5df2a87952..8d3daacc5b 100644 --- a/packages/parsers/src/openapi/utils/__test__/3.1/convertToObjectProperties.test.ts +++ b/packages/parsers/src/openapi/utils/__test__/3.1/convertToObjectProperties.test.ts @@ -61,18 +61,20 @@ describe("convertToObjectProperties", () => { const result = convertToObjectProperties(properties, undefined); expect(result).toEqual([ - { - key: FernRegistry.PropertyKey("name"), - valueShape: mockTypeShape, - description: "The name", - availability: mockAvailabilityShape, - }, - { - key: FernRegistry.PropertyKey("age"), - valueShape: mockTypeShape, - description: undefined, - availability: undefined, - }, + [ + { + key: FernRegistry.PropertyKey("name"), + valueShape: mockTypeShape, + description: "The name", + availability: mockAvailabilityShape, + }, + { + key: FernRegistry.PropertyKey("age"), + valueShape: mockTypeShape, + description: undefined, + availability: undefined, + }, + ], ]); }); @@ -110,12 +112,14 @@ describe("convertToObjectProperties", () => { const result = convertToObjectProperties(properties, undefined); expect(result).toEqual([ - { - key: FernRegistry.PropertyKey("valid"), - valueShape: mockTypeShape, - description: undefined, - availability: undefined, - }, + [ + { + key: FernRegistry.PropertyKey("valid"), + valueShape: mockTypeShape, + description: undefined, + availability: undefined, + }, + ], ]); }); });