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/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/html-program-viewer",
"comment": "Fix issue with showing enum members",
"type": "none"
}
],
"packageName": "@typespec/html-program-viewer"
}
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"
}
19 changes: 19 additions & 0 deletions docs/standard-library/built-in-decorators.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,25 @@ dec doc(target: unknown, doc: string, formatArgs?: object)
| formatArgs | `model object` | Record with key value pair that can be interpolated in the doc. |


### `@encode` {#@encode}

Specify how to encode the target type.

```typespec
dec encode(target: Scalar | ModelProperty, encoding: string | EnumMember, encodedAs?: string | numeric)
```

#### Target

`union Scalar | ModelProperty`

#### Parameters
| Name | Type | Description |
|------|------|-------------|
| encoding | `union string \| EnumMember` | Known name of an encoding. |
| encodedAs | `union string \| numeric` | What target type is this being encoded as. Default to string. |


### `@error` {#@error}

Specify that this model is an error type. Operations return error types when the operation has failed.
Expand Down
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
16 changes: 15 additions & 1 deletion packages/compiler/core/decorator-utils.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { getPropertyType } from "../lib/decorators.js";
import { getTypeName } from "./helpers/type-name-utils.js";
import { compilerAssert, Interface, Model, SyntaxKind } from "./index.js";
import { compilerAssert, ignoreDiagnostics, Interface, Model, SyntaxKind } from "./index.js";
import { createDiagnostic, reportDiagnostic } from "./messages.js";
import { Program } from "./program.js";
import {
Expand Down Expand Up @@ -61,6 +61,20 @@ export function validateDecoratorTarget<K extends TypeKind>(
return true;
}

export function isIntrinsicType(
program: Program,
type: Scalar,
kind: IntrinsicScalarName
): boolean {
return ignoreDiagnostics(
program.checker.isTypeAssignableTo(
type.projectionBase ?? type,
program.checker.getStdType(kind),
type
)
);
}

export function validateDecoratorTargetIntrinsic(
context: DecoratorContext,
target: Scalar | ModelProperty,
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
121 changes: 117 additions & 4 deletions packages/compiler/lib/decorators.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
import {
isIntrinsicType,
validateDecoratorNotOnType,
validateDecoratorTarget,
validateDecoratorTargetIntrinsic,
} from "../core/decorator-utils.js";
import {
StdTypeName,
getDiscriminatedUnion,
getTypeName,
ignoreDiagnostics,
reportDeprecated,
validateDecoratorUniqueOnNode,
} from "../core/index.js";
import { createDiagnostic, reportDiagnostic } from "../core/messages.js";
Expand Down Expand Up @@ -208,6 +212,14 @@ export function $format(context: DecoratorContext, target: Scalar | ModelPropert
if (!validateDecoratorTargetIntrinsic(context, target, "@format", ["string", "bytes"])) {
return;
}
const targetType = getPropertyType(target);
if (targetType.kind === "Scalar" && isIntrinsicType(context.program, targetType, "bytes")) {
reportDeprecated(
context.program,
"Using `@format` on a bytes scalar is deprecated. Use `@encode` instead. https://github.com/microsoft/typespec/issues/1873",
target
);
}

context.program.stateMap(formatValuesKey).set(target, format);
}
Expand All @@ -227,7 +239,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 +262,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 +287,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 +535,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 +545,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
73 changes: 73 additions & 0 deletions packages/compiler/lib/decorators.tsp
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,79 @@ 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.
*
* @example offsetDateTime encoded with rfc7231
*
* ```tsp
* @encode("rfc7231")
* scalar myDateTime extends offsetDateTime;
* ```
*
* @example utcDateTime encoded with unixTimestamp
*
* ```tsp
* @encode("unixTimestamp", int32)
* scalar myDateTime extends unixTimestamp;
* ```
*/
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