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

Fix issues with examples #3875

Merged
merged 2 commits into from
Jul 17, 2024
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
8 changes: 8 additions & 0 deletions .chronus/changes/fix-examples-issues-2024-6-17-15-39-58.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
# Change versionKind to one of: internal, fix, dependencies, feature, deprecation, breaking
changeKind: fix
packages:
- "@typespec/compiler"
---

Fix issues with examples not working with `Array`, `Record`, `Union` and `unknown` types
64 changes: 59 additions & 5 deletions packages/compiler/src/lib/examples.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
import { Temporal } from "temporal-polyfill";
import { ignoreDiagnostics } from "../core/diagnostics.js";
import type { Program } from "../core/program.js";
import type { Model, ObjectValue, Scalar, ScalarValue, Type, Value } from "../core/types.js";
import { isArrayModelType, isUnknownType } from "../core/type-utils.js";
import {
type ObjectValue,
type Scalar,
type ScalarValue,
type Type,
type Value,
} from "../core/types.js";
import { getEncode, type EncodeData } from "./decorators.js";

/**
Expand All @@ -27,22 +35,68 @@ export function serializeValueAsJson(
case "EnumValue":
return value.value.value ?? value.value.name;
case "ArrayValue":
return value.values.map((v) => serializeValueAsJson(program, v, type));
return value.values.map((v) =>
serializeValueAsJson(
program,
v,
type.kind === "Model" && isArrayModelType(program, type)
? type.indexer.value
: program.checker.anyType
)
);
case "ObjectValue":
return serializeObjectValueAsJson(program, value, type as Model);
return serializeObjectValueAsJson(program, value, type);
case "ScalarValue":
return serializeScalarValueAsJson(program, value, type, encodeAs);
}
}

/** Try to get the property of the type */
function getPropertyOfType(type: Type, name: string): Type | undefined {
switch (type.kind) {
case "Model":
return type.properties.get(name) ?? type.indexer?.value;
case "Intrinsic":
if (isUnknownType(type)) {
return type;
} else {
return;
}
default:
return undefined;
}
}

function resolveUnions(program: Program, value: ObjectValue, type: Type): Type | undefined {
if (type.kind !== "Union") {
return type;
}
for (const variant of type.variants.values()) {
if (
variant.type.kind === "Model" &&
ignoreDiagnostics(
program.checker.isTypeAssignableTo(
value,
{ entityKind: "MixedParameterConstraint", valueType: variant.type },
value
)
)
) {
return variant.type;
}
}
return type;
}

