Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"changes": [
{
"packageName": "@typespec/http",
"comment": "Add validation step to check whether any operation references another operation with a route prefix defined on a parent container. This helps avoid unexpected route changes when using operation references.",
"type": "none"
}
],
"packageName": "@typespec/http"
}
77 changes: 33 additions & 44 deletions packages/http/src/decorators.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { createDiagnostic, reportDiagnostic } from "./lib.js";

import {
DecoratorContext,
Diagnostic,
Expand All @@ -20,8 +22,8 @@ import {
validateDecoratorTarget,
validateDecoratorUniqueOnNode,
} from "@typespec/compiler";
import { createDiagnostic, createStateSymbol, reportDiagnostic } from "./lib.js";
import { setRoute, setSharedRoute } from "./route.js";
import { HttpStateKeys } from "./state.js";
import {
AuthenticationOption,
HeaderFieldOptions,
Expand All @@ -35,7 +37,6 @@ import { extractParamsFromPath } from "./utils.js";

export const namespace = "TypeSpec.Http";

const headerFieldsKey = createStateSymbol("header");
export function $header(
context: DecoratorContext,
entity: ModelProperty,
Expand Down Expand Up @@ -80,22 +81,21 @@ export function $header(
target: context.decoratorTarget,
});
}
context.program.stateMap(headerFieldsKey).set(entity, options);
context.program.stateMap(HttpStateKeys.headerFieldsKey).set(entity, options);
}

export function getHeaderFieldOptions(program: Program, entity: Type): HeaderFieldOptions {
return program.stateMap(headerFieldsKey).get(entity);
return program.stateMap(HttpStateKeys.headerFieldsKey).get(entity);
}

export function getHeaderFieldName(program: Program, entity: Type): string {
return getHeaderFieldOptions(program, entity)?.name;
}

export function isHeader(program: Program, entity: Type) {
return program.stateMap(headerFieldsKey).has(entity);
return program.stateMap(HttpStateKeys.headerFieldsKey).has(entity);
}

const queryFieldsKey = createStateSymbol("query");
export function $query(
context: DecoratorContext,
entity: ModelProperty,
Expand Down Expand Up @@ -129,54 +129,51 @@ export function $query(
target: context.decoratorTarget,
});
}
context.program.stateMap(queryFieldsKey).set(entity, options);
context.program.stateMap(HttpStateKeys.queryFieldsKey).set(entity, options);
}

export function getQueryParamOptions(program: Program, entity: Type): QueryParameterOptions {
return program.stateMap(queryFieldsKey).get(entity);
return program.stateMap(HttpStateKeys.queryFieldsKey).get(entity);
}

export function getQueryParamName(program: Program, entity: Type): string {
return getQueryParamOptions(program, entity)?.name;
}

export function isQueryParam(program: Program, entity: Type) {
return program.stateMap(queryFieldsKey).has(entity);
return program.stateMap(HttpStateKeys.queryFieldsKey).has(entity);
}

const pathFieldsKey = createStateSymbol("path");
export function $path(context: DecoratorContext, entity: ModelProperty, paramName?: string) {
const options: PathParameterOptions = {
type: "path",
name: paramName ?? entity.name,
};
context.program.stateMap(pathFieldsKey).set(entity, options);
context.program.stateMap(HttpStateKeys.pathFieldsKey).set(entity, options);
}

export function getPathParamOptions(program: Program, entity: Type): PathParameterOptions {
return program.stateMap(pathFieldsKey).get(entity);
return program.stateMap(HttpStateKeys.pathFieldsKey).get(entity);
}

export function getPathParamName(program: Program, entity: Type): string {
return getPathParamOptions(program, entity)?.name;
}

export function isPathParam(program: Program, entity: Type) {
return program.stateMap(pathFieldsKey).has(entity);
return program.stateMap(HttpStateKeys.pathFieldsKey).has(entity);
}

const bodyFieldsKey = createStateSymbol("body");
export function $body(context: DecoratorContext, entity: ModelProperty) {
context.program.stateSet(bodyFieldsKey).add(entity);
context.program.stateSet(HttpStateKeys.bodyFieldsKey).add(entity);
}

