@contractkit/plugin-csharp 0.1.5 → 0.2.0
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/.turbo/turbo-build$colon$ci.log +5 -5
- package/.turbo/turbo-test$colon$ci.log +16 -15
- package/CHANGELOG.md +28 -0
- package/README.md +86 -13
- package/dist/codegen-client.d.ts +3 -0
- package/dist/codegen-client.d.ts.map +1 -1
- package/dist/codegen-models.d.ts +15 -0
- package/dist/codegen-models.d.ts.map +1 -1
- package/dist/index.d.ts +24 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +566 -19
- package/dist/index.js.map +1 -1
- package/dist/runtime-converters.d.ts +8 -2
- package/dist/runtime-converters.d.ts.map +1 -1
- package/dist/runtime-polyfills.d.ts +20 -0
- package/dist/runtime-polyfills.d.ts.map +1 -0
- package/dist/runtime.d.ts.map +1 -1
- package/dist/scaffold.d.ts +18 -4
- package/dist/scaffold.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/codegen-client.ts +19 -9
- package/src/codegen-models.ts +53 -2
- package/src/index.ts +74 -5
- package/src/runtime-converters.ts +135 -4
- package/src/runtime-polyfills.ts +352 -0
- package/src/runtime.ts +24 -0
- package/src/scaffold.ts +45 -6
- package/tests/codegen-client.test.ts +38 -4
- package/tests/codegen-models.test.ts +40 -3
- package/tests/index.test.ts +43 -0
- package/tests/runtime.test.ts +83 -1
- package/tests/scaffold.test.ts +22 -0
package/tests/index.test.ts
CHANGED
|
@@ -64,6 +64,25 @@ describe('assertValidConfig', () => {
|
|
|
64
64
|
expect(() => assertValidConfig({ scaffold: 'yes' as never })).toThrow(/scaffold must be a boolean/);
|
|
65
65
|
expect(() => assertValidConfig({ includeInternal: 1 as never })).toThrow(/includeInternal must be a boolean/);
|
|
66
66
|
});
|
|
67
|
+
|
|
68
|
+
it('accepts the frameworks the scaffold knows, in either order', () => {
|
|
69
|
+
expect(() => assertValidConfig({ targetFrameworks: ['net10.0'] })).not.toThrow();
|
|
70
|
+
expect(() => assertValidConfig({ targetFrameworks: ['netstandard2.0'] })).not.toThrow();
|
|
71
|
+
expect(() => assertValidConfig({ targetFrameworks: ['netstandard2.0', 'net10.0'] })).not.toThrow();
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('rejects a targetFrameworks list that is empty, unknown, or repeated', () => {
|
|
75
|
+
expect(() => assertValidConfig({ targetFrameworks: [] })).toThrow(/must be a non-empty array/);
|
|
76
|
+
expect(() => assertValidConfig({ targetFrameworks: 'net10.0' as never })).toThrow(/must be a non-empty array/);
|
|
77
|
+
expect(() => assertValidConfig({ targetFrameworks: ['net48' as never] })).toThrow(/is not supported/);
|
|
78
|
+
expect(() => assertValidConfig({ targetFrameworks: ['net10.0', 'net10.0'] })).toThrow(/lists 'net10.0' twice/);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('accepts either date mapping and rejects anything else', () => {
|
|
82
|
+
expect(() => assertValidConfig({ dateTypes: 'dateonly' })).not.toThrow();
|
|
83
|
+
expect(() => assertValidConfig({ dateTypes: 'datetime' })).not.toThrow();
|
|
84
|
+
expect(() => assertValidConfig({ dateTypes: 'DateTime' as never })).toThrow(/dateTypes "DateTime" is not supported/);
|
|
85
|
+
});
|
|
67
86
|
});
|
|
68
87
|
|
|
69
88
|
describe('generateTargets', () => {
|
|
@@ -115,6 +134,30 @@ describe('generateTargets', () => {
|
|
|
115
134
|
expect(on.ifAbsent).toEqual(['cssdk/AcmeSdk.csproj']);
|
|
116
135
|
});
|
|
117
136
|
|
|
137
|
+
it('emits the polyfills only for a build that includes netstandard2.0', async () => {
|
|
138
|
+
const off = makeCtx();
|
|
139
|
+
await createCSharpSdkPlugin({ baseDir: 'cssdk' }, ROOT_DIR).generateTargets!(INPUTS, off);
|
|
140
|
+
expect(off.emitted.has('cssdk/Runtime/Polyfills.cs')).toBe(false);
|
|
141
|
+
|
|
142
|
+
const on = makeCtx();
|
|
143
|
+
await createCSharpSdkPlugin({ baseDir: 'cssdk', namespace: 'Acme.Sdk', targetFrameworks: ['netstandard2.0', 'net10.0'] }, ROOT_DIR)
|
|
144
|
+
.generateTargets!(INPUTS, on);
|
|
145
|
+
const polyfills = on.emitted.get('cssdk/Runtime/Polyfills.cs');
|
|
146
|
+
expect(polyfills).toContain('namespace Acme.Sdk.Runtime');
|
|
147
|
+
expect(polyfills).toContain('public readonly struct DateOnly');
|
|
148
|
+
// The whole file is conditional, so the net10.0 leg of the same build compiles none of it.
|
|
149
|
+
expect(polyfills?.includes('#if NETSTANDARD2_0')).toBe(true);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it('multi-targets the scaffolded project file when the config asks for netstandard2.0', async () => {
|
|
153
|
+
const ctx = makeCtx();
|
|
154
|
+
await createCSharpSdkPlugin(
|
|
155
|
+
{ baseDir: 'cssdk', sdkName: 'AcmeSdk', scaffold: true, targetFrameworks: ['netstandard2.0', 'net10.0'] },
|
|
156
|
+
ROOT_DIR,
|
|
157
|
+
).generateTargets!(INPUTS, ctx);
|
|
158
|
+
expect(ctx.emitted.get('cssdk/AcmeSdk.csproj')).toContain('<TargetFrameworks>netstandard2.0;net10.0</TargetFrameworks>');
|
|
159
|
+
});
|
|
160
|
+
|
|
118
161
|
it('surfaces an invalid namespace as a build error rather than emitting broken C#', async () => {
|
|
119
162
|
const ctx = makeCtx();
|
|
120
163
|
const plugin = createCSharpSdkPlugin({ namespace: 'Acme.class.Sdk' }, ROOT_DIR);
|
package/tests/runtime.test.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest';
|
|
2
2
|
import { generateRuntimeCs } from '../src/runtime.js';
|
|
3
3
|
import { generateConvertersCs } from '../src/runtime-converters.js';
|
|
4
|
+
import { generatePolyfillsCs } from '../src/runtime-polyfills.js';
|
|
4
5
|
import { generateSdkCs } from '../src/codegen-sdk.js';
|
|
5
6
|
|
|
6
7
|
describe('generateRuntimeCs', () => {
|
|
@@ -23,6 +24,10 @@ describe('generateRuntimeCs', () => {
|
|
|
23
24
|
expect(out).toContain('public IEnumerable<KeyValuePair<string, string>> Params<T>(T value)');
|
|
24
25
|
});
|
|
25
26
|
|
|
27
|
+
it('carries a PATCH verb of its own, the one HttpMethod does not spell on every framework', () => {
|
|
28
|
+
expect(out).toContain('public static readonly HttpMethod Patch = new HttpMethod("PATCH");');
|
|
29
|
+
});
|
|
30
|
+
|
|
26
31
|
it('offers a content factory per request body kind', () => {
|
|
27
32
|
expect(out).toContain('public HttpContent JsonContent<T>(T value, string mediaType)');
|
|
28
33
|
expect(out).toContain('public HttpContent FormContent<T>(T value)');
|
|
@@ -53,6 +58,55 @@ describe('generateRuntimeCs', () => {
|
|
|
53
58
|
expect(out).toContain('_ownsClient = options.HttpClient is null;');
|
|
54
59
|
expect(out).toContain('if (_ownsClient) Client.Dispose();');
|
|
55
60
|
});
|
|
61
|
+
|
|
62
|
+
it('takes the older spelling of the two members netstandard2.0 declares differently', () => {
|
|
63
|
+
// HttpRequestException gained a status-carrying constructor in .NET 5, and
|
|
64
|
+
// ReadAsByteArrayAsync a cancellable overload; both have a fallback on the old framework.
|
|
65
|
+
expect(out).toContain(' : base(message ?? $"Request failed with status {status}")\n#else');
|
|
66
|
+
expect(out).toContain('var bytes = await message.Content.ReadAsByteArrayAsync().ConfigureAwait(false);');
|
|
67
|
+
// The status conversion has no caller on the old framework, so it is not compiled there.
|
|
68
|
+
expect(out).toContain('#if !NETSTANDARD2_0\n private static HttpStatusCode? ToStatusCode(int status)');
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
describe('generatePolyfillsCs', () => {
|
|
73
|
+
const out = generatePolyfillsCs('Acme.Sdk');
|
|
74
|
+
|
|
75
|
+
it('compiles on netstandard2.0 alone, so a project with no old leg carries nothing', () => {
|
|
76
|
+
expect(out).toContain('#if NETSTANDARD2_0');
|
|
77
|
+
expect(out.trimEnd().endsWith('#endif')).toBe(true);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('declares the attributes the compiler looks for before it accepts init and required', () => {
|
|
81
|
+
expect(out).toContain('namespace System.Runtime.CompilerServices');
|
|
82
|
+
expect(out).toContain('internal static class IsExternalInit');
|
|
83
|
+
expect(out).toContain('internal sealed class RequiredMemberAttribute : Attribute');
|
|
84
|
+
expect(out).toContain('internal sealed class CompilerFeatureRequiredAttribute : Attribute');
|
|
85
|
+
expect(out).toContain('internal sealed class SetsRequiredMembersAttribute : Attribute');
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('declares the date and time types in the SDK’s own namespace, not in System', () => {
|
|
89
|
+
expect(out).toContain('namespace Acme.Sdk.Runtime');
|
|
90
|
+
expect(out).toContain('public readonly struct DateOnly : IEquatable<DateOnly>, IComparable<DateOnly>, IComparable');
|
|
91
|
+
expect(out).toContain('public readonly struct TimeOnly : IEquatable<TimeOnly>, IComparable<TimeOnly>, IComparable');
|
|
92
|
+
expect(out).not.toContain('namespace System;');
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('writes the same wire form the framework types do, so one service reads either leg', () => {
|
|
96
|
+
expect(out).toContain(`_value.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture)`);
|
|
97
|
+
expect(out).toContain(`_value.ToString("hh':'mm':'ss", CultureInfo.InvariantCulture)`);
|
|
98
|
+
// Seven digits, not the trimmed form: System.Text.Json writes 09:30:15.2500000.
|
|
99
|
+
expect(out).toContain(`_value.ToString("hh':'mm':'ss'.'fffffff", CultureInfo.InvariantCulture)`);
|
|
100
|
+
// Reading stays lenient, so a body written by a trimming client still parses.
|
|
101
|
+
expect(out).toContain(`{ "HH:mm:ss.FFFFFFF", "HH:mm" }`);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('converts to the types a XAML picker binds to', () => {
|
|
105
|
+
expect(out).toContain('public DateTime ToDateTime()');
|
|
106
|
+
expect(out).toContain('public TimeSpan ToTimeSpan()');
|
|
107
|
+
expect(out).toContain('public static TimeOnly FromTimeSpan(TimeSpan value)');
|
|
108
|
+
expect(out).toContain('public static DateOnly FromDateTime(DateTime value)');
|
|
109
|
+
});
|
|
56
110
|
});
|
|
57
111
|
|
|
58
112
|
describe('generateConvertersCs', () => {
|
|
@@ -73,13 +127,41 @@ describe('generateConvertersCs', () => {
|
|
|
73
127
|
|
|
74
128
|
it('reads the bigint forms every other ContractKit SDK writes', () => {
|
|
75
129
|
expect(out).toContain("BigInteger.Parse(text.TrimEnd('n'), NumberStyles.Integer, CultureInfo.InvariantCulture)");
|
|
76
|
-
|
|
130
|
+
// Both branches copy to an array: the span overload of GetString is not on every framework.
|
|
131
|
+
expect(out).toContain('reader.HasValueSequence ? reader.ValueSequence.ToArray() : reader.ValueSpan.ToArray()');
|
|
77
132
|
});
|
|
78
133
|
|
|
79
134
|
it('carries a duration as ISO 8601 rather than the BCL default', () => {
|
|
80
135
|
expect(out).toContain('XmlConvert.ToTimeSpan(text)');
|
|
81
136
|
expect(out).toContain('writer.WriteStringValue(XmlConvert.ToString(value));');
|
|
82
137
|
});
|
|
138
|
+
|
|
139
|
+
it('registers a date and time converter only where those types are the SDK’s own', () => {
|
|
140
|
+
expect(out).toContain('#if NETSTANDARD2_0');
|
|
141
|
+
expect(out).toContain('options.Converters.Add(new DateOnlyConverter());');
|
|
142
|
+
expect(out).toContain('options.Converters.Add(new TimeOnlyConverter());');
|
|
143
|
+
expect(out).toContain('public sealed class DateOnlyConverter : JsonConverter<DateOnly>');
|
|
144
|
+
expect(out).toContain('public sealed class TimeOnlyConverter : JsonConverter<TimeOnly>');
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
describe('under dateTypes: datetime', () => {
|
|
148
|
+
const dt = generateConvertersCs('Acme.Sdk', 'datetime');
|
|
149
|
+
|
|
150
|
+
it('carries a date as the date part of a DateTime, on every framework', () => {
|
|
151
|
+
expect(dt).toContain('options.Converters.Add(new IsoDateConverter());');
|
|
152
|
+
expect(dt).toContain('public sealed class IsoDateConverter : JsonConverter<DateTime>');
|
|
153
|
+
expect(dt).toContain(`writer.WriteStringValue(value.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture));`);
|
|
154
|
+
// Options-level rather than per-property, which is only safe because `datetime` maps to
|
|
155
|
+
// DateTimeOffset and so nothing else in a generated model is a DateTime.
|
|
156
|
+
expect(dt).not.toContain('JsonConverter<DateTimeOffset>');
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('drops the DateOnly converter, which no generated type would reach any more', () => {
|
|
160
|
+
expect(dt).not.toContain('DateOnlyConverter');
|
|
161
|
+
// TimeOnly is unchanged by the option, so its netstandard2.0 converter stays.
|
|
162
|
+
expect(dt).toContain('options.Converters.Add(new TimeOnlyConverter());');
|
|
163
|
+
});
|
|
164
|
+
});
|
|
83
165
|
});
|
|
84
166
|
|
|
85
167
|
describe('generateSdkCs', () => {
|
package/tests/scaffold.test.ts
CHANGED
|
@@ -25,4 +25,26 @@ describe('generateCsproj', () => {
|
|
|
25
25
|
it('says it is never regenerated, since the file is the user’s after the first run', () => {
|
|
26
26
|
expect(generateCsproj('Acme.Sdk', 'AcmeSdk')).toContain('never regenerated');
|
|
27
27
|
});
|
|
28
|
+
|
|
29
|
+
it('keeps the single-framework output identical however the one framework is spelled', () => {
|
|
30
|
+
expect(generateCsproj('Acme.Sdk', 'AcmeSdk', ['net10.0'])).toBe(generateCsproj('Acme.Sdk', 'AcmeSdk'));
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('multi-targets, pins the language version and references System.Text.Json for netstandard2.0', () => {
|
|
34
|
+
const out = generateCsproj('Acme.Sdk', 'AcmeSdk', ['netstandard2.0', 'net10.0']);
|
|
35
|
+
expect(out).toContain('<TargetFrameworks>netstandard2.0;net10.0</TargetFrameworks>');
|
|
36
|
+
expect(out).not.toContain('<TargetFramework>');
|
|
37
|
+
// Both conditioned on the old framework, so net10.0 keeps its default language version and
|
|
38
|
+
// its dependency-free restore.
|
|
39
|
+
expect(out).toContain(`<PropertyGroup Condition="'$(TargetFramework)' == 'netstandard2.0'">`);
|
|
40
|
+
expect(out).toContain(`<LangVersion>${SCAFFOLD_VERSIONS.netstandardLangVersion}</LangVersion>`);
|
|
41
|
+
expect(out).toContain(`<ItemGroup Condition="'$(TargetFramework)' == 'netstandard2.0'">`);
|
|
42
|
+
expect(out).toContain(`<PackageReference Include="System.Text.Json" Version="${SCAFFOLD_VERSIONS.systemTextJson}" />`);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('can target netstandard2.0 on its own', () => {
|
|
46
|
+
const out = generateCsproj('Acme.Sdk', 'AcmeSdk', ['netstandard2.0']);
|
|
47
|
+
expect(out).toContain('<TargetFramework>netstandard2.0</TargetFramework>');
|
|
48
|
+
expect(out).toContain('<PackageReference Include="System.Text.Json"');
|
|
49
|
+
});
|
|
28
50
|
});
|