From ba9a4691e79ea54174be0c22e5288f9848d11c3c Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Wed, 3 May 2023 13:58:57 -0700 Subject: [PATCH 1/9] Wip --- packages/compiler/lib/decorators.tsp | 33 ++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/packages/compiler/lib/decorators.tsp b/packages/compiler/lib/decorators.tsp index 7c5df6adf1..c80baa6018 100644 --- a/packages/compiler/lib/decorators.tsp +++ b/packages/compiler/lib/decorators.tsp @@ -339,6 +339,39 @@ extern dec projectedName(target: unknown, targetName: string, projectedName: str */ extern dec discriminator(target: Model | Union, propertyName: string); +// Todo uncomment when valueof allows converting enum member to string +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", +} + +enum DurationKnownEncoding { + rfc3339: "rfc3339", + /** + * 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: unknown, 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 From 15e5bb45ceedd71547faa247bd8e714e6049c67a Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Wed, 3 May 2023 15:31:08 -0700 Subject: [PATCH 2/9] @encode decorator --- packages/compiler/core/checker.ts | 8 +- packages/compiler/core/messages.ts | 9 ++ packages/compiler/core/types.ts | 7 ++ packages/compiler/lib/decorators.ts | 103 +++++++++++++++++- packages/compiler/lib/decorators.tsp | 32 +++++- .../test/decorators/decorators.test.ts | 91 ++++++++++++++++ 6 files changed, 237 insertions(+), 13 deletions(-) diff --git a/packages/compiler/core/checker.ts b/packages/compiler/core/checker.ts index 2128c12273..22abe4f536 100644 --- a/packages/compiler/core/checker.ts +++ b/packages/compiler/core/checker.ts @@ -94,6 +94,8 @@ import { ReturnRecord, Scalar, ScalarStatementNode, + StdTypeName, + StdTypes, StringLiteral, StringLiteralNode, Sym, @@ -243,12 +245,6 @@ const TypeInstantiationMap = class extends MultiKeyMap implements TypeInstantiationMap {}; -type StdTypeName = IntrinsicScalarName | "Array" | "Record" | "object"; -type StdTypes = { - // Models - Array: Model; - Record: Model; -} & Record; type ReflectionTypeName = keyof typeof ReflectionNameToKind; let currentSymbolId = 0; diff --git a/packages/compiler/core/messages.ts b/packages/compiler/core/messages.ts index cb710f231c..3f52dd0d90 100644 --- a/packages/compiler/core/messages.ts +++ b/packages/compiler/core/messages.ts @@ -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 */ diff --git a/packages/compiler/core/types.ts b/packages/compiler/core/types.ts index 229ab9a549..ff5add2e41 100644 --- a/packages/compiler/core/types.ts +++ b/packages/compiler/core/types.ts @@ -91,6 +91,13 @@ export type Type = | ObjectType | Projection; +export type StdTypes = { + // Models + Array: Model; + Record: Model; +} & Record; +export type StdTypeName = keyof StdTypes; + export type TypeOrReturnRecord = Type | ReturnRecord; export interface ObjectType extends BaseType { diff --git a/packages/compiler/lib/decorators.ts b/packages/compiler/lib/decorators.ts index 2593aead0d..7d4112c46c 100644 --- a/packages/compiler/lib/decorators.ts +++ b/packages/compiler/lib/decorators.ts @@ -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"; @@ -227,7 +229,7 @@ export function $pattern( ) { validateDecoratorUniqueOnNode(context, target, $pattern); - if (!validateDecoratorTargetIntrinsic(context, target, "@pattern", "string")) { + if (!validateDecoratorTargetIntrinsic(context, target, "@pattern", ["string"])) { return; } @@ -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; @@ -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; @@ -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); @@ -533,6 +535,99 @@ 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, 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, 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"); diff --git a/packages/compiler/lib/decorators.tsp b/packages/compiler/lib/decorators.tsp index c80baa6018..23c3635e28 100644 --- a/packages/compiler/lib/decorators.tsp +++ b/packages/compiler/lib/decorators.tsp @@ -339,7 +339,9 @@ extern dec projectedName(target: unknown, targetName: string, projectedName: str */ extern dec discriminator(target: Model | Union, propertyName: string); -// Todo uncomment when valueof allows converting enum member to string +/** + * Known encoding to use on utcDateTime or offsetDateTime + */ enum DateTimeKnownEncoding { /** * RFC 3339 standard. https://www.ietf.org/rfc/rfc3339.txt @@ -357,8 +359,28 @@ enum DateTimeKnownEncoding { unixTimestamp: "unixTimestamp", } +/** + * Known encoding to use on duration + */ enum DurationKnownEncoding { - rfc3339: "rfc3339", + /** + * 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 */ @@ -370,7 +392,11 @@ enum DurationKnownEncoding { * @param encoding Known name of an encoding. * @param encodedAs What target type is this being encoded as. Default to string. */ -extern dec encode(target: unknown, encoding: string | EnumMember, encodedAs: string | numeric); +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 diff --git a/packages/compiler/test/decorators/decorators.test.ts b/packages/compiler/test/decorators/decorators.test.ts index c8c4c9ccdc..41aceb66fe 100644 --- a/packages/compiler/test/decorators/decorators.test.ts +++ b/packages/compiler/test/decorators/decorators.test.ts @@ -2,6 +2,7 @@ import { deepStrictEqual, ok, strictEqual } from "assert"; import { Model, Operation, Scalar, getVisibility, isSecret } from "../../core/index.js"; import { getDoc, + getEncode, getFriendlyName, getKeyName, getKnownValues, @@ -357,6 +358,96 @@ describe("compiler: built-in decorators", () => { }); }); + describe("@encode", () => { + it(`set encoding on scalar`, async () => { + const { s } = (await runner.compile(` + @encode("rfc3339") + @test + scalar s extends utcDateTime; + `)) as { s: Scalar }; + + strictEqual(getEncode(runner.program, s)?.encoding, "rfc3339"); + }); + + it(`encode type default to string`, async () => { + const { s } = (await runner.compile(` + @encode("rfc3339") + @test + scalar s extends utcDateTime; + `)) as { s: Scalar }; + + strictEqual(getEncode(runner.program, s)?.type.name, "string"); + }); + + it(`change encode type`, async () => { + const { s } = (await runner.compile(` + @encode("unixTimestamp", int32) + @test + scalar s extends utcDateTime; + `)) as { s: Scalar }; + + strictEqual(getEncode(runner.program, s)?.type.name, "int32"); + }); + + describe("known encoding validation", () => { + const validCases = [ + ["utcDateTime", "rfc3339", undefined], + ["offsetDateTime", "rfc7231", undefined], + ["utcDateTime", "unixTimeStamp", undefined], + ["duration", "ISO8601", undefined], + ["duration", "seconds", "int32"], + ["bytes", "base64", undefined], + ["bytes", "base64url", undefined], + // Do not block unknown encoding + ["utcDateTime", "custom-encoding", undefined], + ["duration", "custom-encoding", "int32"], + ]; + const invalidCases = [ + ["utcDateTime", "rfc3339", "int32"], + ["offsetDateTime", "rfc7231", "int64"], + ["offsetDateTime", "unixTimeStamp", undefined], + ["duration", "seconds", undefined], + ["duration", "rfc3339", undefined], + ["bytes", "rfc3339", undefined], + ]; + describe("valid", () => { + validCases.forEach(([target, encoding, encodeAs]) => { + it(`encoding '${encoding}' on ${target} encoded as ${encodeAs ?? "string"}`, async () => { + const encodeAsParam = encodeAs ? `, ${encodeAs}` : ""; + const { s } = (await runner.compile(` + @encode("${encoding}"${encodeAsParam}) + @test + scalar s extends ${target}; + `)) as { s: Scalar }; + + const encodeData = getEncode(runner.program, s); + ok(encodeData); + strictEqual(encodeData.encoding, encoding); + strictEqual(encodeData.type.name, encodeAs ?? "string"); + }); + }); + }); + describe("invalid", () => { + invalidCases.forEach(([target, encoding, encodeAs]) => { + it(`encoding '${encoding}' on ${target} encoded as ${ + encodeAs ?? "string" + }`, async () => { + const encodeAsParam = encodeAs ? `, ${encodeAs}` : ""; + const diagnostics = await runner.diagnose(` + @encode("${encoding}"${encodeAsParam}) + @test + scalar s extends ${target}; + `); + + expectDiagnostics(diagnostics, { + code: "invalid-encode", + }); + }); + }); + }); + }); + }); + describe("@withoutOmittedProperties", () => { it("removes a model property when given a string literal", async () => { const { TestModel } = await runner.compile( From d8dbe517bc07e8219dbe34de110a05f89e7de562 Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Wed, 3 May 2023 15:31:45 -0700 Subject: [PATCH 3/9] changelog --- .../compiler/feature-encode_2023-05-03-22-31.json | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 common/changes/@typespec/compiler/feature-encode_2023-05-03-22-31.json diff --git a/common/changes/@typespec/compiler/feature-encode_2023-05-03-22-31.json b/common/changes/@typespec/compiler/feature-encode_2023-05-03-22-31.json new file mode 100644 index 0000000000..f802eea9cb --- /dev/null +++ b/common/changes/@typespec/compiler/feature-encode_2023-05-03-22-31.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@typespec/compiler", + "comment": "**Added** `@encode` decorator used to specify encoding of types", + "type": "none" + } + ], + "packageName": "@typespec/compiler" +} \ No newline at end of file From 7773f50c525aafeef87af1dc4ae929eb808fd270 Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Wed, 3 May 2023 16:10:22 -0700 Subject: [PATCH 4/9] Add handling in openapi3 --- packages/compiler/lib/decorators.ts | 12 ++++- .../test/decorators/decorators.test.ts | 2 + packages/openapi3/src/openapi.ts | 26 ++++++++++ .../openapi3/test/primitive-types.test.ts | 51 +++++++++++++++++++ 4 files changed, 89 insertions(+), 2 deletions(-) diff --git a/packages/compiler/lib/decorators.ts b/packages/compiler/lib/decorators.ts index 7d4112c46c..a0ce52953e 100644 --- a/packages/compiler/lib/decorators.ts +++ b/packages/compiler/lib/decorators.ts @@ -569,7 +569,11 @@ function validateEncodeData(context: DecoratorContext, target: Scalar, encodeDat const checker = context.program.checker; const isTargetValid = validTargets.some((validTarget) => { return ignoreDiagnostics( - checker.isTypeAssignableTo(target, checker.getStdType(validTarget), target) + checker.isTypeAssignableTo( + target.projectionBase ?? target, + checker.getStdType(validTarget), + target + ) ); }); @@ -587,7 +591,11 @@ function validateEncodeData(context: DecoratorContext, target: Scalar, encodeDat } const isEncodingTypeValid = validEncodeTypes.some((validEncoding) => { return ignoreDiagnostics( - checker.isTypeAssignableTo(encodeData.type, checker.getStdType(validEncoding), target) + checker.isTypeAssignableTo( + encodeData.type.projectionBase ?? encodeData.type, + checker.getStdType(validEncoding), + target + ) ); }); diff --git a/packages/compiler/test/decorators/decorators.test.ts b/packages/compiler/test/decorators/decorators.test.ts index 41aceb66fe..7ae8065407 100644 --- a/packages/compiler/test/decorators/decorators.test.ts +++ b/packages/compiler/test/decorators/decorators.test.ts @@ -392,6 +392,8 @@ describe("compiler: built-in decorators", () => { describe("known encoding validation", () => { const validCases = [ ["utcDateTime", "rfc3339", undefined], + ["utcDateTime", "rfc7231", undefined], + ["offsetDateTime", "rfc3339", undefined], ["offsetDateTime", "rfc7231", undefined], ["utcDateTime", "unixTimeStamp", undefined], ["duration", "ISO8601", undefined], diff --git a/packages/openapi3/src/openapi.ts b/packages/openapi3/src/openapi.ts index 3cb298c34a..cf8108fd9c 100644 --- a/packages/openapi3/src/openapi.ts +++ b/packages/openapi3/src/openapi.ts @@ -11,6 +11,7 @@ import { getDiscriminatedUnion, getDiscriminator, getDoc, + getEncode, getFormat, getKnownValues, getMaxItems, @@ -1690,6 +1691,13 @@ function createOAPIEmitter(program: Program, options: ResolvedOpenAPI3EmitterOpt newTarget.format = "password"; } + const encodeData = getEncode(program, typespecType); + if (encodeData) { + const newType = getSchemaForScalar(encodeData.type); + newTarget.type = newType.type; + newTarget.format = mergeFormatAndEncoding(newTarget.format, encodeData.encoding); + } + if (isString) { const values = getKnownValues(program, typespecType); if (values) { @@ -1704,6 +1712,24 @@ function createOAPIEmitter(program: Program, options: ResolvedOpenAPI3EmitterOpt return newTarget; } + function mergeFormatAndEncoding(format: string | undefined, encoding: string): string { + switch (format) { + case undefined: + return encoding; + case "date-time": + switch (encoding) { + case "rfc3339": + return "date-time"; + case "unixTimestamp": + return "unix-timestamp"; + default: + return `date-time-${encoding}`; + } + default: + return encoding; + } + } + function applyExternalDocs(typespecType: Type, target: Record) { const externalDocs = getExternalDocs(program, typespecType); if (externalDocs) { diff --git a/packages/openapi3/test/primitive-types.test.ts b/packages/openapi3/test/primitive-types.test.ts index 5ebd66f3b1..bcf79e35ed 100644 --- a/packages/openapi3/test/primitive-types.test.ts +++ b/packages/openapi3/test/primitive-types.test.ts @@ -1,4 +1,5 @@ import { deepStrictEqual, ok } from "assert"; +import { OpenAPI3Schema } from "../src/types.js"; import { oapiForModel } from "./test-host.js"; describe("openapi3: primitives", () => { @@ -190,4 +191,54 @@ describe("openapi3: primitives", () => { }); }); }); + + describe("using @encode decorator", () => { + async function testEncode( + scalar: string, + expectedOpenApi: OpenAPI3Schema, + encoding?: string, + encodeAs?: string + ) { + const encodeAsParam = encodeAs ? `, ${encodeAs}` : ""; + const encodeDecorator = encoding ? `@encode("${encoding}"${encodeAsParam})` : ""; + const res = await oapiForModel("s", `${encodeDecorator} scalar s extends ${scalar};`); + deepStrictEqual(res.schemas.s, expectedOpenApi); + } + + describe("utcDateTime", () => { + it("set format to 'date-time' by default", () => + testEncode("utcDateTime", { type: "string", format: "date-time" })); + it("set format to 'date-time-rfc7231' when encoding is rfc7231", () => + testEncode("utcDateTime", { type: "string", format: "date-time-rfc7231" }, "rfc7231")); + + it("set type to integer and format to 'unixTimeStamp' when encoding is unixTimestamp", () => + testEncode( + "utcDateTime", + { type: "integer", format: "unix-timestamp" }, + "unixTimestamp", + "int32" + )); + }); + + describe("offsetDateTime", () => { + it("set format to 'date-time' by default", () => + testEncode("offsetDateTime", { type: "string", format: "date-time" })); + it("set format to 'date-time-rfc7231' when encoding is rfc7231", () => + testEncode("offsetDateTime", { type: "string", format: "date-time-rfc7231" }, "timestamp")); + }); + + describe("duration", () => { + it("set format to 'duration' by default", () => + testEncode("duration", { type: "string", format: "duration" })); + it("set interger with seconds format setting duration as seconds", () => + testEncode("duration", { type: "integer", format: "seconds" }, "seconds", "int32")); + }); + + describe("bytes", () => { + it("set format to 'base64' by default", () => + testEncode("bytes", { type: "string", format: "byte" })); + it("set interger with seconds format setting duration as seconds", () => + testEncode("bytes", { type: "string", format: "base64url" }, "base64url")); + }); + }); }); From c7d1df4a728b52e67ea241ffa63bb6917a83d1bd Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Wed, 3 May 2023 16:12:31 -0700 Subject: [PATCH 5/9] Changelog --- .../openapi3/feature-encode_2023-05-03-23-12.json | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 common/changes/@typespec/openapi3/feature-encode_2023-05-03-23-12.json diff --git a/common/changes/@typespec/openapi3/feature-encode_2023-05-03-23-12.json b/common/changes/@typespec/openapi3/feature-encode_2023-05-03-23-12.json new file mode 100644 index 0000000000..cadeac9d9a --- /dev/null +++ b/common/changes/@typespec/openapi3/feature-encode_2023-05-03-23-12.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@typespec/openapi3", + "comment": "**Added** support for `@encode` decorator", + "type": "none" + } + ], + "packageName": "@typespec/openapi3" +} \ No newline at end of file From 1e6265c20e2cfe1725acaac518ce00a323b36aa5 Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Thu, 4 May 2023 08:44:06 -0700 Subject: [PATCH 6/9] Fix tests --- packages/compiler/core/decorator-utils.ts | 16 +++++++++++++++- packages/compiler/lib/decorators.ts | 10 ++++++++++ packages/compiler/lib/decorators.tsp | 14 ++++++++++++++ packages/html-program-viewer/src/ui.tsx | 2 +- packages/openapi3/test/primitive-types.test.ts | 2 +- 5 files changed, 41 insertions(+), 3 deletions(-) diff --git a/packages/compiler/core/decorator-utils.ts b/packages/compiler/core/decorator-utils.ts index 973a92819b..62c1c66602 100644 --- a/packages/compiler/core/decorator-utils.ts +++ b/packages/compiler/core/decorator-utils.ts @@ -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 { @@ -61,6 +61,20 @@ export function validateDecoratorTarget( 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, diff --git a/packages/compiler/lib/decorators.ts b/packages/compiler/lib/decorators.ts index a4f6a36b75..ac276784e7 100644 --- a/packages/compiler/lib/decorators.ts +++ b/packages/compiler/lib/decorators.ts @@ -1,4 +1,5 @@ import { + isIntrinsicType, validateDecoratorNotOnType, validateDecoratorTarget, validateDecoratorTargetIntrinsic, @@ -8,6 +9,7 @@ import { getDiscriminatedUnion, getTypeName, ignoreDiagnostics, + reportDeprecated, validateDecoratorUniqueOnNode, } from "../core/index.js"; import { createDiagnostic, reportDiagnostic } from "../core/messages.js"; @@ -210,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); } diff --git a/packages/compiler/lib/decorators.tsp b/packages/compiler/lib/decorators.tsp index 23c3635e28..5fcdd3fe71 100644 --- a/packages/compiler/lib/decorators.tsp +++ b/packages/compiler/lib/decorators.tsp @@ -391,6 +391,20 @@ enum BytesKnownEncoding { * 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, diff --git a/packages/html-program-viewer/src/ui.tsx b/packages/html-program-viewer/src/ui.tsx index 7ff490faa5..1946204afa 100644 --- a/packages/html-program-viewer/src/ui.tsx +++ b/packages/html-program-viewer/src/ui.tsx @@ -136,7 +136,7 @@ const NamedTypeUI = ({ type, name, properties }: NamedTypeU valueUI = value; } else if (value.kind) { valueUI = render(value); - } else if (value[Symbol.iterator]) { + } else if (typeof value === "object" && "entries" in value && typeof value.entries === "function") { valueUI = ; } else { valueUI = value; diff --git a/packages/openapi3/test/primitive-types.test.ts b/packages/openapi3/test/primitive-types.test.ts index bcf79e35ed..61fad8baae 100644 --- a/packages/openapi3/test/primitive-types.test.ts +++ b/packages/openapi3/test/primitive-types.test.ts @@ -224,7 +224,7 @@ describe("openapi3: primitives", () => { it("set format to 'date-time' by default", () => testEncode("offsetDateTime", { type: "string", format: "date-time" })); it("set format to 'date-time-rfc7231' when encoding is rfc7231", () => - testEncode("offsetDateTime", { type: "string", format: "date-time-rfc7231" }, "timestamp")); + testEncode("offsetDateTime", { type: "string", format: "date-time-rfc7231" }, "rfc7231")); }); describe("duration", () => { From e7c02aa7b93a8f05802beedbfe196514f1184161 Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Thu, 4 May 2023 08:44:31 -0700 Subject: [PATCH 7/9] Changelog --- .../feature-encode_2023-05-04-15-44.json | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 common/changes/@typespec/html-program-viewer/feature-encode_2023-05-04-15-44.json diff --git a/common/changes/@typespec/html-program-viewer/feature-encode_2023-05-04-15-44.json b/common/changes/@typespec/html-program-viewer/feature-encode_2023-05-04-15-44.json new file mode 100644 index 0000000000..e6e6387094 --- /dev/null +++ b/common/changes/@typespec/html-program-viewer/feature-encode_2023-05-04-15-44.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@typespec/html-program-viewer", + "comment": "Fix issue with showing enum members", + "type": "none" + } + ], + "packageName": "@typespec/html-program-viewer" +} \ No newline at end of file From 41f340cc11b3a68477b51fa33aa1c451037083b3 Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Thu, 4 May 2023 09:36:40 -0700 Subject: [PATCH 8/9] Regen docs --- docs/standard-library/built-in-decorators.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/docs/standard-library/built-in-decorators.md b/docs/standard-library/built-in-decorators.md index 26758262a6..3633a37e57 100644 --- a/docs/standard-library/built-in-decorators.md +++ b/docs/standard-library/built-in-decorators.md @@ -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. From 1332eb300cc472d4cb23339eb0bb9cb98ce3afbf Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Thu, 4 May 2023 10:21:03 -0700 Subject: [PATCH 9/9] Format --- packages/html-program-viewer/src/ui.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/html-program-viewer/src/ui.tsx b/packages/html-program-viewer/src/ui.tsx index 1946204afa..b470f09342 100644 --- a/packages/html-program-viewer/src/ui.tsx +++ b/packages/html-program-viewer/src/ui.tsx @@ -136,7 +136,11 @@ const NamedTypeUI = ({ type, name, properties }: NamedTypeU valueUI = value; } else if (value.kind) { valueUI = render(value); - } else if (typeof value === "object" && "entries" in value && typeof value.entries === "function") { + } else if ( + typeof value === "object" && + "entries" in value && + typeof value.entries === "function" + ) { valueUI = ; } else { valueUI = value;