@typespec/openapi
Version:
TypeSpec library providing OpenAPI concepts
209 lines • 7.91 kB
JavaScript
import { getFriendlyName, getLifecycleVisibilityEnum, getProperty, getTypeName, getVisibilityForClass, isGlobalNamespace, isService, isTemplateInstance, } from "@typespec/compiler";
import { getOperationId } from "./decorators.js";
import { createDiagnostic, reportDiagnostic } from "./lib.js";
/**
* Determines whether a type will be inlined in OpenAPI rather than defined
* as a schema and referenced.
*
* All anonymous types (anonymous models, arrays, tuples, etc.) are inlined.
*
* Template instantiations are inlined unless they have a friendly name.
*
* A friendly name can be provided by the user using `@friendlyName`
* decorator, or chosen by default in simple cases.
*/
export function shouldInline(program, type) {
if (getFriendlyName(program, type)) {
return false;
}
switch (type.kind) {
case "Model":
return !type.name || isTemplateInstance(type);
case "Scalar":
return program.checker.isStdType(type) || isTemplateInstance(type);
case "Enum":
case "Union":
return !type.name;
default:
return true;
}
}
/**
* Gets the name of a type to be used in OpenAPI.
*
* For inlined types: this is the TypeSpec-native name written to `x-typespec-name`.
*
* For non-inlined types: this is either the friendly name or the TypeSpec-native name.
*
* TypeSpec-native names are shortened to exclude root `TypeSpec` namespace and service
* namespace using the provided `TypeNameOptions`.
*/
export function getOpenAPITypeName(program, type, options, existing) {
const name = getFriendlyName(program, type) ?? getTypeName(type, options);
checkDuplicateTypeName(program, type, name, existing);
return name;
}
/**
* Check the given name is not already specific in the existing map. Report a diagnostic if it is.
* @param program Program
* @param type Type with the name to check
* @param name Name to check
* @param existing Existing map of name
*/
export function checkDuplicateTypeName(program, type, name, existing) {
if (existing && existing[name]) {
reportDiagnostic(program, {
code: "duplicate-type-name",
format: {
value: name,
},
target: type,
});
}
}
/**
* Gets the key that is used to define a parameter in OpenAPI.
*/
export function getParameterKey(program, property, newParam, existingParams, options) {
const parent = property.model;
let key = getOpenAPITypeName(program, parent, options);
if (parent.properties.size > 1) {
key += `.${property.name}`;
}
if (existingParams[key]) {
reportDiagnostic(program, {
code: "duplicate-type-name",
messageId: "parameter",
format: {
value: key,
},
target: property,
});
}
return key;
}
/**
* Resolve the OpenAPI operation ID for the given operation using the following logic:
* - If `@operationId` was specified use that value
* - If operation is defined at the root or under the service namespace return `<operation.name>`
* - Otherwise(operation is under another namespace or interface) return `<namespace/interface.name>_<operation.name>`
*
* @param program TypeSpec Program
* @param operation Operation
* @returns Operation ID in this format `<name>` or `<group>_<name>`
*/
export function resolveOperationId(program, operation) {
const explicitOperationId = getOperationId(program, operation);
if (explicitOperationId) {
return explicitOperationId;
}
if (operation.interface) {
return `${operation.interface.name}_${operation.name}`;
}
const namespace = operation.namespace;
if (namespace === undefined ||
isGlobalNamespace(program, namespace) ||
isService(program, namespace)) {
return operation.name;
}
return `${namespace.name}_${operation.name}`;
}
/**
* Determines if a property is read-only, which is defined as having the
* only the `Lifecycle.Read` modifier.
*
* If there is more than one Lifecycle visibility modifier active on the property,
* then the property is not read-only. For example, `@visibility(Lifecycle.Read, Lifecycle.Update)`
* does not designate a read-only property.
*/
export function isReadonlyProperty(program, property) {
const Lifecycle = getLifecycleVisibilityEnum(program);
const visibility = getVisibilityForClass(program, property, getLifecycleVisibilityEnum(program));
// note: multiple visibilities that include read are not handled using
// readonly: true, but using separate schemas.
return visibility.size === 1 && visibility.has(Lifecycle.members.get("Read"));
}
/**
* Determines if a OpenAPIExtensionKey is start with `x-`.
*/
export function isOpenAPIExtensionKey(key) {
return key.startsWith("x-");
}
/**
* Validate that the given string is a valid URL.
* @param program Program
* @param target Diagnostic target for any diagnostics that are reported
* @param url The URL to validate
* @param propertyName The name of the property that the URL is associated with
* @returns true if the URL is valid, false otherwise
*/
export function validateIsUri(program, target, url, propertyName) {
try {
// Attempt to create a URL object from the given string. If
// successful, the URL is valid.
new URL(url);
return true;
}
catch {
// If the URL is invalid, report a diagnostic with the given
// target, property name and value.
reportDiagnostic(program, {
code: "not-url",
target: target,
format: { property: propertyName, value: url },
});
return false;
}
}
/**
* Validate the AdditionalInfo model against a reference.
*
* This function checks that the properties of the given AdditionalInfo object
* are a subset of the properties defined in the AdditionalInfo model.
*
* @param program - The TypeSpec Program instance
* @param target - Diagnostic target for reporting any diagnostics
* @param jsonObject - The AdditionalInfo object to validate
* @param reference - The reference string to resolve the model
* @returns true if the AdditionalInfo object is valid, false otherwise
*/
export function validateAdditionalInfoModel(program, target, jsonObject, reference) {
// Resolve the reference to get the corresponding model
const propertyModel = program.resolveTypeReference(reference)[0];
// Check if jsonObject and propertyModel are defined
if (jsonObject && propertyModel) {
// Validate that the properties of typespecType do not exceed those in propertyModel
const diagnostics = checkNoAdditionalProperties(jsonObject, target, propertyModel);
program.reportDiagnostics(diagnostics);
// Return false if any diagnostics were reported, indicating a validation failure
if (diagnostics.length > 0) {
return false;
}
}
// Return true if validation is successful
return true;
}
/**
* Check Additional Properties
*/
function checkNoAdditionalProperties(jsonObject, target, source) {
const diagnostics = [];
for (const name of Object.keys(jsonObject)) {
const sourceProperty = getProperty(source, name);
if (sourceProperty) {
if (sourceProperty.type.kind === "Model") {
const nestedDiagnostics = checkNoAdditionalProperties(jsonObject[name], target, sourceProperty.type);
diagnostics.push(...nestedDiagnostics);
}
}
else if (!isOpenAPIExtensionKey(name)) {
diagnostics.push(createDiagnostic({
code: "invalid-extension-key",
format: { value: name },
target,
}));
}
}
return diagnostics;
}
//# sourceMappingURL=helpers.js.map