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

Feature: Add @encode decorator #1899

Merged
merged 11 commits into from
May 4, 2023
Merged
Show file tree
Hide file tree
Changes from 6 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/compiler",
"comment": "**Added** `@encode` decorator used to specify encoding of types",
"type": "none"
}
],
"packageName": "@typespec/compiler"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"changes": [
{
"packageName": "@typespec/openapi3",
"comment": "**Added** support for `@encode` decorator",
"type": "none"
}
],
"packageName": "@typespec/openapi3"
}
8 changes: 2 additions & 6 deletions packages/compiler/core/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,8 @@ import {
ReturnRecord,
Scalar,
ScalarStatementNode,
StdTypeName,
StdTypes,
StringLiteral,
StringLiteralNode,
Sym,
Expand Down Expand Up @@ -243,12 +245,6 @@ const TypeInstantiationMap = class
extends MultiKeyMap<readonly Type[], Type>
implements TypeInstantiationMap {};

type StdTypeName = IntrinsicScalarName | "Array" | "Record" | "object";
type StdTypes = {
// Models
Array: Model;
Record: Model;
} & Record<IntrinsicScalarName, Scalar>;
type ReflectionTypeName = keyof typeof ReflectionNameToKind;

let currentSymbolId = 0;
Expand Down
9 changes: 9 additions & 0 deletions packages/compiler/core/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -681,6 +681,15 @@ const diagnostics = {
},
},

"invalid-encode": {
severity: "error",
messages: {
default: "Invalid encoding",
wrongType: paramMessage`Encoding '${"encoding"}' cannot be used on type ${"type"}. Expected a ${"expected"}.`,
wrongEncodingType: paramMessage`Encoding '${"encoding"}' cannot be used on type ${"type"}. Expected a ${"expected"}.`,
},
},

/**
* Service
*/
Expand Down
7 changes: 7 additions & 0 deletions packages/compiler/core/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,13 @@ export type Type =
| ObjectType
| Projection;

export type StdTypes = {
// Models
Array: Model;
Record: Model;
} & Record<IntrinsicScalarName, Scalar>;
export type StdTypeName = keyof StdTypes;

export type TypeOrReturnRecord = Type | ReturnRecord;

