Skip to content
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

Add support for URI templates #3932

Merged
merged 35 commits into from
Aug 6, 2024
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
85f6c13
Initial
timotheeguerin Jul 19, 2024
27b56f8
Basic uri template parsing
timotheeguerin Jul 19, 2024
cc97ec9
Add uri template resolution and keep existing test working
timotheeguerin Jul 22, 2024
0cb48df
Implicit extract
timotheeguerin Jul 22, 2024
451705d
Route progress
timotheeguerin Jul 22, 2024
8cd5317
Rest fix
timotheeguerin Jul 22, 2024
905f248
Fix rest
timotheeguerin Jul 22, 2024
a74b933
Merge branch 'main' into uri-templates
timotheeguerin Jul 22, 2024
8fc3238
Merge branch 'main' into uri-templates
timotheeguerin Jul 23, 2024
e9df1b8
Merge branch 'main' of https://github.com/Microsoft/typespec into uri…
timotheeguerin Jul 24, 2024
36084ec
Some doc fixes
timotheeguerin Jul 24, 2024
2565b9f
Add path parameter tests
timotheeguerin Jul 24, 2024
a392551
Create uri-templates-2024-6-24-20-7-39.md
timotheeguerin Jul 24, 2024
d8d960b
Create uri-templates-2024-6-24-21-37-52.md
timotheeguerin Jul 25, 2024
3653a35
ADd explode support to query
timotheeguerin Jul 25, 2024
0fbfa04
Merge branch 'main' into uri-templates
timotheeguerin Jul 25, 2024
e990691
regen samples
timotheeguerin Jul 25, 2024
b250d05
Update packages/http/lib/decorators.tsp
timotheeguerin Jul 25, 2024
2b1d1dc
regen
timotheeguerin Jul 26, 2024
c38a8d6
Merge branch 'main' of https://github.com/microsoft/typespec into uri…
timotheeguerin Jul 26, 2024
661a1bb
regen docs
timotheeguerin Jul 26, 2024
a981368
Fix CR comments
timotheeguerin Jul 31, 2024
ba26263
Merge branch 'uri-templates' of https://github.com/timotheeguerin/typ…
timotheeguerin Jul 31, 2024
8ef0c75
Merge branch 'main' of https://github.com/Microsoft/typespec into uri…
timotheeguerin Jul 31, 2024
9d20aa7
regen
timotheeguerin Jul 31, 2024
38b8422
keep legacy format
timotheeguerin Aug 2, 2024
6eba177
Fix
timotheeguerin Aug 5, 2024
6ae6919
Merge branch 'main' of https://github.com/Microsoft/typespec into uri…
timotheeguerin Aug 5, 2024
ca098c1
Merge branch 'main' of https://github.com/Microsoft/typespec into uri…
timotheeguerin Aug 5, 2024
6d922f7
Fix
timotheeguerin Aug 5, 2024
cc7f4f3
Merge branch 'main' of https://github.com/Microsoft/typespec into uri…
timotheeguerin Aug 5, 2024
6d950fb
add more tests
timotheeguerin Aug 6, 2024
08fc781
Fixes
timotheeguerin Aug 6, 2024
c9bd8ba
Create uri-templates-2024-7-6-16-39-59.md
timotheeguerin Aug 6, 2024
5522c81
Regen docs
timotheeguerin Aug 6, 2024
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
9 changes: 8 additions & 1 deletion packages/http/generated-defs/TypeSpec.Http.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,13 @@ import type {
Type,
} from "@typespec/compiler";

export interface PathOptions {
readonly name?: string;
readonly explode?: boolean;
readonly style?: "simple" | "label" | "matrix" | "fragment" | "path";
readonly allowReserved?: boolean;
}

/**
* Specify the status code for this response. Property type must be a status code integer or a union of status code integer.
*
Expand Down Expand Up @@ -89,7 +96,7 @@ export type QueryDecorator = (
export type PathDecorator = (
context: DecoratorContext,
target: ModelProperty,
paramName?: string
paramNameOrOptions?: string | PathOptions
) => void;

/**
Expand Down
29 changes: 28 additions & 1 deletion packages/http/lib/decorators.tsp
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,33 @@ model QueryOptions {
*/
extern dec query(target: ModelProperty, queryNameOrOptions?: string | QueryOptions);

