Skip to content

Commit f488fbc

Browse files
Feature: Add @encode decorator (#1899)
1 parent a92cc3d commit f488fbc

14 files changed

Lines changed: 447 additions & 12 deletions

File tree

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"changes": [
3+
{
4+
"packageName": "@typespec/compiler",
5+
"comment": "**Added** `@encode` decorator used to specify encoding of types",
6+
"type": "none"
7+
}
8+
],
9+
"packageName": "@typespec/compiler"
10+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"changes": [
3+
{
4+
"packageName": "@typespec/html-program-viewer",
5+
"comment": "Fix issue with showing enum members",
6+
"type": "none"
7+
}
8+
],
9+
"packageName": "@typespec/html-program-viewer"
10+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"changes": [
3+
{
4+
"packageName": "@typespec/openapi3",
5+
"comment": "**Added** support for `@encode` decorator",
6+
"type": "none"
7+
}
8+
],
9+
"packageName": "@typespec/openapi3"
10+
}

docs/standard-library/built-in-decorators.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,25 @@ dec doc(target: unknown, doc: string, formatArgs?: object)
6161
| formatArgs | `model object` | Record with key value pair that can be interpolated in the doc. |
6262

6363

64+
### `@encode` {#@encode}
65+
66+
Specify how to encode the target type.
67+
68+
```typespec
69+
dec encode(target: Scalar | ModelProperty, encoding: string | EnumMember, encodedAs?: string | numeric)
70+
```
71+
72+
#### Target
73+
74+
`union Scalar | ModelProperty`
75+
76+
#### Parameters
77+
| Name | Type | Description |
78+
|------|------|-------------|
79+
| encoding | `union string \| EnumMember` | Known name of an encoding. |
80+
| encodedAs | `union string \| numeric` | What target type is this being encoded as. Default to string. |
81+
82+
6483
### `@error` {#@error}
6584

6685
Specify that this model is an error type. Operations return error types when the operation has failed.