export interface ObjectType extends BaseType {
Expand Down
111 changes: 107 additions & 4 deletions packages/compiler/lib/decorators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@ import {
validateDecoratorTargetIntrinsic,
} from "../core/decorator-utils.js";
import {
StdTypeName,
getDiscriminatedUnion,
getTypeName,
ignoreDiagnostics,
validateDecoratorUniqueOnNode,
} from "../core/index.js";
import { createDiagnostic, reportDiagnostic } from "../core/messages.js";
Expand Down Expand Up @@ -227,7 +229,7 @@ export function $pattern(
) {
validateDecoratorUniqueOnNode(context, target, $pattern);

if (!validateDecoratorTargetIntrinsic(context, target, "@pattern", "string")) {
if (!validateDecoratorTargetIntrinsic(context, target, "@pattern", ["string"])) {
return;
}

Expand All @@ -250,7 +252,7 @@ export function $minLength(
validateDecoratorUniqueOnNode(context, target, $minLength);

if (
!validateDecoratorTargetIntrinsic(context, target, "@minLength", "string") ||
!validateDecoratorTargetIntrinsic(context, target, "@minLength", ["string"]) ||
!validateRange(context, minLength, getMaxLength(context.program, target))
) {
return;
Expand All @@ -275,7 +277,7 @@ export function $maxLength(
validateDecoratorUniqueOnNode(context, target, $maxLength);

if (
!validateDecoratorTargetIntrinsic(context, target, "@maxLength", "string") ||
!validateDecoratorTargetIntrinsic(context, target, "@maxLength", ["string"]) ||
!validateRange(context, getMinLength(context.program, target), maxLength)
) {
return;
Expand Down Expand Up @@ -523,7 +525,7 @@ const secretTypesKey = createStateSymbol("secretTypes");
export function $secret(context: DecoratorContext, target: Scalar | ModelProperty) {
validateDecoratorUniqueOnNode(context, target, $secret);

if (!validateDecoratorTargetIntrinsic(context, target, "@secret", "string")) {
if (!validateDecoratorTargetIntrinsic(context, target, "@secret", ["string"])) {
return;
}
context.program.stateMap(secretTypesKey).set(target, true);
Expand All @@ -533,6 +535,107 @@ export function isSecret(program: Program, target: Type): boolean | undefined {
return program.stateMap(secretTypesKey).get(target);
}

export type DateTimeKnownEncoding = "rfc3339" | "rfc7231" | "unixTimeStamp";
export type DurationKnownEncoding = "ISO8601" | "seconds";
export type BytesKnownEncoding = "base64" | "base64url";
export interface EncodeData {
encoding: DateTimeKnownEncoding | DurationKnownEncoding | string;
type: Scalar;
}

const encodeKey = createStateSymbol("encode");
export function $encode(
context: DecoratorContext,
target: Scalar | ModelProperty,
encoding: string | EnumMember,
encodeAs?: Scalar
) {
validateDecoratorUniqueOnNode(context, target, $encode);

const encodeData: EncodeData = {
encoding: typeof encoding === "string" ? encoding : encoding.value?.toString() ?? encoding.name,
type: encodeAs ?? context.program.checker.getStdType("string"),
};
const targetType = getPropertyType(target);
if (targetType.kind !== "Scalar") {
return;
}
validateEncodeData(context, targetType, encodeData);
context.program.stateMap(encodeKey).set(target, encodeData);
}

function validateEncodeData(context: DecoratorContext, target: Scalar, encodeData: EncodeData) {
function check(validTargets: StdTypeName[], validEncodeTypes: StdTypeName[]) {
const checker = context.program.checker;
const isTargetValid = validTargets.some((validTarget) => {
return ignoreDiagnostics(
checker.isTypeAssignableTo(
target.projectionBase ?? target,
checker.getStdType(validTarget),
target
)
);
});

if (!isTargetValid) {
reportDiagnostic(context.program, {
code: "invalid-encode",
messageId: "wrongType",
format: {
encoding: encodeData.encoding,
type: getTypeName(target),
expected: validTargets.join(", "),
},
target: context.decoratorTarget,
});
}
const isEncodingTypeValid = validEncodeTypes.some((validEncoding) => {
return ignoreDiagnostics(
checker.isTypeAssignableTo(
encodeData.type.projectionBase ?? encodeData.type,
checker.getStdType(validEncoding),
target
)
);
});

if (!isEncodingTypeValid) {
reportDiagnostic(context.program, {
code: "invalid-encode",
messageId: "wrongEncodingType",
format: {
encoding: encodeData.encoding,
type: getTypeName(target),
expected: validEncodeTypes.join(", "),
},
target: context.decoratorTarget,
});
}
}

switch (encodeData.encoding) {
case "rfc3339":
return check(["utcDateTime", "offsetDateTime"], ["string"]);
case "rfc7231":
return check(["utcDateTime", "offsetDateTime"], ["string"]);
case "unixTimeStamp":
return check(["utcDateTime"], ["string"]);
case "seconds":
return check(["duration"], ["numeric"]);
case "base64":
return check(["bytes"], ["string"]);
case "base64url":
return check(["bytes"], ["string"]);
}
}

export function getEncode(
program: Program,
target: Scalar | ModelProperty
): EncodeData | undefined {
return program.stateMap(encodeKey).get(target);
}

// -- @visibility decorator ---------------------

const visibilitySettingsKey = createStateSymbol("visibilitySettings");
Expand Down
59 changes: 59 additions & 0 deletions packages/compiler/lib/decorators.tsp
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,65 @@ extern dec projectedName(target: unknown, targetName: string, projectedName: str
*/
extern dec discriminator(target: Model | Union, propertyName: string);

/**
* Known encoding to use on utcDateTime or offsetDateTime
*/
enum DateTimeKnownEncoding {
/**
* RFC 3339 standard. https://www.ietf.org/rfc/rfc3339.txt
* Encode to string.
*/
rfc3339: "rfc3339",
/**
* RFC 7231 standard. https://www.ietf.org/rfc/rfc7231.txt
* Encode to string.
*/
rfc7231: "rfc7231",
/**
* Encode to integer
*/
unixTimestamp: "unixTimestamp",
}

/**
* Known encoding to use on duration
*/
enum DurationKnownEncoding {
/**
* ISO8601 duration
*/
ISO8601: "ISO8601",
/**
* Encode to integer or float
*/
seconds: "seconds",
}

/**
* Known encoding to use on bytes
*/
enum BytesKnownEncoding {
/**
* ISO8601 duration
*/
ISO8601: "ISO8601",
/**
* Encode to integer or float
*/
seconds: "seconds",
}

/**
* Specify how to encode the target type.
* @param encoding Known name of an encoding.
* @param encodedAs What target type is this being encoded as. Default to string.
*/
extern dec encode(
target: Scalar | ModelProperty,
encoding: string | EnumMember,
encodedAs?: string | numeric
);

/**
* Indicates that a property is only considered to be present or applicable ("visible") with
* the in the given named contexts ("visibilities"). When a property has no visibilities applied
Expand Down
Loading