model PathOptions {
/** Name of the parameter in the uri template. */
name?: string;

/**
* When interpolating this parameter in the case of array or object expand each value using the given style.
* Equivalent of adding `*` in the path parameter as per [RFC-6570](https://datatracker.ietf.org/doc/html/rfc6570#section-3.2.3)
*/
explode?: boolean;

/**
* Different interpolating styles for the path parameter.
* - `simple`: No special encoding.
* - `label`: Using `.` separator.
* - `matrix`: `;` as separator.
* - `fragment`: `#` as separator.
* - `path`: `/` as separator.
*/
style?: "simple" | "label" | "matrix" | "fragment" | "path";

/**
* When interpolating this parameter do not encode reserved characters.
* Equivalent of adding `+` in the path parameter as per [RFC-6570](https://datatracker.ietf.org/doc/html/rfc6570#section-3.2.3)
*/
allowReserved?: boolean;
}

/**
* Explicitly specify that this property is to be interpolated as a path parameter.
*
Expand All @@ -80,7 +107,7 @@ extern dec query(target: ModelProperty, queryNameOrOptions?: string | QueryOptio
* op read(@path explicit: string, implicit: string): void;
* ```
*/
extern dec path(target: ModelProperty, paramName?: valueof string);
extern dec path(target: ModelProperty, paramNameOrOptions?: valueof string | PathOptions);
timotheeguerin marked this conversation as resolved.
Show resolved Hide resolved