function serializeObjectValueAsJson(
program: Program,
value: ObjectValue,
type: Model
type: Type
): Record<string, unknown> {
type = resolveUnions(program, value, type) ?? type;
const obj: Record<string, unknown> = {};
for (const propValue of value.properties.values()) {
const definition = type.properties.get(propValue.name);
const definition = getPropertyOfType(type, propValue.name);
if (definition) {
obj[propValue.name] = serializeValueAsJson(program, propValue.value, definition);
}
Expand Down
249 changes: 165 additions & 84 deletions packages/compiler/test/decorators/examples.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -255,107 +255,188 @@ describe("json serialization of examples", () => {
return serializeValueAsJson(program, examples[0].value, target);
}

const allCases: [
string,
{
value: string;
expect: unknown;
encode?: string;
}[],
][] = [
["int32", [{ value: `123`, expect: 123 }]],
["string", [{ value: `"abc"`, expect: "abc" }]],
["boolean", [{ value: `true`, expect: true }]],
[
"utcDateTime",
describe("scalar encoding", () => {
timotheeguerin marked this conversation as resolved.
Show resolved Hide resolved
const allCases: [
string,
{
value: string;
expect: unknown;
encode?: string;
}[],
][] = [
["int32", [{ value: `123`, expect: 123 }]],
["string", [{ value: `"abc"`, expect: "abc" }]],
["boolean", [{ value: `true`, expect: true }]],
[
{ value: `utcDateTime.fromISO("2024-01-01T11:32:00Z")`, expect: "2024-01-01T11:32:00Z" },
{
value: `utcDateTime.fromISO("2024-01-01T11:32:00Z")`,
expect: "Mon, 01 Jan 2024 11:32:00 GMT",
encode: `@encode("rfc7231")`,
},
{
value: `utcDateTime.fromISO("2024-01-01T11:32:00Z")`,
expect: 1704108720,
encode: `@encode("unixTimestamp", int32)`,
},
"utcDateTime",
[
{ value: `utcDateTime.fromISO("2024-01-01T11:32:00Z")`, expect: "2024-01-01T11:32:00Z" },
{
value: `utcDateTime.fromISO("2024-01-01T11:32:00Z")`,
expect: "Mon, 01 Jan 2024 11:32:00 GMT",
encode: `@encode("rfc7231")`,
},
{
value: `utcDateTime.fromISO("2024-01-01T11:32:00Z")`,
expect: 1704108720,
encode: `@encode("unixTimestamp", int32)`,
},
],
],
],
[
"offsetDateTime",
[
{
value: `offsetDateTime.fromISO("2024-01-01T11:32:00+01:00")`,
expect: "2024-01-01T11:32:00+01:00",
},
{
value: `offsetDateTime.fromISO("2024-01-01T11:32:00+01:00")`,
expect: "Mon, 01 Jan 2024 10:32:00 GMT",
encode: `@encode("rfc7231")`,
},
"offsetDateTime",
[
{
value: `offsetDateTime.fromISO("2024-01-01T11:32:00+01:00")`,
expect: "2024-01-01T11:32:00+01:00",
},
{
value: `offsetDateTime.fromISO("2024-01-01T11:32:00+01:00")`,
expect: "Mon, 01 Jan 2024 10:32:00 GMT",
encode: `@encode("rfc7231")`,
},
],
],
],
[
"plainDate",
[
{
value: `plainDate.fromISO("2024-01-01")`,
expect: "2024-01-01",
},
"plainDate",
[
{
value: `plainDate.fromISO("2024-01-01")`,
expect: "2024-01-01",
},
],
],
],
[
"plainTime",
[
{
value: `plainTime.fromISO("11:31")`,
expect: "11:31",
},
"plainTime",
[
{
value: `plainTime.fromISO("11:31")`,
expect: "11:31",
},
],
],
],
[
"duration",
[
{
value: `duration.fromISO("PT5M")`,
expect: "PT5M",
},
{
value: `duration.fromISO("PT5M")`,
expect: 300,
encode: `@encode("seconds", int32)`,
},
{
value: `duration.fromISO("PT0.5S")`,
expect: 0.5,
encode: `@encode("seconds", float32)`,
},
"duration",
[
{
value: `duration.fromISO("PT5M")`,
expect: "PT5M",
},
{
value: `duration.fromISO("PT5M")`,
expect: 300,
encode: `@encode("seconds", int32)`,
},
{
value: `duration.fromISO("PT0.5S")`,
expect: 0.5,
encode: `@encode("seconds", float32)`,
},
],
],
],
];

describe.each(allCases)("%s", (type, cases) => {
const casesWithLabel = cases.map((x) => ({
...x,
encodeLabel: x.encode ?? "default encoding",
}));
it.each(casesWithLabel)(
`serialize with $encodeLabel`,
async ({ value, expect: expected, encode }) => {
const result = await getJsonValueOfExample(`
];

describe.each(allCases)("%s", (type, cases) => {
const casesWithLabel = cases.map((x) => ({
...x,
encodeLabel: x.encode ?? "default encoding",
}));
it.each(casesWithLabel)(
`serialize with $encodeLabel`,
async ({ value, expect: expected, encode }) => {
const result = await getJsonValueOfExample(`
model TestModel {
@example(${value})
${encode ?? ""}
@test test: ${type};
}
`);
if (expected instanceof RegExp) {
expect(result).toMatch(expected);
} else {
expect(result).toEqual(expected);
if (expected instanceof RegExp) {
expect(result).toMatch(expected);
} else {
expect(result).toEqual(expected);
}
}
);
});
});

it("serialize nested models", async () => {
const result = await getJsonValueOfExample(`
@example(#{ a: #{ name: "one" } })
@test("test") model B {
a: A;
}

model A {
name: string;
}

`);

expect(result).toEqual({ a: { name: "one" } });
});

it("serialize nested models in arrays", async () => {
const result = await getJsonValueOfExample(`
@example(#{ items: #[#{ name: "one" }, #{ name: "two" }] })
@test("test") model B {
items: Array<A>;
}

model A {
name: string;
}

`);

expect(result).toEqual({ items: [{ name: "one" }, { name: "two" }] });
});

it("serialize nested record in arrays", async () => {
const result = await getJsonValueOfExample(`
@example(#{ items: #{one: #{ name: "one" }, two: #{ name: "two" }} })
@test("test") model B {
items: Record<A>;
}

model A {
name: string;
}

`);

expect(result).toEqual({ items: { one: { name: "one" }, two: { name: "two" } } });
});

it("serialize example as it is when type is unknown", async () => {
const result = await getJsonValueOfExample(`
@example(#{ a: #{ name: "one", other: 123 } })
@test("test") model B {
a: unknown
}
`);

expect(result).toEqual({ a: { name: "one", other: 123 } });
});

it("serialize example targetting a union using one of the types", async () => {
const result = await getJsonValueOfExample(`
@example(#{ a: #{ name: "one", other: 123 } })
@test("test") model Test {
a: A | B;
}

model A {
a: string
}

model B {
name: string;
other: int32;
}
);
`);

expect(result).toEqual({ a: { name: "one", other: 123 } });
});
});
Loading