@typespec/emitter-framework 0.13.0-dev.1 → 0.13.0-dev.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/csharp/components/index.d.ts +2 -0
- package/dist/src/csharp/components/index.d.ts.map +1 -1
- package/dist/src/csharp/components/index.js +2 -0
- package/dist/src/csharp/components/index.js.map +1 -1
- package/dist/src/csharp/components/json-converter/json-converter-resolver.d.ts +28 -0
- package/dist/src/csharp/components/json-converter/json-converter-resolver.d.ts.map +1 -0
- package/dist/src/csharp/components/json-converter/json-converter-resolver.js +82 -0
- package/dist/src/csharp/components/json-converter/json-converter-resolver.js.map +1 -0
- package/dist/src/csharp/components/json-converter/json-converter-resolver.test.d.ts +2 -0
- package/dist/src/csharp/components/json-converter/json-converter-resolver.test.d.ts.map +1 -0
- package/dist/src/csharp/components/json-converter/json-converter-resolver.test.js +134 -0
- package/dist/src/csharp/components/json-converter/json-converter-resolver.test.js.map +1 -0
- package/dist/src/csharp/components/json-converter/json-converter.d.ts +28 -0
- package/dist/src/csharp/components/json-converter/json-converter.d.ts.map +1 -0
- package/dist/src/csharp/components/json-converter/json-converter.js +168 -0
- package/dist/src/csharp/components/json-converter/json-converter.js.map +1 -0
- package/dist/src/csharp/components/json-converter/json-converter.test.d.ts +2 -0
- package/dist/src/csharp/components/json-converter/json-converter.test.d.ts.map +1 -0
- package/dist/src/csharp/components/json-converter/json-converter.test.js +134 -0
- package/dist/src/csharp/components/json-converter/json-converter.test.js.map +1 -0
- package/dist/src/csharp/components/property/property.d.ts +4 -1
- package/dist/src/csharp/components/property/property.d.ts.map +1 -1
- package/dist/src/csharp/components/property/property.js +46 -11
- package/dist/src/csharp/components/property/property.js.map +1 -1
- package/dist/src/csharp/components/property/property.test.js +125 -8
- package/dist/src/csharp/components/property/property.test.js.map +1 -1
- package/package.json +7 -7
- package/package.json.bak +7 -7
- package/src/csharp/components/index.ts +2 -0
- package/src/csharp/components/json-converter/json-converter-resolver.test.tsx +120 -0
- package/src/csharp/components/json-converter/json-converter-resolver.tsx +117 -0
- package/src/csharp/components/json-converter/json-converter.test.tsx +141 -0
- package/src/csharp/components/json-converter/json-converter.tsx +146 -0
- package/src/csharp/components/property/property.test.tsx +111 -7
- package/src/csharp/components/property/property.tsx +47 -6
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { Tester } from "#test/test-host.js";
|
|
2
|
+
import { code, For, List, namekey, type Children } from "@alloy-js/core";
|
|
3
|
+
import { createCSharpNamePolicy, SourceFile } from "@alloy-js/csharp";
|
|
4
|
+
import { t, type TesterInstance } from "@typespec/compiler/testing";
|
|
5
|
+
import { $ } from "@typespec/compiler/typekit";
|
|
6
|
+
import { beforeEach, expect, it } from "vitest";
|
|
7
|
+
import { Output } from "../../../core/components/output.jsx";
|
|
8
|
+
import { Property } from "../property/property.jsx";
|
|
9
|
+
import {
|
|
10
|
+
createJsonConverterResolver,
|
|
11
|
+
JsonConverterResolver,
|
|
12
|
+
useJsonConverterResolver,
|
|
13
|
+
type JsonConverterResolverOptions,
|
|
14
|
+
} from "./json-converter-resolver.jsx";
|
|
15
|
+
import { JsonConverter } from "./json-converter.jsx";
|
|
16
|
+
|
|
17
|
+
let tester: TesterInstance;
|
|
18
|
+
|
|
19
|
+
beforeEach(async () => {
|
|
20
|
+
tester = await Tester.createInstance();
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
function Wrapper(props: { children: Children }) {
|
|
24
|
+
const policy = createCSharpNamePolicy();
|
|
25
|
+
return (
|
|
26
|
+
<Output program={tester.program} namePolicy={policy}>
|
|
27
|
+
<SourceFile path="test.cs">{props.children}</SourceFile>
|
|
28
|
+
</Output>
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const fakeJsonConverterKey = namekey("FakeJsonConverter");
|
|
33
|
+
function FakeJsonConverter() {
|
|
34
|
+
return (
|
|
35
|
+
<JsonConverter
|
|
36
|
+
name={fakeJsonConverterKey}
|
|
37
|
+
type={$(tester.program).builtin.string}
|
|
38
|
+
decodeAndReturn={(reader) => {
|
|
39
|
+
return code`return ${reader}.GetString();`;
|
|
40
|
+
}}
|
|
41
|
+
encodeAndWrite={(writer, value) => {
|
|
42
|
+
return code`${writer}.WriteStringValue(${value});`;
|
|
43
|
+
}}
|
|
44
|
+
/>
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function createTestJsonConverterResolver() {
|
|
49
|
+
const option: JsonConverterResolverOptions = {
|
|
50
|
+
customConverters: [
|
|
51
|
+
{
|
|
52
|
+
type: $(tester.program).builtin.string,
|
|
53
|
+
encodeData: {
|
|
54
|
+
type: $(tester.program).builtin.string,
|
|
55
|
+
encoding: "fake-change",
|
|
56
|
+
},
|
|
57
|
+
info: {
|
|
58
|
+
converter: FakeJsonConverter,
|
|
59
|
+
namekey: fakeJsonConverterKey,
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
],
|
|
63
|
+
};
|
|
64
|
+
return createJsonConverterResolver(option);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
it("No resolved converters", async () => {
|
|
68
|
+
await tester.compile(t.code``);
|
|
69
|
+
expect(
|
|
70
|
+
<Wrapper>
|
|
71
|
+
<JsonConverterResolver.Provider value={createTestJsonConverterResolver()}>
|
|
72
|
+
{code`Resolved JsonConverter: ${useJsonConverterResolver()?.listResolvedJsonConverters().length}`}
|
|
73
|
+
</JsonConverterResolver.Provider>
|
|
74
|
+
</Wrapper>,
|
|
75
|
+
).toRenderTo(`Resolved JsonConverter: 0`);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("Resolve custom converter", async () => {
|
|
79
|
+
const r = await tester.compile(t.code`
|
|
80
|
+
model BaseModel {
|
|
81
|
+
@encode("fake-change", string)
|
|
82
|
+
${t.modelProperty("prop1")}: string;
|
|
83
|
+
}
|
|
84
|
+
`);
|
|
85
|
+
|
|
86
|
+
expect(
|
|
87
|
+
<Wrapper>
|
|
88
|
+
<JsonConverterResolver.Provider value={createTestJsonConverterResolver()}>
|
|
89
|
+
<List>
|
|
90
|
+
<Property type={r.prop1} jsonAttributes />
|
|
91
|
+
<br />
|
|
92
|
+
<For each={useJsonConverterResolver()?.listResolvedJsonConverters() ?? []}>
|
|
93
|
+
{(x) => <>{x.converter}</>}
|
|
94
|
+
</For>
|
|
95
|
+
</List>
|
|
96
|
+
</JsonConverterResolver.Provider>
|
|
97
|
+
</Wrapper>,
|
|
98
|
+
).toRenderTo(`
|
|
99
|
+
using System;
|
|
100
|
+
using System.Text.Json;
|
|
101
|
+
using System.Text.Json.Serialization;
|
|
102
|
+
|
|
103
|
+
[JsonPropertyName("prop1")]
|
|
104
|
+
[JsonConverter(typeof(FakeJsonConverter))]
|
|
105
|
+
public required string Prop1 { get; set; }
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
internal sealed class FakeJsonConverter : JsonConverter<string>
|
|
109
|
+
{
|
|
110
|
+
public override string Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
|
111
|
+
{
|
|
112
|
+
return reader.GetString();
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
public override void Write(Utf8JsonWriter writer, string value, JsonSerializerOptions options)
|
|
116
|
+
{
|
|
117
|
+
writer.WriteStringValue(value);
|
|
118
|
+
}
|
|
119
|
+
}`);
|
|
120
|
+
});
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { useTsp } from "#core/index.js";
|
|
2
|
+
import { createContext, namekey, useContext, type Children, type Namekey } from "@alloy-js/core";
|
|
3
|
+
import {
|
|
4
|
+
getTypeName,
|
|
5
|
+
type DurationKnownEncoding,
|
|
6
|
+
type EncodeData,
|
|
7
|
+
type Type,
|
|
8
|
+
} from "@typespec/compiler";
|
|
9
|
+
import { capitalize } from "@typespec/compiler/casing";
|
|
10
|
+
import { getNullableUnionInnerType } from "../utils/nullable-util.js";
|
|
11
|
+
import { TimeSpanIso8601JsonConverter, TimeSpanSecondsJsonConverter } from "./json-converter.jsx";
|
|
12
|
+
|
|
13
|
+
interface JsonConverterInfo {
|
|
14
|
+
namekey: Namekey;
|
|
15
|
+
converter: Children;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Help to resolve JsonConverter for a given type with encoding:
|
|
20
|
+
* 1. Avoid unnecessary duplicate JsonConverter declaration for the same type with same encoding.
|
|
21
|
+
* 2. Provide resolved JsonConverters to be generated centralized properly as needed.
|
|
22
|
+
* */
|
|
23
|
+
export interface useJsonConverterResolver {
|
|
24
|
+
resolveJsonConverter: (type: Type, encodeData: EncodeData) => JsonConverterInfo | undefined;
|
|
25
|
+
listResolvedJsonConverters: () => JsonConverterInfo[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const JsonConverterResolver = createContext<useJsonConverterResolver>();
|
|
29
|
+
|
|
30
|
+
export function useJsonConverterResolver(): useJsonConverterResolver | undefined {
|
|
31
|
+
return useContext(JsonConverterResolver);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface JsonConverterResolverOptions {
|
|
35
|
+
/** Custom JSON converters besides the built-in ones for known encode*/
|
|
36
|
+
customConverters?: { type: Type; encodeData: EncodeData; info: JsonConverterInfo }[];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function createJsonConverterResolver(
|
|
40
|
+
options?: JsonConverterResolverOptions,
|
|
41
|
+
): useJsonConverterResolver {
|
|
42
|
+
const resolvedConverters = new Map<string, JsonConverterInfo>();
|
|
43
|
+
const customConverters = new Map<string, JsonConverterInfo>();
|
|
44
|
+
|
|
45
|
+
if (options?.customConverters) {
|
|
46
|
+
for (const item of options.customConverters) {
|
|
47
|
+
const key = getJsonConverterKey(item.type, item.encodeData);
|
|
48
|
+
customConverters.set(key, item.info);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
resolveJsonConverter: (type: Type, encodeData: EncodeData) => {
|
|
54
|
+
const key = getJsonConverterKey(type, encodeData);
|
|
55
|
+
const found = resolvedConverters.get(key);
|
|
56
|
+
if (found) {
|
|
57
|
+
return found;
|
|
58
|
+
} else {
|
|
59
|
+
const resolved = customConverters.get(key) ?? resolveKnownJsonConverter(type, encodeData);
|
|
60
|
+
if (resolved) {
|
|
61
|
+
resolvedConverters.set(key, resolved);
|
|
62
|
+
return resolved;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return undefined;
|
|
66
|
+
},
|
|
67
|
+
listResolvedJsonConverters: () => Array.from(resolvedConverters.values()),
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
function getJsonConverterKey(type: Type, encodeData: EncodeData) {
|
|
71
|
+
return `type:${getTypeName(type)}-encoding:${encodeData.encoding}-encodeType:${getTypeName(encodeData.type)}`;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function resolveKnownJsonConverter(
|
|
75
|
+
type: Type,
|
|
76
|
+
encodeData: EncodeData,
|
|
77
|
+
): JsonConverterInfo | undefined {
|
|
78
|
+
const ENCODING_DURATION_SECONDS: DurationKnownEncoding = "seconds";
|
|
79
|
+
const ENCODING_DURATION_ISO8601: DurationKnownEncoding = "ISO8601";
|
|
80
|
+
const { $ } = useTsp();
|
|
81
|
+
// Unwrap nullable because JsonConverter<T> would handle null by default for us.
|
|
82
|
+
const unwrappedType = type.kind === "Union" ? (getNullableUnionInnerType(type) ?? type) : type;
|
|
83
|
+
if (
|
|
84
|
+
unwrappedType === $.builtin.duration &&
|
|
85
|
+
encodeData.encoding === ENCODING_DURATION_SECONDS &&
|
|
86
|
+
[
|
|
87
|
+
$.builtin.int16,
|
|
88
|
+
$.builtin.uint16,
|
|
89
|
+
$.builtin.int32,
|
|
90
|
+
$.builtin.uint32,
|
|
91
|
+
$.builtin.int64,
|
|
92
|
+
$.builtin.uint64,
|
|
93
|
+
$.builtin.float32,
|
|
94
|
+
$.builtin.float64,
|
|
95
|
+
].includes(encodeData.type)
|
|
96
|
+
) {
|
|
97
|
+
const capitalizedTypeName = capitalize(encodeData.type.name);
|
|
98
|
+
const key: Namekey = namekey(`TimeSpanSeconds${capitalizedTypeName}JsonConverter`);
|
|
99
|
+
return {
|
|
100
|
+
namekey: key,
|
|
101
|
+
converter: <TimeSpanSecondsJsonConverter name={key} encodeType={encodeData.type} />,
|
|
102
|
+
};
|
|
103
|
+
} else if (
|
|
104
|
+
unwrappedType === $.builtin.duration &&
|
|
105
|
+
encodeData.encoding === ENCODING_DURATION_ISO8601
|
|
106
|
+
) {
|
|
107
|
+
const key = namekey(`TimeSpanIso8601JsonConverter`);
|
|
108
|
+
return {
|
|
109
|
+
namekey: key,
|
|
110
|
+
converter: <TimeSpanIso8601JsonConverter name={key} />,
|
|
111
|
+
};
|
|
112
|
+
} else {
|
|
113
|
+
// TODO: support other known encodings
|
|
114
|
+
return undefined;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { Tester } from "#test/test-host.js";
|
|
2
|
+
import { code, namekey, type Children } from "@alloy-js/core";
|
|
3
|
+
import { createCSharpNamePolicy, SourceFile } from "@alloy-js/csharp";
|
|
4
|
+
import { t, type TesterInstance } from "@typespec/compiler/testing";
|
|
5
|
+
import { $ } from "@typespec/compiler/typekit";
|
|
6
|
+
import { beforeEach, describe, expect, it } from "vitest";
|
|
7
|
+
import { Output } from "../../../core/components/output.jsx";
|
|
8
|
+
import {
|
|
9
|
+
JsonConverter,
|
|
10
|
+
TimeSpanIso8601JsonConverter,
|
|
11
|
+
TimeSpanSecondsJsonConverter,
|
|
12
|
+
} from "./json-converter.jsx";
|
|
13
|
+
|
|
14
|
+
let tester: TesterInstance;
|
|
15
|
+
|
|
16
|
+
beforeEach(async () => {
|
|
17
|
+
tester = await Tester.createInstance();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
function Wrapper(props: { children: Children }) {
|
|
21
|
+
const policy = createCSharpNamePolicy();
|
|
22
|
+
return (
|
|
23
|
+
<Output program={tester.program} namePolicy={policy}>
|
|
24
|
+
<SourceFile path="test.cs">{props.children}</SourceFile>
|
|
25
|
+
</Output>
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const fakeJsonConverterKey = namekey("FakeJsonConverter");
|
|
30
|
+
function FakeJsonConverter() {
|
|
31
|
+
return (
|
|
32
|
+
<JsonConverter
|
|
33
|
+
name={fakeJsonConverterKey}
|
|
34
|
+
type={$(tester.program).builtin.string}
|
|
35
|
+
decodeAndReturn={(reader) => {
|
|
36
|
+
return code`return ${reader}.GetString();`;
|
|
37
|
+
}}
|
|
38
|
+
encodeAndWrite={(writer, value) => {
|
|
39
|
+
return code`${writer}.WriteStringValue(${value});`;
|
|
40
|
+
}}
|
|
41
|
+
/>
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
it("Custom JsonConverter", async () => {
|
|
46
|
+
await tester.compile(t.code``);
|
|
47
|
+
expect(
|
|
48
|
+
<Wrapper>
|
|
49
|
+
<FakeJsonConverter />
|
|
50
|
+
</Wrapper>,
|
|
51
|
+
).toRenderTo(`
|
|
52
|
+
using System;
|
|
53
|
+
using System.Text.Json;
|
|
54
|
+
using System.Text.Json.Serialization;
|
|
55
|
+
|
|
56
|
+
internal sealed class FakeJsonConverter : JsonConverter<string>
|
|
57
|
+
{
|
|
58
|
+
public override string Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
|
59
|
+
{
|
|
60
|
+
return reader.GetString();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
public override void Write(Utf8JsonWriter writer, string value, JsonSerializerOptions options)
|
|
64
|
+
{
|
|
65
|
+
writer.WriteStringValue(value);
|
|
66
|
+
}
|
|
67
|
+
}`);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
describe("Known JsonConverters", () => {
|
|
71
|
+
it("TimeSpanIso8601JsonConverter", async () => {
|
|
72
|
+
await tester.compile(t.code``);
|
|
73
|
+
expect(
|
|
74
|
+
<Wrapper>
|
|
75
|
+
<TimeSpanIso8601JsonConverter />
|
|
76
|
+
</Wrapper>,
|
|
77
|
+
).toRenderTo(`
|
|
78
|
+
using System;
|
|
79
|
+
using System.Text.Json;
|
|
80
|
+
using System.Text.Json.Serialization;
|
|
81
|
+
using System.Xml;
|
|
82
|
+
|
|
83
|
+
internal sealed class TimeSpanIso8601JsonConverter : JsonConverter<TimeSpan>
|
|
84
|
+
{
|
|
85
|
+
public override TimeSpan Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
|
86
|
+
{
|
|
87
|
+
var isoString = reader.GetString();
|
|
88
|
+
if( isoString == null)
|
|
89
|
+
{
|
|
90
|
+
throw new FormatException("Invalid ISO8601 duration string: null");
|
|
91
|
+
}
|
|
92
|
+
return XmlConvert.ToTimeSpan(isoString);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
public override void Write(Utf8JsonWriter writer, TimeSpan value, JsonSerializerOptions options)
|
|
96
|
+
{
|
|
97
|
+
writer.WriteStringValue(XmlConvert.ToString(value));
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
`);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
describe.each([
|
|
104
|
+
["int16", "(short)", "GetInt16"],
|
|
105
|
+
["uint16", "(ushort)", "GetUInt16"],
|
|
106
|
+
["int32", "(int)", "GetInt32"],
|
|
107
|
+
["uint32", "(uint)", "GetUInt32"],
|
|
108
|
+
["int64", "(long)", "GetInt64"],
|
|
109
|
+
["uint64", "(ulong)", "GetUInt64"],
|
|
110
|
+
["float32", "(float)", "GetSingle"],
|
|
111
|
+
["float64", "", "GetDouble"],
|
|
112
|
+
] as const)("%s", (typeName, jsonWriteType, jsonReaderMethod) => {
|
|
113
|
+
it("TimeSpanSecondsJsonConverter", async () => {
|
|
114
|
+
await tester.compile(t.code``);
|
|
115
|
+
const type = $(tester.program).builtin[typeName];
|
|
116
|
+
expect(
|
|
117
|
+
<Wrapper>
|
|
118
|
+
<TimeSpanSecondsJsonConverter encodeType={type} />
|
|
119
|
+
</Wrapper>,
|
|
120
|
+
).toRenderTo(`
|
|
121
|
+
using System;
|
|
122
|
+
using System.Text.Json;
|
|
123
|
+
using System.Text.Json.Serialization;
|
|
124
|
+
|
|
125
|
+
internal sealed class TimeSpanSeconds${typeName.charAt(0).toUpperCase() + typeName.slice(1)}JsonConverter : JsonConverter<TimeSpan>
|
|
126
|
+
{
|
|
127
|
+
public override TimeSpan Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
|
128
|
+
{
|
|
129
|
+
var seconds = reader.${jsonReaderMethod}();
|
|
130
|
+
return TimeSpan.FromSeconds(seconds);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
public override void Write(Utf8JsonWriter writer, TimeSpan value, JsonSerializerOptions options)
|
|
134
|
+
{
|
|
135
|
+
writer.WriteNumberValue(${jsonWriteType}value.TotalSeconds);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
`);
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
});
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { useTsp } from "#core/index.js";
|
|
2
|
+
import { code, List, namekey, type Namekey, type Refkey } from "@alloy-js/core";
|
|
3
|
+
import type { Children } from "@alloy-js/core/jsx-runtime";
|
|
4
|
+
import { ClassDeclaration, Method } from "@alloy-js/csharp";
|
|
5
|
+
import System, { Xml } from "@alloy-js/csharp/global/System";
|
|
6
|
+
import Json, { Serialization } from "@alloy-js/csharp/global/System/Text/Json";
|
|
7
|
+
import { type Type } from "@typespec/compiler";
|
|
8
|
+
import { capitalize } from "@typespec/compiler/casing";
|
|
9
|
+
import { TypeExpression } from "../type-expression.jsx";
|
|
10
|
+
|
|
11
|
+
interface JsonConverterProps {
|
|
12
|
+
name: string | Namekey;
|
|
13
|
+
type: Type;
|
|
14
|
+
refkey?: Refkey;
|
|
15
|
+
/** Decode and return value from reader*/
|
|
16
|
+
decodeAndReturn: (reader: Namekey, typeToConvert: Namekey, options: Namekey) => Children;
|
|
17
|
+
/** Encode the given value and send to writer*/
|
|
18
|
+
encodeAndWrite: (writer: Namekey, value: Namekey, options: Namekey) => Children;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Generate a Json converter class inheriting System.Text.Json.Serialization.JsonConverter<T> which can be used by System.Text.Json.Serialization.JsonConverterAttribute to provide custom serialization.
|
|
23
|
+
* @see https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json/converters-how-to#steps-to-follow-the-basic-pattern
|
|
24
|
+
*/
|
|
25
|
+
export function JsonConverter(props: JsonConverterProps) {
|
|
26
|
+
const readParamReader: Namekey = namekey("reader");
|
|
27
|
+
const readParamTypeToConvert: Namekey = namekey("typeToConvert");
|
|
28
|
+
const readParamOptions: Namekey = namekey("options");
|
|
29
|
+
const writeParamWriter: Namekey = namekey("writer");
|
|
30
|
+
const writeParamValue: Namekey = namekey("value");
|
|
31
|
+
const writeParamOptions: Namekey = namekey("options");
|
|
32
|
+
const propTypeExpression = code`${(<TypeExpression type={props.type} />)}`;
|
|
33
|
+
return (
|
|
34
|
+
<ClassDeclaration
|
|
35
|
+
refkey={props.refkey}
|
|
36
|
+
sealed
|
|
37
|
+
internal
|
|
38
|
+
name={props.name}
|
|
39
|
+
baseType={code`${Serialization.JsonConverter}<${propTypeExpression}>`}
|
|
40
|
+
>
|
|
41
|
+
<List doubleHardline>
|
|
42
|
+
<Method
|
|
43
|
+
name={"Read"}
|
|
44
|
+
public
|
|
45
|
+
override
|
|
46
|
+
parameters={[
|
|
47
|
+
{
|
|
48
|
+
name: readParamReader,
|
|
49
|
+
ref: true,
|
|
50
|
+
type: code`${Json.Utf8JsonReader}`,
|
|
51
|
+
},
|
|
52
|
+
{ name: readParamTypeToConvert, type: code`${System.Type}` },
|
|
53
|
+
{
|
|
54
|
+
name: readParamOptions,
|
|
55
|
+
type: code`${Json.JsonSerializerOptions}`,
|
|
56
|
+
},
|
|
57
|
+
]}
|
|
58
|
+
returns={propTypeExpression}
|
|
59
|
+
>
|
|
60
|
+
{code`${props.decodeAndReturn(readParamReader, readParamTypeToConvert, readParamOptions)}`}
|
|
61
|
+
</Method>
|
|
62
|
+
<Method
|
|
63
|
+
name={"Write"}
|
|
64
|
+
public
|
|
65
|
+
override
|
|
66
|
+
parameters={[
|
|
67
|
+
{ name: writeParamWriter, type: code`${Json.Utf8JsonWriter}` },
|
|
68
|
+
{
|
|
69
|
+
name: writeParamValue,
|
|
70
|
+
type: propTypeExpression,
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
name: writeParamOptions,
|
|
74
|
+
type: code`${Json.JsonSerializerOptions}`,
|
|
75
|
+
},
|
|
76
|
+
]}
|
|
77
|
+
>
|
|
78
|
+
{code`${props.encodeAndWrite(writeParamWriter, writeParamValue, writeParamOptions)}`}
|
|
79
|
+
</Method>
|
|
80
|
+
</List>
|
|
81
|
+
</ClassDeclaration>
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function TimeSpanSecondsJsonConverter(props: {
|
|
86
|
+
name?: string | Namekey;
|
|
87
|
+
refkey?: Refkey;
|
|
88
|
+
encodeType: Type;
|
|
89
|
+
}) {
|
|
90
|
+
const { $ } = useTsp();
|
|
91
|
+
const map: Map<Type, { jsonWriteType: string; jsonReaderMethod: string }> = new Map([
|
|
92
|
+
[$.builtin.int16, { jsonWriteType: "short", jsonReaderMethod: "GetInt16" }],
|
|
93
|
+
[$.builtin.uint16, { jsonWriteType: "ushort", jsonReaderMethod: "GetUInt16" }],
|
|
94
|
+
[$.builtin.int32, { jsonWriteType: "int", jsonReaderMethod: "GetInt32" }],
|
|
95
|
+
[$.builtin.uint32, { jsonWriteType: "uint", jsonReaderMethod: "GetUInt32" }],
|
|
96
|
+
[$.builtin.int64, { jsonWriteType: "long", jsonReaderMethod: "GetInt64" }],
|
|
97
|
+
[$.builtin.uint64, { jsonWriteType: "ulong", jsonReaderMethod: "GetUInt64" }],
|
|
98
|
+
[$.builtin.float32, { jsonWriteType: "float", jsonReaderMethod: "GetSingle" }],
|
|
99
|
+
[$.builtin.float64, { jsonWriteType: "double", jsonReaderMethod: "GetDouble" }],
|
|
100
|
+
]);
|
|
101
|
+
if (props.encodeType.kind !== "Scalar" || !map.has(props.encodeType)) {
|
|
102
|
+
throw new Error(
|
|
103
|
+
`TimeSpanSecondsJsonConverter only supports encodeType of int16, uint16, int32, uint32, int64, uint64, float32, or float64. Received: kind = ${props.encodeType.kind} ${props.encodeType.kind === "Scalar" ? `name = ${props.encodeType.name}` : ""}`,
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
const found = map.get(props.encodeType)!;
|
|
107
|
+
const capitalizedTypeName = capitalize(props.encodeType.name);
|
|
108
|
+
const defaultName = `TimeSpanSeconds${capitalizedTypeName}JsonConverter`;
|
|
109
|
+
|
|
110
|
+
return (
|
|
111
|
+
<JsonConverter
|
|
112
|
+
refkey={props.refkey}
|
|
113
|
+
name={props.name ?? namekey(defaultName)}
|
|
114
|
+
type={$.builtin.duration}
|
|
115
|
+
decodeAndReturn={(reader) => {
|
|
116
|
+
return code`var seconds = ${reader}.${found.jsonReaderMethod}();
|
|
117
|
+
return ${System.TimeSpan}.FromSeconds(seconds);`;
|
|
118
|
+
}}
|
|
119
|
+
encodeAndWrite={(writer, value) => {
|
|
120
|
+
return code`${writer}.WriteNumberValue(${found.jsonWriteType === "double" || `(${found.jsonWriteType})`}${value}.TotalSeconds);`;
|
|
121
|
+
}}
|
|
122
|
+
/>
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function TimeSpanIso8601JsonConverter(props: { name?: string | Namekey; refkey?: Refkey }) {
|
|
127
|
+
const { $ } = useTsp();
|
|
128
|
+
return (
|
|
129
|
+
<JsonConverter
|
|
130
|
+
refkey={props.refkey}
|
|
131
|
+
name={props.name ?? namekey("TimeSpanIso8601JsonConverter")}
|
|
132
|
+
type={$.builtin.duration}
|
|
133
|
+
decodeAndReturn={(reader) => {
|
|
134
|
+
return code`var isoString = ${reader}.GetString();
|
|
135
|
+
if( isoString == null)
|
|
136
|
+
{
|
|
137
|
+
throw new ${System.FormatException}("Invalid ISO8601 duration string: null");
|
|
138
|
+
}
|
|
139
|
+
return ${Xml.XmlConvert}.ToTimeSpan(isoString);`;
|
|
140
|
+
}}
|
|
141
|
+
encodeAndWrite={(writer, value) => {
|
|
142
|
+
return code`${writer}.WriteStringValue(${Xml.XmlConvert}.ToString(${value}));`;
|
|
143
|
+
}}
|
|
144
|
+
/>
|
|
145
|
+
);
|
|
146
|
+
}
|
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
import { Tester } from "#test/test-host.js";
|
|
2
|
-
import { List, type Children } from "@alloy-js/core";
|
|
2
|
+
import { For, List, type Children } from "@alloy-js/core";
|
|
3
3
|
import { ClassDeclaration, createCSharpNamePolicy, SourceFile } from "@alloy-js/csharp";
|
|
4
4
|
import { t, type TesterInstance } from "@typespec/compiler/testing";
|
|
5
5
|
import { beforeEach, describe, expect, it } from "vitest";
|
|
6
6
|
import { Output } from "../../../core/components/output.jsx";
|
|
7
|
+
import {
|
|
8
|
+
createJsonConverterResolver,
|
|
9
|
+
JsonConverterResolver,
|
|
10
|
+
useJsonConverterResolver,
|
|
11
|
+
} from "../json-converter/json-converter-resolver.jsx";
|
|
7
12
|
import { Property } from "./property.jsx";
|
|
8
13
|
|
|
9
14
|
let tester: TesterInstance;
|
|
@@ -93,9 +98,11 @@ describe("jsonAttributes", () => {
|
|
|
93
98
|
<Property type={prop1} jsonAttributes />
|
|
94
99
|
</Wrapper>,
|
|
95
100
|
).toRenderTo(`
|
|
101
|
+
using System.Text.Json.Serialization;
|
|
102
|
+
|
|
96
103
|
class Test
|
|
97
104
|
{
|
|
98
|
-
[
|
|
105
|
+
[JsonPropertyName("prop1")]
|
|
99
106
|
public required string Prop1 { get; set; }
|
|
100
107
|
}
|
|
101
108
|
`);
|
|
@@ -114,11 +121,13 @@ describe("jsonAttributes", () => {
|
|
|
114
121
|
<Property type={prop1} jsonAttributes />
|
|
115
122
|
</Wrapper>,
|
|
116
123
|
).toRenderTo(`
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
124
|
+
using System.Text.Json.Serialization;
|
|
125
|
+
|
|
126
|
+
class Test
|
|
127
|
+
{
|
|
128
|
+
[JsonPropertyName("prop_1")]
|
|
129
|
+
public required string Prop1 { get; set; }
|
|
130
|
+
}
|
|
122
131
|
`);
|
|
123
132
|
});
|
|
124
133
|
|
|
@@ -177,4 +186,99 @@ describe("jsonAttributes", () => {
|
|
|
177
186
|
}
|
|
178
187
|
`);
|
|
179
188
|
});
|
|
189
|
+
|
|
190
|
+
it("json converter: duration -> seconds(int32)", async () => {
|
|
191
|
+
const r = await tester.compile(t.code`
|
|
192
|
+
model BaseModel {
|
|
193
|
+
@encode(DurationKnownEncoding.seconds, int32)
|
|
194
|
+
${t.modelProperty("prop1")}?: duration;
|
|
195
|
+
@encode(DurationKnownEncoding.seconds, float64)
|
|
196
|
+
${t.modelProperty("prop2")}: duration;
|
|
197
|
+
@encode(DurationKnownEncoding.ISO8601, string)
|
|
198
|
+
${t.modelProperty("prop3")}: duration;
|
|
199
|
+
}
|
|
200
|
+
`);
|
|
201
|
+
|
|
202
|
+
expect(
|
|
203
|
+
<Wrapper>
|
|
204
|
+
<JsonConverterResolver.Provider value={createJsonConverterResolver()}>
|
|
205
|
+
<List>
|
|
206
|
+
<Property type={r.prop1} jsonAttributes />
|
|
207
|
+
<Property type={r.prop2} jsonAttributes />
|
|
208
|
+
<Property type={r.prop3} jsonAttributes />
|
|
209
|
+
<hbr />
|
|
210
|
+
// JsonConverter wont work as nested class, but good enough for test to verify the
|
|
211
|
+
generated code.
|
|
212
|
+
<For each={useJsonConverterResolver()?.listResolvedJsonConverters() ?? []}>
|
|
213
|
+
{(x) => <>{x.converter}</>}
|
|
214
|
+
</For>
|
|
215
|
+
</List>
|
|
216
|
+
</JsonConverterResolver.Provider>
|
|
217
|
+
</Wrapper>,
|
|
218
|
+
).toRenderTo(`
|
|
219
|
+
using System;
|
|
220
|
+
using System.Text.Json;
|
|
221
|
+
using System.Text.Json.Serialization;
|
|
222
|
+
using System.Xml;
|
|
223
|
+
|
|
224
|
+
class Test
|
|
225
|
+
{
|
|
226
|
+
[JsonPropertyName("prop1")]
|
|
227
|
+
[JsonConverter(typeof(TimeSpanSecondsInt32JsonConverter))]
|
|
228
|
+
public TimeSpan? Prop1 { get; set; }
|
|
229
|
+
[JsonPropertyName("prop2")]
|
|
230
|
+
[JsonConverter(typeof(TimeSpanSecondsFloat64JsonConverter))]
|
|
231
|
+
public required TimeSpan Prop2 { get; set; }
|
|
232
|
+
[JsonPropertyName("prop3")]
|
|
233
|
+
[JsonConverter(typeof(TimeSpanIso8601JsonConverter))]
|
|
234
|
+
public required TimeSpan Prop3 { get; set; }
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
// JsonConverter wont work as nested class, but good enough for test to verify the generated code.
|
|
238
|
+
internal sealed class TimeSpanSecondsInt32JsonConverter : JsonConverter<TimeSpan>
|
|
239
|
+
{
|
|
240
|
+
public override TimeSpan Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
|
241
|
+
{
|
|
242
|
+
var seconds = reader.GetInt32();
|
|
243
|
+
return TimeSpan.FromSeconds(seconds);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
public override void Write(Utf8JsonWriter writer, TimeSpan value, JsonSerializerOptions options)
|
|
247
|
+
{
|
|
248
|
+
writer.WriteNumberValue((int)value.TotalSeconds);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
internal sealed class TimeSpanSecondsFloat64JsonConverter : JsonConverter<TimeSpan>
|
|
252
|
+
{
|
|
253
|
+
public override TimeSpan Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
|
254
|
+
{
|
|
255
|
+
var seconds = reader.GetDouble();
|
|
256
|
+
return TimeSpan.FromSeconds(seconds);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
public override void Write(Utf8JsonWriter writer, TimeSpan value, JsonSerializerOptions options)
|
|
260
|
+
{
|
|
261
|
+
writer.WriteNumberValue(value.TotalSeconds);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
internal sealed class TimeSpanIso8601JsonConverter : JsonConverter<TimeSpan>
|
|
265
|
+
{
|
|
266
|
+
public override TimeSpan Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
|
267
|
+
{
|
|
268
|
+
var isoString = reader.GetString();
|
|
269
|
+
if( isoString == null)
|
|
270
|
+
{
|
|
271
|
+
throw new FormatException("Invalid ISO8601 duration string: null");
|
|
272
|
+
}
|
|
273
|
+
return XmlConvert.ToTimeSpan(isoString);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
public override void Write(Utf8JsonWriter writer, TimeSpan value, JsonSerializerOptions options)
|
|
277
|
+
{
|
|
278
|
+
writer.WriteStringValue(XmlConvert.ToString(value));
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
`);
|
|
283
|
+
});
|
|
180
284
|
});
|