UNPKG

@typespec/openapi3

Version:

TypeSpec library for emitting OpenAPI 3.0 and OpenAPI 3.1 from the TypeSpec REST protocol binding and converting OpenAPI3 to TypeSpec

516 lines (511 loc) 21.4 kB
import { getEncode, getOpExamples, ignoreDiagnostics, serializeValueAsJson, } from "@typespec/compiler"; import { $ } from "@typespec/compiler/typekit"; import { isHeader, } from "@typespec/http"; import { getParameterStyle } from "./parameters.js"; import { getOpenAPI3StatusCodes } from "./status-codes.js"; import { isHttpParameterProperty, isSharedHttpOperation, } from "./util.js"; export function resolveOperationExamples(program, operation, { parameterExamplesStrategy }) { const examples = findOperationExamples(program, operation); const result = { requestBody: {}, parameters: {}, responses: {} }; if (examples.length === 0) { return result; } for (const [op, example] of examples) { if (example.parameters && op.parameters.body) { const contentTypeValue = getContentTypeValue(example.parameters, op.parameters.properties) ?? "application/json"; result.requestBody[contentTypeValue] ??= []; const value = getBodyValue(example.parameters, op.parameters.properties); if (value) { result.requestBody[contentTypeValue].push([ { value, title: example.title, description: example.description, }, op.parameters.body.type, ]); } } if (example.parameters) { // iterate over properties for (const property of op.parameters.properties) { if (!isHttpParameterProperty(property)) continue; const value = getParameterValue(program, example.parameters, property, parameterExamplesStrategy); if (value) { const parameterName = property.options.name; result.parameters[parameterName] ??= []; result.parameters[parameterName].push([ { value, title: example.title, description: example.description, }, property.property, ]); } } } if (example.returnType && op.responses) { const match = findResponseForExample(program, example.returnType, op.responses); if (match) { const value = getBodyValue(example.returnType, match.response.properties); if (value) { for (const statusCode of match.statusCodes) { result.responses[statusCode] ??= {}; result.responses[statusCode][match.contentType] ??= []; result.responses[statusCode][match.contentType].push([ { value, title: example.title, description: example.description, }, match.response.body.type, ]); } } } } } return result; } function findOperationExamples(program, operation) { if (isSharedHttpOperation(operation)) { return operation.operations.flatMap((op) => getOpExamples(program, op.operation).map((x) => [op, x])); } else { return getOpExamples(program, operation.operation).map((x) => [operation, x]); } } function isStatusCodeIn(exampleStatusCode, statusCodes) { if (statusCodes === "*") { return true; } if (typeof statusCodes === "number") { return exampleStatusCode === statusCodes; } return exampleStatusCode >= statusCodes.start && exampleStatusCode <= statusCodes.end; } function findResponseForExample(program, exampleValue, responses) { const tentatives = []; for (const statusCodeResponse of responses) { for (const response of statusCodeResponse.responses) { if (response.body === undefined) { continue; } const contentType = getContentTypeValue(exampleValue, response.properties); const statusCode = getStatusCodeValue(exampleValue, response.properties); const contentTypeProp = response.properties.find((x) => x.kind === "contentType"); // if undefined MUST be application/json const statusCodeProp = response.properties.find((x) => x.kind === "statusCode"); // if undefined MUST be 200 const statusCodeMatch = statusCode && statusCodeProp && isStatusCodeIn(statusCode, statusCodeResponse.statusCodes); const contentTypeMatch = contentType && response.body?.contentTypes.includes(contentType); if (statusCodeMatch && contentTypeMatch) { return { contentType, statusCodes: ignoreDiagnostics(getOpenAPI3StatusCodes(program, statusCodeResponse.statusCodes, statusCodeResponse.type)), response, }; } else if (statusCodeMatch && contentTypeProp === undefined) { tentatives.push([ { response, statusCodes: ignoreDiagnostics(getOpenAPI3StatusCodes(program, statusCodeResponse.statusCodes, statusCodeResponse.type)), }, 1, ]); } else if (contentTypeMatch && statusCodeMatch === undefined) { tentatives.push([{ response, contentType }, 1]); } else if (contentTypeProp === undefined && statusCodeProp === undefined) { tentatives.push([{ response }, 0]); } } } const tentative = tentatives.sort((a, b) => a[1] - b[1]).pop(); if (tentative) { return { contentType: tentative[0].contentType ?? "application/json", statusCodes: tentative[0].statusCodes ?? ["200"], response: tentative[0].response, }; } return undefined; } /** * Only returns an encoding if one is not explicitly defined */ function getDefaultHeaderEncodeAs(program, header) { // Get existing encoded data if it has been explicitly defined const encodeData = getEncode(program, header); // If there's an explicit encoding, return undefined if (encodeData) return; const tk = $(program); if (!tk.scalar.isUtcDateTime(header.type) && tk.scalar.isOffsetDateTime(header.type)) { return; } if (!tk.scalar.is(header.type)) return; // Use the default encoding for date-time headers return { encoding: "rfc7231", type: header.type, }; } /** * This function should only be used for special default encodings. */ function getEncodeAs(program, type) { if (isHeader(program, type)) { return getDefaultHeaderEncodeAs(program, type); } return undefined; } export function getExampleOrExamples(program, examples) { if (examples.length === 0) { return {}; } if (examples.length === 1 && examples[0][0].title === undefined && examples[0][0].description === undefined) { const [example, type] = examples[0]; const encodeAs = getEncodeAs(program, type); return { example: serializeValueAsJson(program, example.value, type, encodeAs) }; } else { const exampleObj = {}; for (const [index, [example, type]] of examples.entries()) { const encodeAs = getEncodeAs(program, type); exampleObj[example.title ?? `example${index}`] = { summary: example.title, description: example.description, value: serializeValueAsJson(program, example.value, type, encodeAs), }; } return { examples: exampleObj }; } } export function getStatusCodeValue(value, properties) { const statusCodeProperty = properties.find((p) => p.kind === "statusCode"); if (statusCodeProperty === undefined) { return undefined; } const statusCode = getValueByPath(value, statusCodeProperty.path); if (statusCode?.valueKind === "NumericValue") { return statusCode.value.asNumber() ?? undefined; } return undefined; } export function getContentTypeValue(value, properties) { const contentTypeProperty = properties.find((p) => p.kind === "contentType"); if (contentTypeProperty === undefined) { return undefined; } const statusCode = getValueByPath(value, contentTypeProperty.path); if (statusCode?.valueKind === "StringValue") { return statusCode.value; } return undefined; } export function getBodyValue(value, properties) { const bodyProperty = properties.find((p) => p.kind === "body" || p.kind === "bodyRoot"); if (bodyProperty !== undefined) { return getValueByPath(value, bodyProperty.path); } return value; } function getParameterValue(program, parameterExamples, property, parameterExamplesStrategy) { if (!parameterExamplesStrategy) { return; } const value = getValueByPath(parameterExamples, property.path); if (!value) return; if (parameterExamplesStrategy === "data") { return value; } // Depending on the parameter type, we may need to serialize the value differently. // https://spec.openapis.org/oas/v3.0.4.html#style-examples /* Supported styles per location: https://spec.openapis.org/oas/v3.0.4.html#style-values | Location | Default Style | Supported Styles | | -------- | ------------- | ----------------------------------------------- | | query | form | form, spaceDelimited, pipeDelimited, deepObject | | header | simple | simple | | path | simple | simple, label, matrix | | cookie | form | form | */ // explode is only relevant for array/object types if (property.kind === "query") { return getQueryParameterValue(program, value, property); } else if (property.kind === "header") { return getHeaderParameterValue(program, value, property); } else if (property.kind === "path") { return getPathParameterValue(program, value, property); } else if (property.kind === "cookie") { return getCookieParameterValue(program, value, property); } return value; } function getQueryParameterValue(program, originalValue, property) { const style = getParameterStyle(program, property.property) ?? "form"; switch (style) { case "form": return getParameterFormValue(program, originalValue, property); case "spaceDelimited": return getParameterDelimitedValue(program, originalValue, property, " "); case "pipeDelimited": return getParameterDelimitedValue(program, originalValue, property, "|"); } } function getHeaderParameterValue(program, originalValue, property) { return getParameterSimpleValue(program, originalValue, property); } function getPathParameterValue(program, originalValue, property) { const { style } = property.options; if (style === "label") { return getParameterLabelValue(program, originalValue, property); } else if (style === "matrix") { return getParameterMatrixValue(program, originalValue, property); } else if (style === "simple") { return getParameterSimpleValue(program, originalValue, property); } return undefined; } function getCookieParameterValue(program, originalValue, property) { return getParameterFormValue(program, originalValue, property); } function getParameterLabelValue(program, originalValue, property) { const { explode } = property.options; const tk = $(program); /* https://spec.openapis.org/oas/v3.0.4.html#style-examples string -> "blue" array -> ["blue", "black", "brown"] object -> { "R": 100, "G": 200, "B": 150 } | explode | string | array | object | | ------- | ------- | ----------------- | ------------------ | | false | .blue | .blue,black,brown | .R,100,G,200,B,150 | | true | .blue | .blue.black.brown | .R=100.G=200.B=150 | */ const joiner = explode ? "." : ","; if (tk.value.isArray(originalValue)) { const pairs = []; for (const value of originalValue.values) { if (!isSerializableScalarValue(value)) continue; pairs.push(`${value.value}`); } return tk.value.createString(`.${pairs.join(joiner)}`); } if (tk.value.isObject(originalValue)) { const pairs = []; for (const [key, { value }] of originalValue.properties) { if (!isSerializableScalarValue(value)) continue; const sep = explode ? "=" : ","; pairs.push(`${key}${sep}${value.value}`); } return tk.value.createString(`.${pairs.join(joiner)}`); } // null (undefined) is treated as a a dot if (tk.value.isNull(originalValue)) { return tk.value.createString("."); } if (isSerializableScalarValue(originalValue)) { return tk.value.createString(`.${originalValue.value}`); } return; } function getParameterMatrixValue(program, originalValue, property) { const { explode, name } = property.options; const tk = $(program); /* https://spec.openapis.org/oas/v3.0.4.html#style-examples string -> "blue" array -> ["blue", "black", "brown"] object -> { "R": 100, "G": 200, "B": 150 } | explode | string | array | object | | ------- | ------------- | ----------------------------------- | ------------------------ | | false | ;color=blue | ;color=blue,black,brown | ;color=R,100,G,200,B,150 | | true | ;color=blue | ;color=blue;color=black;color=brown | ;R=100;G=200;B=150 | */ const joiner = explode ? ";" : ","; const prefix = explode ? "" : `${name}=`; if (tk.value.isArray(originalValue)) { const pairs = []; for (const value of originalValue.values) { if (!isSerializableScalarValue(value)) continue; pairs.push(explode ? `${name}=${value.value}` : `${value.value}`); } return tk.value.createString(`;${prefix}${pairs.join(joiner)}`); } if (tk.value.isObject(originalValue)) { const sep = explode ? "=" : ","; const pairs = []; for (const [key, { value }] of originalValue.properties) { if (!isSerializableScalarValue(value)) continue; pairs.push(`${key}${sep}${value.value}`); } return tk.value.createString(`;${prefix}${pairs.join(joiner)}`); } if (tk.value.isNull(originalValue)) { return tk.value.createString(`;${name}`); } if (isSerializableScalarValue(originalValue)) { return tk.value.createString(`;${name}=${originalValue.value}`); } return; } function getParameterDelimitedValue(program, originalValue, property, delimiter) { const { explode, name } = property.options; // Serialization is undefined for explode=true if (explode) return undefined; const tk = $(program); // cspell: ignore Cblack Cbrown /* https://spec.openapis.org/oas/v3.0.4.html#style-examples array -> ["blue", "black", "brown"] object -> { "R": 100, "G": 200, "B": 150 } | style | explode | string | array | object | | ----- | ------- | ------ | -------------------------- | ----------------------------------| | pipe | false | n/a | color=blue%7Cblack%7Cbrown | color=R%7C100%7CG%7C200%7CB%7C150 | | pipe | true | n/a | n/a | n/a | | space | false | n/a | color=blue%20black%20brown | color=R%20100%20G%20200%20B%20150 | | space | true | n/a | n/a | n/a | */ if (tk.value.isArray(originalValue)) { const pairs = []; for (const value of originalValue.values) { if (!isSerializableScalarValue(value)) continue; pairs.push(`${value.value}`); } return tk.value.createString(`${name}=${encodeURIComponent(pairs.join(delimiter))}`); } if (tk.value.isObject(originalValue)) { const pairs = []; for (const [key, { value }] of originalValue.properties) { if (!isSerializableScalarValue(value)) continue; pairs.push(`${key}${delimiter}${value.value}`); } return tk.value.createString(`${name}=${encodeURIComponent(pairs.join(delimiter))}`); } return undefined; } function getParameterFormValue(program, originalValue, property) { const { name } = property.options; const isCookie = property.kind === "cookie"; const explode = isCookie ? false : property.options.explode; const tk = $(program); /* https://spec.openapis.org/oas/v3.0.4.html#style-examples string -> "blue" array -> ["blue", "black", "brown"] object -> { "R": 100, "G": 200, "B": 150 } | explode | string | array | object | | ------- | ------------ | ---------------------------------- | ----------------------- | | false | color=blue | color=blue,black,brown | color=R,100,G,200,B,150 | | true | color=blue | color=blue&color=black&color=brown | R=100&G=200&B=150 | */ const prefix = explode ? "" : `${name}=`; if (tk.value.isArray(originalValue)) { const sep = explode ? "&" : ","; const pairs = []; for (const value of originalValue.values) { if (!isSerializableScalarValue(value)) continue; pairs.push(explode ? `${name}=${value.value}` : `${value.value}`); } return tk.value.createString(`${prefix}${pairs.join(sep)}`); } if (tk.value.isObject(originalValue)) { const sep = explode ? "=" : ","; const joiner = explode ? "&" : ","; const pairs = []; for (const [key, { value }] of originalValue.properties) { if (!isSerializableScalarValue(value)) continue; pairs.push(`${key}${sep}${value.value}`); } return tk.value.createString(`${prefix}${pairs.join(joiner)}`); } if (isSerializableScalarValue(originalValue)) { return tk.value.createString(`${name}=${originalValue.value}`); } // null is treated as the 'undefined' value if (tk.value.isNull(originalValue)) { return tk.value.createString(`${name}=`); } return; } function getParameterSimpleValue(program, originalValue, property) { const { explode } = property.options; const tk = $(program); /* https://spec.openapis.org/oas/v3.0.4.html#style-examples string -> "blue" array -> ["blue", "black", "brown"] object -> { "R": 100, "G": 200, "B": 150 } | explode | string | array | object | | ------- | ------ | ---------------- | ----------------- | | false | blue | blue,black,brown | R,100,G,200,B,150 | | true | blue | blue,black,brown | R=100,G=200,B=150 | */ if (tk.value.isArray(originalValue)) { const serializedValue = originalValue.values .filter(isSerializableScalarValue) .map((v) => v.value) .join(","); return tk.value.createString(serializedValue); } if (tk.value.isObject(originalValue)) { const pairs = []; for (const [key, { value }] of originalValue.properties) { if (!isSerializableScalarValue(value)) continue; const sep = explode ? "=" : ","; pairs.push(`${key}${sep}${value.value}`); } return tk.value.createString(pairs.join(",")); } // null (undefined) is treated as an empty string - unrelated to allowEmptyValue if (tk.value.isNull(originalValue)) { return tk.value.createString(""); } if (isSerializableScalarValue(originalValue)) { return originalValue; } return; } function isSerializableScalarValue(value) { return ["BooleanValue", "NumericValue", "StringValue"].includes(value.valueKind); } function getValueByPath(value, path) { let current = value; for (const key of path) { switch (current?.valueKind) { case "ObjectValue": current = current.properties.get(key.toString())?.value; break; case "ArrayValue": current = current.values[key]; break; default: return undefined; } } return current; } //# sourceMappingURL=examples.js.map