Skip to content

Commit

Permalink
Check unallow keys in @info decorator (#4505)
Browse files Browse the repository at this point in the history
Fix #3887

---------

Co-authored-by: Kyle Zhang <[email protected]>
Co-authored-by: Timothee Guerin <[email protected]>
  • Loading branch information
3 people authored Sep 30, 2024
1 parent f442b75 commit e9be391
Show file tree
Hide file tree
Showing 4 changed files with 107 additions and 2 deletions.
7 changes: 7 additions & 0 deletions .chronus/changes/InfoUnallowKeyChecker-2024-8-24-11-6-11.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
changeKind: fix
packages:
- "@typespec/openapi"
---

`@info` decorator validate no extra properties not starting with `x-` are provided.
55 changes: 54 additions & 1 deletion packages/openapi/src/decorators.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import {
compilerAssert,
DecoratorContext,
Diagnostic,
DiagnosticTarget,
getDoc,
getProperty,
getService,
getSummary,
Model,
Expand All @@ -19,7 +23,7 @@ import {
InfoDecorator,
OperationIdDecorator,
} from "../generated-defs/TypeSpec.OpenAPI.js";
import { createStateSymbol, reportDiagnostic } from "./lib.js";
import { createDiagnostic, createStateSymbol, reportDiagnostic } from "./lib.js";
import { AdditionalInfo, ExtensionKey, ExternalDocs } from "./types.js";

const operationIdsKey = createStateSymbol("operationIds");
Expand Down Expand Up @@ -185,6 +189,7 @@ export const $info: InfoDecorator = (
if (data === undefined) {
return;
}
validateAdditionalInfoModel(context, model);
if (data.termsOfService) {
if (!validateIsUri(context, data.termsOfService, "TermsOfService")) {
return;
Expand Down Expand Up @@ -233,3 +238,51 @@ function validateIsUri(context: DecoratorContext, url: string, propertyName: str
return false;
}
}

function validateAdditionalInfoModel(context: DecoratorContext, typespecType: TypeSpecValue) {
const propertyModel = context.program.resolveTypeReference(
"TypeSpec.OpenAPI.AdditionalInfo",
)[0]! as Model;

if (typeof typespecType === "object" && propertyModel) {
const diagnostics = checkNoAdditionalProperties(
typespecType,
context.getArgumentTarget(0)!,
propertyModel,
);
context.program.reportDiagnostics(diagnostics);
}
}

function checkNoAdditionalProperties(
typespecType: Type,
target: DiagnosticTarget,
source: Model,
): Diagnostic[] {
const diagnostics: Diagnostic[] = [];
compilerAssert(typespecType.kind === "Model", "Expected type to be a Model.");

for (const [name, type] of typespecType.properties.entries()) {
const sourceProperty = getProperty(source, name);
if (sourceProperty) {
if (sourceProperty.type.kind === "Model") {
const nestedDiagnostics = checkNoAdditionalProperties(
type.type,
target,
sourceProperty.type,
);
diagnostics.push(...nestedDiagnostics);
}
} else if (!isOpenAPIExtensionKey(name)) {
diagnostics.push(
createDiagnostic({
code: "invalid-extension-key",
format: { value: name },
target,
}),
);
}
}

return diagnostics;
}
2 changes: 1 addition & 1 deletion packages/openapi/src/lib.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,4 @@ export const $lib = createTypeSpecLibrary({
},
});

export const { reportDiagnostic, createStateSymbol } = $lib;
export const { createDiagnostic, reportDiagnostic, createStateSymbol } = $lib;
45 changes: 45 additions & 0 deletions packages/openapi/test/decorators.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,51 @@ describe("openapi: decorators", () => {
});

describe("@info", () => {
describe("emit diagnostics when passing extension key not starting with `x-` in additionalInfo", () => {
it.each([
["root", `{ foo:"Bar" }`],
["license", `{ license:{ name: "Apache 2.0", foo:"Bar"} }`],
["contact", `{ contact:{ foo:"Bar"} }`],
["complex", `{ contact:{ "x-custom": "string" }, foo:"Bar" }`],
])("%s", async (_, code) => {
const diagnostics = await runner.diagnose(`
@info(${code})
@test namespace Service;
`);

expectDiagnostics(diagnostics, {
code: "@typespec/openapi/invalid-extension-key",
message: `OpenAPI extension must start with 'x-' but was 'foo'`,
});
});

it("multiple", async () => {
const diagnostics = await runner.diagnose(`
@info({
license:{ name: "Apache 2.0", foo1:"Bar"},
contact:{ "x-custom": "string", foo2:"Bar" },
foo3:"Bar"
})
@test namespace Service;
`);

expectDiagnostics(diagnostics, [
{
code: "@typespec/openapi/invalid-extension-key",
message: `OpenAPI extension must start with 'x-' but was 'foo1'`,
},
{
code: "@typespec/openapi/invalid-extension-key",
message: `OpenAPI extension must start with 'x-' but was 'foo2'`,
},
{
code: "@typespec/openapi/invalid-extension-key",
message: `OpenAPI extension must start with 'x-' but was 'foo3'`,
},
]);
});
});

it("emit diagnostic if termsOfService is not a valid url", async () => {
const diagnostics = await runner.diagnose(`
@info({termsOfService:"notvalidurl"})
Expand Down

0 comments on commit e9be391

Please sign in to comment.