/**
* Explicitly specify that this property type will be exactly the HTTP body.
Expand Down
14 changes: 12 additions & 2 deletions packages/http/src/decorators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import {
MultipartBodyDecorator,
PatchDecorator,
PathDecorator,
PathOptions,
PostDecorator,
PutDecorator,
QueryDecorator,
Expand Down Expand Up @@ -173,11 +174,20 @@ export function isQueryParam(program: Program, entity: Type) {
export const $path: PathDecorator = (
context: DecoratorContext,
entity: ModelProperty,
paramName?: string
paramNameOrOptions?: string | PathOptions
) => {
const paramName =
typeof paramNameOrOptions === "string"
? paramNameOrOptions
: paramNameOrOptions?.name ?? entity.name;

const userOptions: PathOptions = typeof paramNameOrOptions === "object" ? paramNameOrOptions : {};
const options: PathParameterOptions = {
type: "path",
name: paramName ?? entity.name,
explode: userOptions.explode ?? false,
allowReserved: userOptions.allowReserved ?? false,
style: userOptions.style ?? "simple",
name: paramName,
};
context.program.stateMap(HttpStateKeys.path).set(entity, options);
};
Expand Down
47 changes: 40 additions & 7 deletions packages/http/src/http-property.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,9 @@ export interface BodyPropertyProperty extends HttpPropertyBase {
}

export interface GetHttpPropertyOptions {
isImplicitPathParam?: (param: ModelProperty) => boolean;
implicitParameter?: (
param: ModelProperty
) => PathParameterOptions | QueryParameterOptions | undefined;
}
/**
* Find the type of a property in a model
Expand All @@ -98,14 +100,45 @@ export function getHttpProperty(
statusCode: isStatusCode(program, property),
};
const defined = Object.entries(annotations).filter((x) => !!x[1]);
const implicit = options.implicitParameter?.(property);

if (implicit && defined.length > 0) {
if (implicit.type === "path" && annotations.path) {
if (
annotations.path.explode ||
annotations.path.style !== "simple" ||
annotations.path.allowReserved
) {
diagnostics.push(
createDiagnostic({
code: "use-uri-template",
format: {
param: property.name,
},
target: property,
})
);
}
} else if (implicit.type === "query" && annotations.query) {
} else {
diagnostics.push(
createDiagnostic({
code: "incompatible-uri-param",
format: {
param: property.name,
uriKind: implicit.type,
annotationKind: defined[0][0],
},
target: property,
})
);
}
}
if (defined.length === 0) {
if (options.isImplicitPathParam && options.isImplicitPathParam(property)) {
if (implicit) {
return createResult({
kind: "path",
options: {
name: property.name,
type: "path",
},
kind: implicit.type,
options: implicit as any,
property,
});
}
Expand Down
2 changes: 1 addition & 1 deletion packages/http/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export * from "./content-types.js";
export * from "./decorators.js";
export * from "./metadata.js";
export * from "./operations.js";
export * from "./parameters.js";
export { getOperationParameters } from "./parameters.js";
export {
HttpPart,
getHttpFileModel,
Expand Down
17 changes: 15 additions & 2 deletions packages/http/src/lib.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,25 @@ export const $lib = createTypeSpecLibrary({
default: paramMessage`HTTP verb already applied to ${"entityName"}`,
},
},
"missing-path-param": {
"missing-uri-param": {
severity: "error",
messages: {
default: paramMessage`Route reference parameter '${"param"}' but wasn't found in operation parameters`,
},
},
"incompatible-uri-param": {
severity: "error",
messages: {
default: paramMessage`Parameter '${"param"}' is defined in the uri as a ${"uriKind"} but is annotated as a ${"annotationKind"}.`,
},
},
"use-uri-template": {
severity: "error",
messages: {
default: paramMessage`Parameter '${"param"}' is already defined in the uri template. Explode, style and allowReserved property must be defined in the uri template as described by RFC 6570.`,
},
},

"optional-path-param": {
severity: "error",
messages: {
Expand Down Expand Up @@ -187,6 +200,6 @@ export const $lib = createTypeSpecLibrary({
file: { description: "State for the @Private.file decorator" },
httpPart: { description: "State for the @Private.httpPart decorator" },
},
} as const);
});

export const { reportDiagnostic, createDiagnostic, stateKeys: HttpStateKeys } = $lib;
3 changes: 2 additions & 1 deletion packages/http/src/operations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,8 @@ function getHttpOperationInternal(

const httpOperation: HttpOperation = {
path: route.path,
pathSegments: route.pathSegments,
uriTemplate: route.uriTemplate,
pathSegments: [], // TODO: ?
timotheeguerin marked this conversation as resolved.
Show resolved Hide resolved
verb: route.parameters.verb,
container: operation.interface ?? operation.namespace ?? program.getGlobalNamespaceType(),
parameters: route.parameters,
Expand Down
60 changes: 50 additions & 10 deletions packages/http/src/parameters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,16 @@ import {
HttpOperationParameters,
HttpVerb,
OperationParameterOptions,
PathParameterOptions,
QueryParameterOptions,
} from "./types.js";
import { parseUriTemplate } from "./uri-template.js";

export function getOperationParameters(
program: Program,
operation: Operation,
partialUriTemplate: string,
timotheeguerin marked this conversation as resolved.
Show resolved Hide resolved
overloadBase?: HttpOperation,
knownPathParamNames: string[] = [],
options: OperationParameterOptions = {}
): [HttpOperationParameters, readonly Diagnostic[]] {
const verb =
Expand All @@ -30,36 +33,73 @@ export function getOperationParameters(
overloadBase?.verb;

if (verb) {
return getOperationParametersForVerb(program, operation, verb, knownPathParamNames);
return getOperationParametersForVerb(program, operation, verb, partialUriTemplate);
}

// If no verb is explicitly specified, it is POST if there is a body and
// GET otherwise. Theoretically, it is possible to use @visibility
// strangely such that there is no body if the verb is POST and there is a
// body if the verb is GET. In that rare case, GET is chosen arbitrarily.
const post = getOperationParametersForVerb(program, operation, "post", knownPathParamNames);
const post = getOperationParametersForVerb(program, operation, "post", partialUriTemplate);
return post[0].body
? post
: getOperationParametersForVerb(program, operation, "get", knownPathParamNames);
: getOperationParametersForVerb(program, operation, "get", partialUriTemplate);
}

const operatorToStyle = {
";": "matrix",
"#": "fragment",
".": "label",
"/": "path",
} as const;

function getOperationParametersForVerb(
program: Program,
operation: Operation,
verb: HttpVerb,
knownPathParamNames: string[]
partialUriTemplate: string
): [HttpOperationParameters, readonly Diagnostic[]] {
const diagnostics = createDiagnosticCollector();
const visibility = resolveRequestVisibility(program, operation, verb);
function isImplicitPathParam(param: ModelProperty) {
const isTopLevel = param.model === operation.parameters;
return isTopLevel && knownPathParamNames.includes(param.name);
}
const parsedUriTemplate = parseUriTemplate(partialUriTemplate);

const parameters: HttpOperationParameter[] = [];
const { body: resolvedBody, metadata } = diagnostics.pipe(
resolveHttpPayload(program, operation.parameters, visibility, "request", {
isImplicitPathParam,
implicitParameter: (
param: ModelProperty
): QueryParameterOptions | PathParameterOptions | undefined => {
const isTopLevel = param.model === operation.parameters;
const uriParam =
isTopLevel && parsedUriTemplate.parameters.find((x) => x.name === param.name);

if (!uriParam) {
return undefined;
}

if (uriParam.operator === "?" || uriParam.operator === "&") {
return {
type: "query",
name: uriParam.name,
};
} else if (uriParam.operator === "+") {
return {
type: "path",
name: uriParam.name,
explode: uriParam.modifier?.type === "explode",
allowReserved: true,
style: "simple",
};
} else {
return {
type: "path",
name: uriParam.name,
explode: uriParam.modifier?.type === "explode",
allowReserved: false,
style: (uriParam.operator && operatorToStyle[uriParam.operator]) ?? "simple",
};
}
},
})
);

Expand Down
Loading
Loading