UNPKG

@scalar/oas-utils

Version:

Open API spec and Yaml handling utilities

279 lines (277 loc) 12.3 kB
/** Hard limit for rendering circular references */ const MAX_LEVELS_DEEP = 5; /** Sets the max number of properties after the third level to prevent exponential horizontal growth */ const MAX_PROPERTIES = 10; const genericExampleValues = { // 'date-time': '1970-01-01T00:00:00Z', 'date-time': new Date().toISOString(), // 'date': '1970-01-01', 'date': new Date().toISOString().split('T')[0], 'email': 'hello@example.com', 'hostname': 'example.com', // https://tools.ietf.org/html/rfc6531#section-3.3 'idn-email': 'jane.doe@example.com', // https://tools.ietf.org/html/rfc5890#section-2.3.2.3 'idn-hostname': 'example.com', 'ipv4': '127.0.0.1', 'ipv6': '51d4:7fab:bfbf:b7d7:b2cb:d4b4:3dad:d998', 'iri-reference': '/entitiy/1', // https://tools.ietf.org/html/rfc3987 'iri': 'https://example.com/entity/123', 'json-pointer': '/nested/objects', 'password': 'super-secret', 'regex': '/[a-z]/', // https://tools.ietf.org/html/draft-handrews-relative-json-pointer-01 'relative-json-pointer': '1/nested/objects', // full-time in https://tools.ietf.org/html/rfc3339#section-5.6 // 'time': '00:00:00Z', 'time': new Date().toISOString().split('T')[1].split('.')[0], // either a URI or relative-reference https://tools.ietf.org/html/rfc3986#section-4.1 'uri-reference': '../folder', 'uri-template': 'https://example.com/{id}', 'uri': 'https://example.com', 'uuid': '123e4567-e89b-12d3-a456-426614174000', 'object-id': '6592008029c8c3e4dc76256c', }; /** * We can use the `format` to generate some random values. */ function guessFromFormat(schema, fallback = '') { return genericExampleValues[schema.format] ?? fallback; } /** Map of all the results */ const resultCache = new WeakMap(); /** Store result in the cache, and return the result */ function cache(schema, result) { // Avoid unnecessary WeakMap operations for primitive values if (typeof result !== 'object' || result === null) { return result; } resultCache.set(schema, result); return result; } /** * This function takes an OpenAPI schema and generates an example from it */ const getExampleFromSchema = (schema, options, level = 0, parentSchema, name) => { // Check if the result is already cached if (resultCache.has(schema)) { return resultCache.get(schema); } // Check whether it’s a circular reference if (level === MAX_LEVELS_DEEP + 1) { try { // Fails if it contains a circular reference JSON.stringify(schema); } catch { return '[Circular Reference]'; } } // Sometimes, we just want the structure and no values. // But if `emptyString` is set, we do want to see some values. const makeUpRandomData = !!options?.emptyString; // Check if the property is read-only/write-only if ((options?.mode === 'write' && schema.readOnly) || (options?.mode === 'read' && schema.writeOnly)) { return undefined; } // Use given variables as values if (schema['x-variable']) { const value = options?.variables?.[schema['x-variable']]; // Return the value if it’s defined if (value !== undefined) { // Type-casting if (schema.type === 'number' || schema.type === 'integer') { return Number.parseInt(value, 10); } return cache(schema, value); } } // Use the first example, if there’s an array if (Array.isArray(schema.examples) && schema.examples.length > 0) { return cache(schema, schema.examples[0]); } // Use an example, if there’s one if (schema.example !== undefined) { return cache(schema, schema.example); } // Use a default value, if there’s one if (schema.default !== undefined) { return cache(schema, schema.default); } // enum: [ 'available', 'pending', 'sold' ] if (Array.isArray(schema.enum) && schema.enum.length > 0) { return cache(schema, schema.enum[0]); } // Check if the property is required const isObjectOrArray = schema.type === 'object' || schema.type === 'array' || !!schema.allOf?.at?.(0) || !!schema.anyOf?.at?.(0) || !!schema.oneOf?.at?.(0); if (!isObjectOrArray && options?.omitEmptyAndOptionalProperties === true) { const isRequired = schema.required === true || parentSchema?.required === true || parentSchema?.required?.includes(name ?? schema.name); if (!isRequired) { return undefined; } } // Object if (schema.type === 'object' || schema.properties !== undefined) { const response = {}; let propertyCount = 0; // Regular properties if (schema.properties !== undefined) { for (const propertyName in schema.properties) { if (Object.prototype.hasOwnProperty.call(schema.properties, propertyName)) { // Only apply property limit for nested levels (level > 0) if (level > 3 && propertyCount >= MAX_PROPERTIES) { response['...'] = '[Additional Properties Truncated]'; break; } const property = schema.properties[propertyName]; const propertyXmlTagName = options?.xml ? property.xml?.name : undefined; const value = getExampleFromSchema(property, options, level + 1, schema, propertyName); if (typeof value !== 'undefined') { response[propertyXmlTagName ?? propertyName] = value; propertyCount++; } } } } // Pattern properties (regex) if (schema.patternProperties !== undefined) { for (const pattern in schema.patternProperties) { if (Object.prototype.hasOwnProperty.call(schema.patternProperties, pattern)) { const property = schema.patternProperties[pattern]; // Use the regex pattern as an example key const exampleKey = pattern; response[exampleKey] = getExampleFromSchema(property, options, level + 1, schema, exampleKey); } } } // Additional properties if (schema.additionalProperties !== undefined) { const anyTypeIsValid = // true schema.additionalProperties === true || // or an empty object {} (typeof schema.additionalProperties === 'object' && !Object.keys(schema.additionalProperties).length); if (anyTypeIsValid) { response['ANY_ADDITIONAL_PROPERTY'] = 'anything'; } else if (schema.additionalProperties !== false) { response['ANY_ADDITIONAL_PROPERTY'] = getExampleFromSchema(schema.additionalProperties, options, level + 1); } } if (schema.anyOf !== undefined) { Object.assign(response, getExampleFromSchema(schema.anyOf[0], options, level + 1)); } else if (schema.oneOf !== undefined) { Object.assign(response, getExampleFromSchema(schema.oneOf[0], options, level + 1)); } else if (schema.allOf !== undefined) { Object.assign(response, ...schema.allOf .map((item) => getExampleFromSchema(item, options, level + 1, schema)) .filter((item) => item !== undefined)); } return cache(schema, response); } // Array if (schema.type === 'array' || schema.items !== undefined) { const itemsXmlTagName = schema?.items?.xml?.name; const wrapItems = !!(options?.xml && schema.xml?.wrapped && itemsXmlTagName); if (schema.example !== undefined) { return cache(schema, wrapItems ? { [itemsXmlTagName]: schema.example } : schema.example); } // Check whether the array has a anyOf, oneOf, or allOf rule if (schema.items) { // First handle allOf separately since it needs special handling if (schema.items.allOf) { // If the first item is an object type, merge all schemas if (schema.items.allOf[0].type === 'object') { const mergedExample = getExampleFromSchema({ type: 'object', allOf: schema.items.allOf }, options, level + 1, schema); return cache(schema, wrapItems ? [{ [itemsXmlTagName]: mergedExample }] : [mergedExample]); } // For non-objects (like strings), collect all examples const examples = schema.items.allOf .map((item) => getExampleFromSchema(item, options, level + 1, schema)) .filter((item) => item !== undefined); return cache(schema, wrapItems ? examples.map((example) => ({ [itemsXmlTagName]: example })) : examples); } // Handle other rules (anyOf, oneOf) const rules = ['anyOf', 'oneOf']; for (const rule of rules) { if (!schema.items[rule]) { continue; } const schemas = schema.items[rule].slice(0, 1); const exampleFromRule = schemas .map((item) => getExampleFromSchema(item, options, level + 1, schema)) .filter((item) => item !== undefined); return cache(schema, wrapItems ? [{ [itemsXmlTagName]: exampleFromRule }] : exampleFromRule); } } if (schema.items?.type) { const exampleFromSchema = getExampleFromSchema(schema.items, options, level + 1); return wrapItems ? [{ [itemsXmlTagName]: exampleFromSchema }] : [exampleFromSchema]; } return []; } const exampleValues = { string: makeUpRandomData ? guessFromFormat(schema, options?.emptyString) : '', boolean: true, integer: schema.min ?? 1, number: schema.min ?? 1, array: [], }; if (schema.type !== undefined && exampleValues[schema.type] !== undefined) { return cache(schema, exampleValues[schema.type]); } const discriminateSchema = schema.oneOf || schema.anyOf; // Check if property has the `oneOf` | `anyOf` key if (Array.isArray(discriminateSchema) && discriminateSchema.length > 0) { // Get the first item from the `oneOf` | `anyOf` array const firstOneOfItem = discriminateSchema[0]; // Return an example for the first item return getExampleFromSchema(firstOneOfItem, options, level + 1); } // Check if schema has the `allOf` key if (Array.isArray(schema.allOf)) { let example = null; // Loop through all `allOf` schemas schema.allOf.forEach((allOfItem) => { // Return an example from the schema const newExample = getExampleFromSchema(allOfItem, options, level + 1); // Merge or overwrite the example example = typeof newExample === 'object' && typeof example === 'object' ? { ...(example ?? {}), ...newExample, } : Array.isArray(newExample) && Array.isArray(example) ? [...(example ?? {}), ...newExample] : newExample; }); return cache(schema, example); } // Check if schema is a union type if (Array.isArray(schema.type)) { // Return null if the type is nullable if (schema.type.includes('null')) { return null; } // Return an example for the first type in the union const exampleValue = exampleValues[schema.type[0]]; if (exampleValue !== undefined) { return cache(schema, exampleValue); } } // Warn if the type is unknown … // console.warn(`[getExampleFromSchema] Unknown property type "${schema.type}".`) // … and just return null for now. return null; }; export { getExampleFromSchema };