-
Notifications
You must be signed in to change notification settings - Fork 334
Encode OpenAPI refs and remove concept of "ref-safe" names #463
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
22a252f
1a46b6c
b7a6e08
168fee4
7af98af
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| { | ||
| "changes": [ | ||
| { | ||
| "packageName": "@cadl-lang/compiler", | ||
| "comment": "Add option to Checker.getTypeName to filter namespaces", | ||
| "type": "minor" | ||
| } | ||
| ], | ||
| "packageName": "@cadl-lang/compiler" | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| { | ||
| "changes": [ | ||
| { | ||
| "packageName": "@cadl-lang/openapi", | ||
| "comment": "Add shared helpers for OpenAPI 2 and 3 emit", | ||
| "type": "minor" | ||
| } | ||
| ], | ||
| "packageName": "@cadl-lang/openapi" | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| { | ||
| "changes": [ | ||
| { | ||
| "packageName": "@cadl-lang/openapi3", | ||
| "comment": "URI-encode refs", | ||
| "type": "patch" | ||
| } | ||
| ], | ||
| "packageName": "@cadl-lang/openapi3" | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,146 @@ | ||
| import { | ||
| getFriendlyName as getAssignedFriendlyName, | ||
| ModelType, | ||
| ModelTypeProperty, | ||
| Program, | ||
| Type, | ||
| TypeNameOptions, | ||
| } from "@cadl-lang/compiler"; | ||
| import { 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: Program, type: Type): boolean { | ||
| if (hasFriendlyName(program, type)) { | ||
| return false; | ||
| } | ||
|
|
||
| switch (type.kind) { | ||
| case "Model": | ||
| return !type.name || hasTemplateArguments(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 Cadl-native name written to `x-cadl-name`. | ||
| * | ||
| * For non-inlined types: this is either the friendly name or the Cadl-native name. | ||
| * | ||
| * Cadl-native names are shortened to exclude root `Cadl` namespace and service | ||
| * namespace using the provided `TypeNameOptions`. | ||
| */ | ||
| export function getTypeName( | ||
| program: Program, | ||
| type: Type, | ||
| options: TypeNameOptions, | ||
| existing?: Record<string, any> | ||
| ): string { | ||
| const name = | ||
| getFriendlyName(program, type, options) ?? program.checker!.getTypeName(type, options); | ||
|
|
||
| if (existing && existing[name]) { | ||
| reportDiagnostic(program, { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should we report both instances conflicting. That's what we have for duplicate-symbol or duplicate routes.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We should. It will take some more refactoring to do the bookkeeping. If you don't mind, I'd rather add this to the list to review on #464 because it will be easier to review more refactoring separately, I think.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We weren't handling the collisions at all before so this is a strict improvement already.
nguerrera marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| code: "duplicate-type-name", | ||
| format: { | ||
| value: name, | ||
| }, | ||
| target: type, | ||
| }); | ||
| } | ||
|
|
||
| return name; | ||
| } | ||
|
|
||
| /** | ||
| * Gets the key that is used to define a parameter in OpenAPI. | ||
| */ | ||
| export function getParameterKey( | ||
| program: Program, | ||
| propery: ModelTypeProperty, | ||
| newParam: unknown, | ||
| existingParams: Record<string, unknown>, | ||
| options: TypeNameOptions | ||
| ): string { | ||
| const parent = propery.model!; | ||
| let key = getTypeName(program, parent, options); | ||
|
|
||
| if (parent.properties.size > 1) { | ||
| key += `.${propery.name}`; | ||
| } | ||
|
|
||
| // JSON check is workaround for https://github.com/microsoft/cadl/issues/462 | ||
| if (existingParams[key] && JSON.stringify(newParam) !== JSON.stringify(existingParams[key])) { | ||
| reportDiagnostic(program, { | ||
| code: "duplicate-type-name", | ||
| messageId: "parameter", | ||
| format: { | ||
| value: key, | ||
| }, | ||
| target: propery, | ||
| }); | ||
| } | ||
|
|
||
| return key; | ||
| } | ||
|
|
||
| function hasTemplateArguments(type: Type): type is ModelType & { templateArguments: Type[] } { | ||
| return type.kind === "Model" && !!type.templateArguments && type.templateArguments.length > 0; | ||
| } | ||
|
|
||
| function hasFriendlyName(program: Program, type: Type): boolean { | ||
| return !!getAssignedFriendlyName(program, type) || hasDefaultFriendlyName(program, type); | ||
| } | ||
|
|
||
| function getFriendlyName(program: Program, type: Type, options: TypeNameOptions): string { | ||
| return getAssignedFriendlyName(program, type) ?? getDefaultFriendlyName(program, type, options); | ||
| } | ||
|
|
||
| /** | ||
| * A template instantiation has a default friendly name if none if its type | ||
| * arguments are nested template instantiations or inlined types. | ||
| */ | ||
| function hasDefaultFriendlyName( | ||
| program: Program, | ||
| type: Type | ||
| ): type is ModelType & { name: string; templateArguments: Type[] } { | ||
| return ( | ||
| type.kind === "Model" && | ||
| !!type.name && | ||
| hasTemplateArguments(type) && | ||
| !type.templateArguments.some((arg) => hasTemplateArguments(arg) || shouldInline(program, arg)) | ||
| ); | ||
| } | ||
|
|
||
| /** | ||
| * Gets the default friendly name of the form Type_Arg1_..._ArgN when applicable as described | ||
| * by `hasDefaultFriendlyName`. Returns undefined when not applicable. | ||
| */ | ||
| function getDefaultFriendlyName( | ||
| program: Program, | ||
| type: Type, | ||
| options: TypeNameOptions | ||
| ): string | undefined { | ||
| if (!hasDefaultFriendlyName(program, type)) { | ||
| return undefined; | ||
| } | ||
| const ns = program.checker!.getNamespaceString(type.namespace, options); | ||
| const model = (ns ? ns + "." : "") + type.name; | ||
| const args = type.templateArguments.map((arg) => getTypeName(program, arg, options)); | ||
| return `${model}_${args.join("_")}`; | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1 +1,2 @@ | ||
| export * from "./decorators.js"; | ||
| export * from "./helpers.js"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not really in the scope of this PR but couldn't
getTypeNamebe moved out of the checker. Doesn't feel like they depend on it.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good call, I can look at doing that as part of some other work I'm doing nearby next.