Skip to content
Closed
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** New `@dateFormat` decorator",
"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 new `@dateFormat` decorator",
"type": "none"
}
],
"packageName": "@typespec/openapi3"
}
17 changes: 17 additions & 0 deletions docs/standard-library/built-in-decorators.md
Original file line number Diff line number Diff line change
Expand Up @@ -531,6 +531,23 @@ model Dog extends Pet {kind: "dog", bark: boolean}
```


### `@dateFormat` {#@dateFormat}


```typespec
dec dateFormat(target: zonedDateTime | ModelProperty, format: rfc1123 | rfc7231 | unixTimeStamp | string)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The values should be rfc3339 and rfc7231. rfc7231 is the replacement for rfc1123

```

#### Target

`union zonedDateTime | ModelProperty`
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this work for duration as well?


#### Parameters
| Name | Type | Description |
|------|------|-------------|
| format | `union rfc1123 \| rfc7231 \| unixTimeStamp \| string` | |


### `@visibility` {#@visibility}

Indicates that a property is only considered to be present or applicable ("visible") with
Expand Down
25 changes: 25 additions & 0 deletions packages/compiler/lib/decorators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -533,6 +533,31 @@ export function isSecret(program: Program, target: Type): boolean | undefined {
return program.stateMap(secretTypesKey).get(target);
}

// -- @dateFormat decorator ---------------------
export type KnownDateFormat = "rfc3339" | "rfc7231" | "unixTimeStamp";

const dateFormatKey = createStateSymbol("dateFormat");
export function $dateFormat(
context: DecoratorContext,
target: Scalar | ModelProperty,
format: string
) {
validateDecoratorUniqueOnNode(context, target, $dateFormat);

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

context.program.stateMap(dateFormatKey).set(target, format);
}

export function getDateFormat(
program: Program,
target: Scalar | ModelProperty
): KnownDateFormat | string | undefined {
return program.stateMap(dateFormatKey).get(target);
}

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

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

alias KnownDateFormat = "rfc1123" | "rfc7231" | "unixTimeStamp";
extern dec dateFormat(target: zonedDateTime | ModelProperty, format: KnownDateFormat | string);
Copy link
Copy Markdown
Member Author

@timotheeguerin timotheeguerin Apr 5, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does @dateFormat make the most sense as the name and not @dateSerializationFormat. It is different from @format on a string which actually dictate how the string is formatted vs here we are telling what serialization pattern to use

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like shorter names, but dateSerializationFormat is more accurate


/**
* 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
50 changes: 49 additions & 1 deletion packages/compiler/test/decorators/decorators.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
import { deepStrictEqual, ok, strictEqual } from "assert";
import { getVisibility, isSecret, Model, Operation, Scalar } from "../../core/index.js";
import {
getVisibility,
isSecret,
Model,
ModelProperty,
Operation,
Scalar,
} from "../../core/index.js";
import {
getDateFormat,
getDoc,
getFriendlyName,
getKeyName,
Expand Down Expand Up @@ -357,6 +365,46 @@ describe("compiler: built-in decorators", () => {
});
});

describe("@dateFormat", () => {
it("assign the known values to zonedDateTime property", async () => {
const { dob } = (await runner.compile(`

model Foo {
@dateFormat("rfc1123")
@test
dob: zonedDateTime;
}

`)) as { dob: ModelProperty };

strictEqual(getDateFormat(runner.program, dob), "rfc1123");
});

it("assign the known values to zonedDateTime extended scalar", async () => {
const { Bar } = (await runner.compile(`
@test
@dateFormat("rfc1123")
scalar Bar extends zonedDateTime;
`)) as { Bar: Scalar };

ok(Bar.kind);
strictEqual(getDateFormat(runner.program, Bar), "rfc1123");
});

it("emit diagnostics when used on non scalar", async () => {
const diagnostics = await runner.diagnose(`
@dateFormat("rfc1123")
enum Bar {}
`);

expectDiagnostics(diagnostics, {
code: "decorator-wrong-target",
message:
"Cannot apply @dateFormat decorator to Bar since it is not assignable to zonedDateTime | ModelProperty",
});
});
});

describe("@withoutOmittedProperties", () => {
it("removes a model property when given a string literal", async () => {
const { TestModel } = await runner.compile(
Expand Down
11 changes: 11 additions & 0 deletions packages/openapi3/src/openapi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
EnumMember,
getAllTags,
getAnyExtensionFromPath,
getDateFormat,
getDiscriminatedUnion,
getDiscriminator,
getDoc,
Expand Down Expand Up @@ -1307,6 +1308,16 @@ function createOAPIEmitter(program: Program, options: ResolvedOpenAPI3EmitterOpt
newTarget.format = "password";
}

const dateFormat = getDateFormat(program, typespecType);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should also set the default dateFormat, which requires knowing whether the date is used in a header (rfc7231) or elsewhere (rfc 3339)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We’ll in openapi date-time already assume that from what I understand.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AFAIK, no, see: https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md?plain=1#L170

date-time should be the default for non-header types, but for headers, we need to be explicit

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh I see, I’ll update

if (dateFormat) {
if (dateFormat === "unixTimeStamp") {
newTarget.type = "integer";
newTarget.format = "timestamp";
} else {
newTarget.format = `date-time-${dateFormat}`;
}
}

if (isString) {
const values = getKnownValues(program, typespecType);
if (values) {
Expand Down
52 changes: 52 additions & 0 deletions packages/openapi3/test/primitive-types.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -189,4 +189,56 @@ describe("openapi3: primitives", () => {
});
});
});

describe("date format", () => {
it("set format to 'date-time' by default", async () => {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should have an alternate where this is ued in a header, and the default format is rfc-7231

const res = await oapiForModel(
"MyDate",
`
scalar MyDate extends zonedDateTime;
`
);
deepStrictEqual(res.schemas.MyDate, { type: "string", format: "date-time" });
});

it("set format to 'timestamp' and type to 'ingeger' for 'unixtimestamp' format", async () => {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
it("set format to 'timestamp' and type to 'ingeger' for 'unixtimestamp' format", async () => {
it("set format to 'timestamp' and type to 'integer' for 'unixtimestamp' format", async () => {

const res = await oapiForModel(
"MyDate",
`
@dateFormat("unixTimeStamp")
scalar MyDate extends zonedDateTime;
`
);
deepStrictEqual(res.schemas.MyDate, { type: "integer", format: "timestamp" });
});

it("set format to 'date-time-{date-format}' when set on scalar", async () => {
const res = await oapiForModel(
"MyDate",
`
@dateFormat("rfc1123")
scalar MyDate extends zonedDateTime;
`
);
deepStrictEqual(res.schemas.MyDate, { type: "string", format: "date-time-rfc1123" });
});

it("set format to 'date-time-{date-format}' when set on model property", async () => {
const res = await oapiForModel(
"MyDate",
`
model MyDate {
@dateFormat("rfc1123")
foo: zonedDateTime;
}

op test(): MyDate;
`
);
deepStrictEqual(res.schemas.MyDate.properties.foo, {
type: "string",
format: "date-time-rfc1123",
});
});
});
});