export function isBody(program: Program, entity: Type): boolean {
return program.stateSet(bodyFieldsKey).has(entity);
return program.stateSet(HttpStateKeys.bodyFieldsKey).has(entity);
}

const statusCodeKey = createStateSymbol("statusCode");
export function $statusCode(context: DecoratorContext, entity: ModelProperty) {
context.program.stateSet(statusCodeKey).add(entity);
context.program.stateSet(HttpStateKeys.statusCodeKey).add(entity);

const codes: string[] = [];
if (entity.type.kind === "String") {
Expand Down Expand Up @@ -217,7 +214,7 @@ export function $statusCode(context: DecoratorContext, entity: ModelProperty) {
}

export function setStatusCode(program: Program, entity: Model | ModelProperty, codes: string[]) {
program.stateMap(statusCodeKey).set(entity, codes);
program.stateMap(HttpStateKeys.statusCodeKey).set(entity, codes);
}

// Check status code value: 3 digits with first digit in [1-5]
Expand All @@ -236,11 +233,11 @@ function validStatusCode(program: Program, code: string, entity: Type): boolean
}

export function isStatusCode(program: Program, entity: Type) {
return program.stateMap(statusCodeKey).has(entity);
return program.stateMap(HttpStateKeys.statusCodeKey).has(entity);
}

export function getStatusCodes(program: Program, entity: Type): string[] {
return program.stateMap(statusCodeKey).get(entity) ?? [];
return program.stateMap(HttpStateKeys.statusCodeKey).get(entity) ?? [];
}

// Reference: https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html
Expand Down Expand Up @@ -291,12 +288,10 @@ export function getStatusCodeDescription(statusCode: string) {
return undefined;
}

const operationVerbsKey = createStateSymbol("verbs");

function setOperationVerb(program: Program, entity: Type, verb: HttpVerb): void {
if (entity.kind === "Operation") {
if (!program.stateMap(operationVerbsKey).has(entity)) {
program.stateMap(operationVerbsKey).set(entity, verb);
if (!program.stateMap(HttpStateKeys.operationVerbsKey).has(entity)) {
program.stateMap(HttpStateKeys.operationVerbsKey).set(entity, verb);
} else {
reportDiagnostic(program, {
code: "http-verb-duplicate",
Expand All @@ -314,7 +309,7 @@ function setOperationVerb(program: Program, entity: Type, verb: HttpVerb): void
}

export function getOperationVerb(program: Program, entity: Type): HttpVerb | undefined {
return program.stateMap(operationVerbsKey).get(entity);
return program.stateMap(HttpStateKeys.operationVerbsKey).get(entity);
}

export function $get(context: DecoratorContext, entity: Operation) {
Expand Down Expand Up @@ -347,7 +342,6 @@ export interface HttpServer {
parameters: Map<string, ModelProperty>;
}

const serversKey = createStateSymbol("servers");
/**
* Configure the server url for the service.
* @param context Decorator context
Expand Down Expand Up @@ -376,10 +370,10 @@ export function $server(
}
}

let servers: HttpServer[] = context.program.stateMap(serversKey).get(target);
let servers: HttpServer[] = context.program.stateMap(HttpStateKeys.serversKey).get(target);
if (servers === undefined) {
servers = [];
context.program.stateMap(serversKey).set(target, servers);
context.program.stateMap(HttpStateKeys.serversKey).set(target, servers);
}
servers.push({
url,
Expand All @@ -389,19 +383,19 @@ export function $server(
}

export function getServers(program: Program, type: Namespace): HttpServer[] | undefined {
return program.stateMap(serversKey).get(type);
return program.stateMap(HttpStateKeys.serversKey).get(type);
}

export function $plainData(context: DecoratorContext, entity: Model) {
const { program } = context;

const decoratorsToRemove = ["$header", "$body", "$query", "$path", "$statusCode"];
const [headers, bodies, queries, paths, statusCodes] = [
program.stateMap(headerFieldsKey),
program.stateSet(bodyFieldsKey),
program.stateMap(queryFieldsKey),
program.stateMap(pathFieldsKey),
program.stateMap(statusCodeKey),
program.stateMap(HttpStateKeys.headerFieldsKey),
program.stateSet(HttpStateKeys.bodyFieldsKey),
program.stateMap(HttpStateKeys.queryFieldsKey),
program.stateMap(HttpStateKeys.pathFieldsKey),
program.stateMap(HttpStateKeys.statusCodeKey),
];

for (const property of entity.properties.values()) {
Expand All @@ -422,7 +416,6 @@ export function $plainData(context: DecoratorContext, entity: Model) {

setTypeSpecNamespace("Private", $plainData);

const authenticationKey = createStateSymbol("authentication");
export function $useAuth(
context: DecoratorContext,
serviceNamespace: Namespace,
Expand All @@ -440,7 +433,7 @@ export function setAuthentication(
serviceNamespace: Namespace,
auth: ServiceAuthentication
) {
program.stateMap(authenticationKey).set(serviceNamespace, auth);
program.stateMap(HttpStateKeys.authenticationKey).set(serviceNamespace, auth);
}

function extractServiceAuthentication(
Expand Down Expand Up @@ -573,7 +566,7 @@ export function getAuthentication(
program: Program,
namespace: Namespace
): ServiceAuthentication | undefined {
return program.stateMap(authenticationKey).get(namespace);
return program.stateMap(HttpStateKeys.authenticationKey).get(namespace);
}

/**
Expand Down Expand Up @@ -622,10 +615,6 @@ export function $sharedRoute(context: DecoratorContext, entity: Operation) {
setSharedRoute(context.program, entity);
}

const includeInapplicableMetadataInPayloadKey = createStateSymbol(
"includeInapplicableMetadataInPayload"
);

/**
* Specifies if inapplicable metadata should be included in the payload for
* the given entity. This is true by default unless changed by this
Expand Down Expand Up @@ -655,7 +644,7 @@ export function $includeInapplicableMetadataInPayload(
) {
return;
}
const state = context.program.stateMap(includeInapplicableMetadataInPayloadKey);
const state = context.program.stateMap(HttpStateKeys.includeInapplicableMetadataInPayloadKey);
state.set(entity, value);
}

Expand All @@ -672,7 +661,7 @@ export function includeInapplicableMetadataInPayload(
): boolean {
let e: ModelProperty | Namespace | Model | undefined;
for (e = property; e; e = e.kind === "ModelProperty" ? e.model : e.namespace) {
const value = program.stateMap(includeInapplicableMetadataInPayloadKey).get(e);
const value = program.stateMap(HttpStateKeys.includeInapplicableMetadataInPayloadKey).get(e);
if (value !== undefined) {
return value;
}
Expand Down
2 changes: 1 addition & 1 deletion packages/http/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export const namespace = "TypeSpec.Http";
export { $lib } from "./lib.js";

export * from "./content-types.js";
export * from "./decorators.js";
Expand Down
20 changes: 14 additions & 6 deletions packages/http/src/lib.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { createTypeSpecLibrary, paramMessage } from "@typespec/compiler";
import { opReferenceContainerRouteRule } from "./rules/op-reference-container-route.js";

const libDefinition = {
export const $lib = createTypeSpecLibrary({
name: "@typespec/http",
diagnostics: {
"http-verb-duplicate": {
Expand Down Expand Up @@ -125,9 +126,16 @@ const libDefinition = {
},
},
},
} as const;

const httpLib = createTypeSpecLibrary(libDefinition);
const { reportDiagnostic, createDiagnostic, createStateSymbol } = httpLib;
linter: {
rules: [opReferenceContainerRouteRule],
ruleSets: {
all: {
enable: {
[`@typespec/http/${opReferenceContainerRouteRule.name}`]: true,
},
},
},
},
});

export { createDiagnostic, createStateSymbol, httpLib, reportDiagnostic };
export const { reportDiagnostic, createDiagnostic, createStateSymbol } = $lib;
Loading