packages/compiler/core/checker.ts

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,8 @@ import {
9494
ReturnRecord,
9595
Scalar,
9696
ScalarStatementNode,
97+
StdTypeName,
98+
StdTypes,
9799
StringLiteral,
98100
StringLiteralNode,
99101
Sym,
@@ -243,12 +245,6 @@ const TypeInstantiationMap = class
243245
extends MultiKeyMap<readonly Type[], Type>
244246
implements TypeInstantiationMap {};
245247

246-
type StdTypeName = IntrinsicScalarName | "Array" | "Record" | "object";
247-
type StdTypes = {
248-
// Models
249-
Array: Model;
250-
Record: Model;
251-
} & Record<IntrinsicScalarName, Scalar>;
252248
type ReflectionTypeName = keyof typeof ReflectionNameToKind;
253249

254250
let currentSymbolId = 0;

packages/compiler/core/decorator-utils.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { getPropertyType } from "../lib/decorators.js";
22
import { getTypeName } from "./helpers/type-name-utils.js";
3-
import { compilerAssert, Interface, Model, SyntaxKind } from "./index.js";
3+
import { compilerAssert, ignoreDiagnostics, Interface, Model, SyntaxKind } from "./index.js";
44
import { createDiagnostic, reportDiagnostic } from "./messages.js";
55
import { Program } from "./program.js";
66
import {
@@ -61,6 +61,20 @@ export function validateDecoratorTarget<K extends TypeKind>(
6161
return true;
6262
}
6363

64+
export function isIntrinsicType(
65+
program: Program,
66+
type: Scalar,
67+
kind: IntrinsicScalarName
68+
): boolean {
69+
return ignoreDiagnostics(
70+
program.checker.isTypeAssignableTo(
71+
type.projectionBase ?? type,
72+
program.checker.getStdType(kind),
73+
type
74+
)
75+
);
76+
}
77+
6478
export function validateDecoratorTargetIntrinsic(
6579
context: DecoratorContext,
6680
target: Scalar | ModelProperty,

packages/compiler/core/messages.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -681,6 +681,15 @@ const diagnostics = {
681681
},
682682
},
683683

684+
"invalid-encode": {
685+
severity: "error",
686+
messages: {
687+
default: "Invalid encoding",
688+
wrongType: paramMessage`Encoding '${"encoding"}' cannot be used on type ${"type"}. Expected a ${"expected"}.`,
689+
wrongEncodingType: paramMessage`Encoding '${"encoding"}' cannot be used on type ${"type"}. Expected a ${"expected"}.`,
690+
},
691+
},
692+
684693
/**
685694
* Service
686695
*/

packages/compiler/core/types.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,13 @@ export type Type =
9191
| ObjectType
9292
| Projection;
9393

94+
export type StdTypes = {
95+
// Models
96+
Array: Model;
97+
Record: Model;
98+
} & Record<IntrinsicScalarName, Scalar>;
99+
export type StdTypeName = keyof StdTypes;
100+
94101
export type TypeOrReturnRecord = Type | ReturnRecord;
95102

96103
export interface ObjectType extends BaseType {

packages/compiler/lib/decorators.ts

Lines changed: 117 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
11
import {
2+
isIntrinsicType,
23
validateDecoratorNotOnType,
34
validateDecoratorTarget,
45
validateDecoratorTargetIntrinsic,
56
} from "../core/decorator-utils.js";
67
import {
8+
StdTypeName,
79
getDiscriminatedUnion,
810
getTypeName,
11+
ignoreDiagnostics,
12+
reportDeprecated,
913
validateDecoratorUniqueOnNode,
1014
} from "../core/index.js";
1115
import { createDiagnostic, reportDiagnostic } from "../core/messages.js";
@@ -208,6 +212,14 @@ export function $format(context: DecoratorContext, target: Scalar | ModelPropert
208212
if (!validateDecoratorTargetIntrinsic(context, target, "@format", ["string", "bytes"])) {
209213
return;
210214
}
215+
const targetType = getPropertyType(target);
216+
if (targetType.kind === "Scalar" && isIntrinsicType(context.program, targetType, "bytes")) {
217+
reportDeprecated(
218+
context.program,
219+
"Using `@format` on a bytes scalar is deprecated. Use `@encode` instead. https://github.com/microsoft/typespec/issues/1873",
220+
target
221+
);
222+
}
211223

212224
context.program.stateMap(formatValuesKey).set(target, format);
213225
}
@@ -227,7 +239,7 @@ export function $pattern(
227239
) {
228240
validateDecoratorUniqueOnNode(context, target, $pattern);
229241

230-
if (!validateDecoratorTargetIntrinsic(context, target, "@pattern", "string")) {
242+
if (!validateDecoratorTargetIntrinsic(context, target, "@pattern", ["string"])) {
231243
return;
232244
}
233245

@@ -250,7 +262,7 @@ export function $minLength(
250262
validateDecoratorUniqueOnNode(context, target, $minLength);
251263

252264
if (
253-
!validateDecoratorTargetIntrinsic(context, target, "@minLength", "string") ||
265+
!validateDecoratorTargetIntrinsic(context, target, "@minLength", ["string"]) ||
254266
!validateRange(context, minLength, getMaxLength(context.program, target))
255267
) {
256268
return;
@@ -275,7 +287,7 @@ export function $maxLength(
275287
validateDecoratorUniqueOnNode(context, target, $maxLength);
276288

277289
if (
278-
!validateDecoratorTargetIntrinsic(context, target, "@maxLength", "string") ||
290+
!validateDecoratorTargetIntrinsic(context, target, "@maxLength", ["string"]) ||
279291
!validateRange(context, getMinLength(context.program, target), maxLength)
280292
) {
281293
return;
@@ -523,7 +535,7 @@ const secretTypesKey = createStateSymbol("secretTypes");
523535
export function $secret(context: DecoratorContext, target: Scalar | ModelProperty) {
524536
validateDecoratorUniqueOnNode(context, target, $secret);
525537

526-
if (!validateDecoratorTargetIntrinsic(context, target, "@secret", "string")) {
538+
if (!validateDecoratorTargetIntrinsic(context, target, "@secret", ["string"])) {
527539
return;
528540
}
529541
context.program.stateMap(secretTypesKey).set(target, true);
@@ -533,6 +545,107 @@ export function isSecret(program: Program, target: Type): boolean | undefined {
533545
return program.stateMap(secretTypesKey).get(target);
534546
}
535547

548+
export type DateTimeKnownEncoding = "rfc3339" | "rfc7231" | "unixTimeStamp";
549+
export type DurationKnownEncoding = "ISO8601" | "seconds";
550+
export type BytesKnownEncoding = "base64" | "base64url";
551+
export interface EncodeData {
552+
encoding: DateTimeKnownEncoding | DurationKnownEncoding | string;
553+
type: Scalar;
554+
}
555+
556+
const encodeKey = createStateSymbol("encode");
557+
export function $encode(
558+
context: DecoratorContext,
559+
target: Scalar | ModelProperty,
560+
encoding: string | EnumMember,
561+
encodeAs?: Scalar
562+
) {
563+
validateDecoratorUniqueOnNode(context, target, $encode);
564+
565+
const encodeData: EncodeData = {
566+
encoding: typeof encoding === "string" ? encoding : encoding.value?.toString() ?? encoding.name,
567+
type: encodeAs ?? context.program.checker.getStdType("string"),
568+
};
569+
const targetType = getPropertyType(target);
570+
if (targetType.kind !== "Scalar") {
571+
return;
572+
}
573+
validateEncodeData(context, targetType, encodeData);
574+
context.program.stateMap(encodeKey).set(target, encodeData);
575+
}
576+
577+
function validateEncodeData(context: DecoratorContext, target: Scalar, encodeData: EncodeData) {
578+
function check(validTargets: StdTypeName[], validEncodeTypes: StdTypeName[]) {
579+
const checker = context.program.checker;
580+
const isTargetValid = validTargets.some((validTarget) => {
581+
return ignoreDiagnostics(
582+
checker.isTypeAssignableTo(
583+
target.projectionBase ?? target,
584+
checker.getStdType(validTarget),
585+
target
586+
)
587+
);
588+
});
589+
590+
if (!isTargetValid) {
591+
reportDiagnostic(context.program, {
592+
code: "invalid-encode",
593+
messageId: "wrongType",
594+
format: {
595+
encoding: encodeData.encoding,
596+
type: getTypeName(target),
597+
expected: validTargets.join(", "),
598+
},
599+
target: context.decoratorTarget,
600+
});
601+
}
602+
const isEncodingTypeValid = validEncodeTypes.some((validEncoding) => {
603+
return ignoreDiagnostics(
604+
checker.isTypeAssignableTo(
605+
encodeData.type.projectionBase ?? encodeData.type,
606+
checker.getStdType(validEncoding),
607+
target
608+
)
609+
);
610+
});
611+
612+
if (!isEncodingTypeValid) {
613+
reportDiagnostic(context.program, {
614+
code: "invalid-encode",
615+
messageId: "wrongEncodingType",
616+
format: {
617+
encoding: encodeData.encoding,
618+
type: getTypeName(target),
619+
expected: validEncodeTypes.join(", "),
620+
},
621+
target: context.decoratorTarget,
622+
});
623+
}
624+
}
625+
626+
switch (encodeData.encoding) {
627+
case "rfc3339":
628+
return check(["utcDateTime", "offsetDateTime"], ["string"]);
629+
case "rfc7231":
630+
return check(["utcDateTime", "offsetDateTime"], ["string"]);
631+
case "unixTimeStamp":
632+
return check(["utcDateTime"], ["string"]);
633+
case "seconds":
634+
return check(["duration"], ["numeric"]);
635+
case "base64":
636+
return check(["bytes"], ["string"]);
637+
case "base64url":
638+
return check(["bytes"], ["string"]);
639+
}
640+
}
641+
642+
export function getEncode(
643+
program: Program,
644+
target: Scalar | ModelProperty
645+
): EncodeData | undefined {
646+
return program.stateMap(encodeKey).get(target);
647+
}
648+
536649
// -- @visibility decorator ---------------------
537650

538651
const visibilitySettingsKey = createStateSymbol("visibilitySettings");

packages/compiler/lib/decorators.tsp

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -339,6 +339,79 @@ extern dec projectedName(target: unknown, targetName: string, projectedName: str
339339
*/
340340
extern dec discriminator(target: Model | Union, propertyName: string);
341341

342+
/**
343+
* Known encoding to use on utcDateTime or offsetDateTime
344+
*/
345+
enum DateTimeKnownEncoding {
346+
/**
347+
* RFC 3339 standard. https://www.ietf.org/rfc/rfc3339.txt
348+
* Encode to string.
349+
*/
350+
rfc3339: "rfc3339",
351+
/**
352+
* RFC 7231 standard. https://www.ietf.org/rfc/rfc7231.txt
353+
* Encode to string.
354+
*/
355+
rfc7231: "rfc7231",
356+
/**
357+
* Encode to integer
358+
*/
359+
unixTimestamp: "unixTimestamp",
360+
}
361+
362+
/**
363+
* Known encoding to use on duration
364+
*/
365+
enum DurationKnownEncoding {
366+
/**
367+
* ISO8601 duration
368+
*/
369+
ISO8601: "ISO8601",
370+
/**
371+
* Encode to integer or float
372+
*/
373+
seconds: "seconds",
374+
}
375+
376+
/**
377+
* Known encoding to use on bytes
378+
*/
379+
enum BytesKnownEncoding {
380+
/**
381+
* ISO8601 duration
382+
*/
383+
ISO8601: "ISO8601",
384+
/**
385+
* Encode to integer or float
386+
*/
387+
seconds: "seconds",
388+
}
389+
390+
/**
391+
* Specify how to encode the target type.
392+
* @param encoding Known name of an encoding.
393+
* @param encodedAs What target type is this being encoded as. Default to string.
394+
*
395+
* @example offsetDateTime encoded with rfc7231
396+
*
397+
* ```tsp
398+
* @encode("rfc7231")
399+
* scalar myDateTime extends offsetDateTime;
400+
* ```
401+
*
402+
* @example utcDateTime encoded with unixTimestamp
403+
*
404+
* ```tsp
405+
* @encode("unixTimestamp", int32)
406+
* scalar myDateTime extends unixTimestamp;
407+
* ```
408+
*/
409+
extern dec encode(
410+
target: Scalar | ModelProperty,
411+
encoding: string | EnumMember,
412+
encodedAs?: string | numeric
413+
);
414+
342415
/**
343416
* Indicates that a property is only considered to be present or applicable ("visible") with
344417
* the in the given named contexts ("visibilities"). When a property has no visibilities applied

0 commit comments

Comments
 (0)