@contractkit/plugin-csharp 0.0.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.
Files changed (48) hide show
  1. package/.turbo/turbo-build$colon$ci.log +13 -0
  2. package/.turbo/turbo-build.log +12 -0
  3. package/.turbo/turbo-format.log +34 -0
  4. package/.turbo/turbo-test.log +17 -0
  5. package/CHANGELOG.md +1 -0
  6. package/LICENSE +21 -0
  7. package/README.md +173 -0
  8. package/dist/codegen-client.d.ts +35 -0
  9. package/dist/codegen-client.d.ts.map +1 -0
  10. package/dist/codegen-models.d.ts +75 -0
  11. package/dist/codegen-models.d.ts.map +1 -0
  12. package/dist/codegen-sdk.d.ts +13 -0
  13. package/dist/codegen-sdk.d.ts.map +1 -0
  14. package/dist/hoist.d.ts +53 -0
  15. package/dist/hoist.d.ts.map +1 -0
  16. package/dist/index.d.ts +30 -0
  17. package/dist/index.d.ts.map +1 -0
  18. package/dist/index.js +2569 -0
  19. package/dist/index.js.map +1 -0
  20. package/dist/naming.d.ts +89 -0
  21. package/dist/naming.d.ts.map +1 -0
  22. package/dist/runtime-converters.d.ts +15 -0
  23. package/dist/runtime-converters.d.ts.map +1 -0
  24. package/dist/runtime.d.ts +10 -0
  25. package/dist/runtime.d.ts.map +1 -0
  26. package/dist/scaffold.d.ts +26 -0
  27. package/dist/scaffold.d.ts.map +1 -0
  28. package/eslint.config.js +6 -0
  29. package/package.json +48 -0
  30. package/src/codegen-client.ts +680 -0
  31. package/src/codegen-models.ts +909 -0
  32. package/src/codegen-sdk.ts +52 -0
  33. package/src/hoist.ts +402 -0
  34. package/src/index.ts +373 -0
  35. package/src/naming.ts +262 -0
  36. package/src/runtime-converters.ts +147 -0
  37. package/src/runtime.ts +381 -0
  38. package/src/scaffold.ts +41 -0
  39. package/tests/codegen-client.test.ts +275 -0
  40. package/tests/codegen-models.test.ts +410 -0
  41. package/tests/helpers.ts +202 -0
  42. package/tests/hoist.test.ts +92 -0
  43. package/tests/index.test.ts +124 -0
  44. package/tests/naming.test.ts +133 -0
  45. package/tests/runtime.test.ts +104 -0
  46. package/tests/scaffold.test.ts +28 -0
  47. package/tsconfig.json +9 -0
  48. package/vitest.config.ts +14 -0
package/dist/index.js ADDED
@@ -0,0 +1,2569 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
3
+
4
+ // src/index.ts
5
+ import { dirname, join, resolve } from "path";
6
+ import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, rmdirSync, writeFileSync } from "fs";
7
+ import { buildModelIndex as buildModelIndex2, collectTransitiveModelRefs, collectTypeRefs as collectTypeRefs2, emptyIncrementalManifest, hashFingerprint, parseIncrementalManifest, runIncrementalCodegen, serializeIncrementalManifest } from "@contractkit/core";
8
+
9
+ // src/codegen-models.ts
10
+ import { buildModelIndex, computeModelsWithInput, resolveEffectiveFields, topoSortModels } from "@contractkit/core";
11
+
12
+ // src/naming.ts
13
+ var CSHARP_KEYWORDS = /* @__PURE__ */ new Set([
14
+ "abstract",
15
+ "as",
16
+ "base",
17
+ "bool",
18
+ "break",
19
+ "byte",
20
+ "case",
21
+ "catch",
22
+ "char",
23
+ "checked",
24
+ "class",
25
+ "const",
26
+ "continue",
27
+ "decimal",
28
+ "default",
29
+ "delegate",
30
+ "do",
31
+ "double",
32
+ "else",
33
+ "enum",
34
+ "event",
35
+ "explicit",
36
+ "extern",
37
+ "false",
38
+ "finally",
39
+ "fixed",
40
+ "float",
41
+ "for",
42
+ "foreach",
43
+ "goto",
44
+ "if",
45
+ "implicit",
46
+ "in",
47
+ "int",
48
+ "interface",
49
+ "internal",
50
+ "is",
51
+ "lock",
52
+ "long",
53
+ "namespace",
54
+ "new",
55
+ "null",
56
+ "object",
57
+ "operator",
58
+ "out",
59
+ "override",
60
+ "params",
61
+ "private",
62
+ "protected",
63
+ "public",
64
+ "readonly",
65
+ "ref",
66
+ "return",
67
+ "sbyte",
68
+ "sealed",
69
+ "short",
70
+ "sizeof",
71
+ "stackalloc",
72
+ "static",
73
+ "string",
74
+ "struct",
75
+ "switch",
76
+ "this",
77
+ "throw",
78
+ "true",
79
+ "try",
80
+ "typeof",
81
+ "uint",
82
+ "ulong",
83
+ "unchecked",
84
+ "unsafe",
85
+ "ushort",
86
+ "using",
87
+ "virtual",
88
+ "void",
89
+ "volatile",
90
+ "while"
91
+ ]);
92
+ var RESERVED_MEMBER_NAMES = /* @__PURE__ */ new Set([
93
+ "Equals",
94
+ "GetHashCode",
95
+ "GetType",
96
+ "ToString",
97
+ "EqualityContract",
98
+ "PrintMembers"
99
+ ]);
100
+ function escapeCSharpIdentifier(name) {
101
+ return CSHARP_KEYWORDS.has(name) ? `@${name}` : name;
102
+ }
103
+ __name(escapeCSharpIdentifier, "escapeCSharpIdentifier");
104
+ function toCSharpPropertyName(name) {
105
+ const words = splitWords(name);
106
+ if (words.length === 0) return "_";
107
+ let result = words.map(capitalize).join("");
108
+ if (/^\d/.test(result)) result = `_${result}`;
109
+ return result;
110
+ }
111
+ __name(toCSharpPropertyName, "toCSharpPropertyName");
112
+ function toCSharpParameterName(name) {
113
+ const words = splitWords(name);
114
+ if (words.length === 0) return "_";
115
+ const head = words[0].toLowerCase();
116
+ const rest = words.slice(1).map(capitalize);
117
+ let result = head + rest.join("");
118
+ if (/^\d/.test(result)) result = `_${result}`;
119
+ return escapeCSharpIdentifier(result);
120
+ }
121
+ __name(toCSharpParameterName, "toCSharpParameterName");
122
+ function safeMemberName(propertyName, ownerTypeName) {
123
+ if (propertyName === ownerTypeName || RESERVED_MEMBER_NAMES.has(propertyName)) return `${propertyName}Value`;
124
+ return propertyName;
125
+ }
126
+ __name(safeMemberName, "safeMemberName");
127
+ function toCSharpTypeName(name) {
128
+ const words = splitWords(name);
129
+ if (words.length === 0) return "_";
130
+ let result = words.map(capitalize).join("");
131
+ if (/^\d/.test(result)) result = `_${result}`;
132
+ return result;
133
+ }
134
+ __name(toCSharpTypeName, "toCSharpTypeName");
135
+ function sanitizeCSharpTypeName(name) {
136
+ let result = name.replace(/[^a-zA-Z0-9]/g, "");
137
+ if (result.length === 0) return "_";
138
+ result = result.charAt(0).toUpperCase() + result.slice(1);
139
+ if (/^\d/.test(result)) result = `_${result}`;
140
+ return result;
141
+ }
142
+ __name(sanitizeCSharpTypeName, "sanitizeCSharpTypeName");
143
+ function toCSharpEnumMemberName(value) {
144
+ const words = splitWords(value);
145
+ if (words.length === 0) return "_";
146
+ let result = words.map(capitalize).join("");
147
+ if (/^\d/.test(result)) result = `_${result}`;
148
+ return result;
149
+ }
150
+ __name(toCSharpEnumMemberName, "toCSharpEnumMemberName");
151
+ function deriveCSharpFileBase(file) {
152
+ const base = file.split("/").pop()?.replace(/\.(op\.)?ck$/, "") ?? "models";
153
+ return toCSharpTypeName(base);
154
+ }
155
+ __name(deriveCSharpFileBase, "deriveCSharpFileBase");
156
+ function xmlDocLines(text, indent, tag = "summary") {
157
+ if (text.length === 0) return [];
158
+ const safe = escapeXml(text);
159
+ const sourceLines = safe.split("\n");
160
+ if (sourceLines.length === 1) return [
161
+ `${indent}/// <${tag}>${sourceLines[0]}</${tag}>`
162
+ ];
163
+ return [
164
+ `${indent}/// <${tag}>`,
165
+ ...sourceLines.map((line) => `${indent}/// ${line}`.trimEnd()),
166
+ `${indent}/// </${tag}>`
167
+ ];
168
+ }
169
+ __name(xmlDocLines, "xmlDocLines");
170
+ function escapeXml(text) {
171
+ return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
172
+ }
173
+ __name(escapeXml, "escapeXml");
174
+ function quoteCSharpString(value) {
175
+ const escaped = value.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n").replace(/\r/g, "\\r").replace(/\t/g, "\\t").replace(/\0/g, "\\0");
176
+ return `"${escaped}"`;
177
+ }
178
+ __name(quoteCSharpString, "quoteCSharpString");
179
+ function splitWords(name) {
180
+ return name.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2").split(/[^a-zA-Z0-9]+/).filter(Boolean);
181
+ }
182
+ __name(splitWords, "splitWords");
183
+ function capitalize(word) {
184
+ return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();
185
+ }
186
+ __name(capitalize, "capitalize");
187
+
188
+ // src/codegen-models.ts
189
+ var MODEL_USINGS = [
190
+ "using System;",
191
+ "using System.Collections.Generic;",
192
+ "using System.Numerics;",
193
+ "using System.Text.Json;",
194
+ "using System.Text.Json.Serialization;"
195
+ ];
196
+ function generateCSharpModels(root, opts) {
197
+ const modelsWithInput = resolveModelsWithInput(root.models, opts.modelsWithInput);
198
+ const modelIndex = opts.modelIndex ?? buildModelIndex(root.models);
199
+ const ctx = {
200
+ namespace: opts.namespace,
201
+ modelsWithInput,
202
+ modelIndex,
203
+ hoisted: opts.hoisted,
204
+ globalAliases: [],
205
+ warn: opts.warn
206
+ };
207
+ const bodies = [];
208
+ const append = /* @__PURE__ */ __name((lines) => {
209
+ if (lines.length === 0) return;
210
+ bodies.push("", ...lines);
211
+ }, "append");
212
+ for (const model of topoSortModels(root.models)) append(generateModel(model, ctx));
213
+ for (const decl of opts.hoisted?.byFile.get(root.file) ?? []) append(generateHoisted(decl, ctx));
214
+ return renderFile(`${opts.namespace}.Models`, ctx.globalAliases, [
215
+ ...MODEL_USINGS
216
+ ], bodies);
217
+ }
218
+ __name(generateCSharpModels, "generateCSharpModels");
219
+ function resolveModelsWithInput(models, external = /* @__PURE__ */ new Set()) {
220
+ const seed = new Set(external);
221
+ return /* @__PURE__ */ new Set([
222
+ ...seed,
223
+ ...computeModelsWithInput([
224
+ ...models
225
+ ], seed)
226
+ ]);
227
+ }
228
+ __name(resolveModelsWithInput, "resolveModelsWithInput");
229
+ function createRenderContext(opts) {
230
+ return {
231
+ namespace: opts.namespace,
232
+ modelsWithInput: opts.modelsWithInput,
233
+ modelIndex: opts.modelIndex ?? /* @__PURE__ */ new Map(),
234
+ hoisted: opts.hoisted,
235
+ globalAliases: [],
236
+ warn: opts.warn
237
+ };
238
+ }
239
+ __name(createRenderContext, "createRenderContext");
240
+ function renderFile(namespaceName, globalAliases, usings, bodies) {
241
+ const lines = [
242
+ "// <auto-generated/>",
243
+ "// Generated by @contractkit/plugin-csharp. Do not edit manually.",
244
+ "#nullable enable",
245
+ ""
246
+ ];
247
+ if (globalAliases.length > 0) {
248
+ lines.push(...[
249
+ ...globalAliases
250
+ ].sort());
251
+ lines.push("");
252
+ }
253
+ lines.push(...usings);
254
+ lines.push("");
255
+ lines.push(`namespace ${namespaceName};`);
256
+ lines.push(...bodies);
257
+ lines.push("");
258
+ return lines.join("\n");
259
+ }
260
+ __name(renderFile, "renderFile");
261
+ function renderCSharpType(type, ctx, forInput = false) {
262
+ const decl = ctx.hoisted?.byNode.get(type);
263
+ if (decl) return hoistedTypeName(decl, ctx, forInput);
264
+ switch (type.kind) {
265
+ case "scalar":
266
+ return renderScalar(type.name, ctx);
267
+ case "literal":
268
+ return literalCSharpType(type.value, ctx);
269
+ case "array":
270
+ return `${qualify("List", "System.Collections.Generic.List", ctx)}<${renderCSharpType(type.item, ctx, forInput)}>`;
271
+ case "record": {
272
+ const key = renderCSharpType(type.key, ctx, forInput);
273
+ const value = renderCSharpType(type.value, ctx, forInput);
274
+ const stringType = qualify("string", "System.String", ctx);
275
+ if (key !== stringType) {
276
+ ctx.warn?.(`A record key of type '${key}' is not representable as a JSON object key; emitting Dictionary<string, ${value}>. Parse the key yourself, or declare the key as a string.`);
277
+ }
278
+ return `${qualify("Dictionary", "System.Collections.Generic.Dictionary", ctx)}<${stringType}, ${value}>`;
279
+ }
280
+ case "tuple":
281
+ return jsonElement(ctx);
282
+ case "ref": {
283
+ const name = forInput && ctx.modelsWithInput.has(type.name) ? `${type.name}Input` : type.name;
284
+ return ctx.qualify ? `${ctx.namespace}.Models.${name}` : name;
285
+ }
286
+ case "lazy":
287
+ return renderCSharpType(type.inner, ctx, forInput);
288
+ case "union": {
289
+ const nonNull = type.members.filter((m) => !isNullScalar(m));
290
+ const nullable = nonNull.length !== type.members.length;
291
+ if (nonNull.length === 0) return `${qualify("object", "System.Object", ctx)}?`;
292
+ if (nonNull.length === 1) {
293
+ const inner = renderCSharpType(nonNull[0], ctx, forInput);
294
+ return nullable && !inner.endsWith("?") ? `${inner}?` : inner;
295
+ }
296
+ return jsonElement(ctx);
297
+ }
298
+ case "enum":
299
+ case "inlineObject":
300
+ case "intersection":
301
+ case "discriminatedUnion":
302
+ return jsonElement(ctx);
303
+ }
304
+ }
305
+ __name(renderCSharpType, "renderCSharpType");
306
+ function hoistedTypeName(decl, ctx, forInput) {
307
+ const bare = forInput && decl.needsInput ? `${decl.name}Input` : decl.name;
308
+ const name = ctx.qualify ? `${ctx.namespace}.Models.${bare}` : bare;
309
+ return decl.nullable ? `${name}?` : name;
310
+ }
311
+ __name(hoistedTypeName, "hoistedTypeName");
312
+ function qualify(short, full, ctx) {
313
+ return ctx.qualify ? full : short;
314
+ }
315
+ __name(qualify, "qualify");
316
+ function jsonElement(ctx) {
317
+ return qualify("JsonElement", "System.Text.Json.JsonElement", ctx);
318
+ }
319
+ __name(jsonElement, "jsonElement");
320
+ function isNullScalar(type) {
321
+ return type.kind === "scalar" && type.name === "null";
322
+ }
323
+ __name(isNullScalar, "isNullScalar");
324
+ function renderScalar(name, ctx) {
325
+ switch (name) {
326
+ case "string":
327
+ case "email":
328
+ case "url":
329
+ case "interval":
330
+ return qualify("string", "System.String", ctx);
331
+ case "number":
332
+ return qualify("double", "System.Double", ctx);
333
+ // `int` is a JS safe integer in the source language, which overflows C#'s 32-bit int.
334
+ case "int":
335
+ return qualify("long", "System.Int64", ctx);
336
+ case "bigint":
337
+ return qualify("BigInteger", "System.Numerics.BigInteger", ctx);
338
+ // Carried as a quoted string by DecimalStringConverter, never as a JSON number.
339
+ case "decimal":
340
+ return qualify("decimal", "System.Decimal", ctx);
341
+ case "boolean":
342
+ return qualify("bool", "System.Boolean", ctx);
343
+ case "date":
344
+ return qualify("DateOnly", "System.DateOnly", ctx);
345
+ case "time":
346
+ return qualify("TimeOnly", "System.TimeOnly", ctx);
347
+ case "datetime":
348
+ return qualify("DateTimeOffset", "System.DateTimeOffset", ctx);
349
+ // Carried as ISO 8601 by IsoTimeSpanConverter, not the BCL's own `d.hh:mm:ss`.
350
+ case "duration":
351
+ return qualify("TimeSpan", "System.TimeSpan", ctx);
352
+ case "uuid":
353
+ return qualify("Guid", "System.Guid", ctx);
354
+ case "binary":
355
+ return qualify("byte[]", "System.Byte[]", ctx);
356
+ case "null":
357
+ return `${qualify("object", "System.Object", ctx)}?`;
358
+ case "unknown":
359
+ case "json":
360
+ case "object":
361
+ return jsonElement(ctx);
362
+ default: {
363
+ const _exhaustive = name;
364
+ throw new Error(`plugin-csharp: unmapped scalar '${String(_exhaustive)}' \u2014 add a case`);
365
+ }
366
+ }
367
+ }
368
+ __name(renderScalar, "renderScalar");
369
+ function literalCSharpType(value, ctx) {
370
+ if (typeof value === "string") return qualify("string", "System.String", ctx);
371
+ if (typeof value === "boolean") return qualify("bool", "System.Boolean", ctx);
372
+ return Number.isInteger(value) ? qualify("long", "System.Int64", ctx) : qualify("double", "System.Double", ctx);
373
+ }
374
+ __name(literalCSharpType, "literalCSharpType");
375
+ function renderDefault(value, type, ctx) {
376
+ const inner = type.kind === "lazy" ? type.inner : type;
377
+ if (typeof value === "boolean") return String(value);
378
+ if (typeof value === "number") {
379
+ if (inner.kind === "scalar") {
380
+ switch (inner.name) {
381
+ case "int":
382
+ return `${value}L`;
383
+ case "number":
384
+ return `${value}d`;
385
+ case "decimal":
386
+ return `${value}m`;
387
+ case "bigint":
388
+ return Number.isSafeInteger(value) ? `new BigInteger(${value})` : `BigInteger.Parse("${value}")`;
389
+ }
390
+ }
391
+ return Number.isInteger(value) ? `${value}L` : `${value}d`;
392
+ }
393
+ if (inner.kind === "enum") {
394
+ const decl = ctx.hoisted?.byNode.get(inner);
395
+ if (!decl || !inner.values.includes(value)) return void 0;
396
+ return `${decl.name}.${enumMemberNames(inner.values).get(value)}`;
397
+ }
398
+ if (inner.kind === "ref") {
399
+ const target = ctx.modelIndex.get(inner.name);
400
+ const targetType = target?.type?.kind === "lazy" ? target.type.inner : target?.type;
401
+ if (targetType?.kind !== "enum" || !targetType.values.includes(value)) return void 0;
402
+ return `${inner.name}.${enumMemberNames(targetType.values).get(value)}`;
403
+ }
404
+ if (inner.kind === "scalar") {
405
+ switch (inner.name) {
406
+ case "decimal":
407
+ return /^-?\d+(\.\d+)?$/.test(value) ? `${value}m` : void 0;
408
+ case "bigint":
409
+ return /^-?\d+$/.test(value) ? `BigInteger.Parse(${quoteCSharpString(value)})` : void 0;
410
+ case "string":
411
+ case "email":
412
+ case "url":
413
+ case "interval":
414
+ return quoteCSharpString(value);
415
+ default:
416
+ return void 0;
417
+ }
418
+ }
419
+ return quoteCSharpString(value);
420
+ }
421
+ __name(renderDefault, "renderDefault");
422
+ function applyWireCase(name, wireCase) {
423
+ if (!wireCase || wireCase === "camel") return name;
424
+ if (wireCase === "snake") return name.replace(/[A-Z]/g, (c) => `_${c.toLowerCase()}`);
425
+ return name.charAt(0).toUpperCase() + name.slice(1);
426
+ }
427
+ __name(applyWireCase, "applyWireCase");
428
+ function renamingCase(wireCase) {
429
+ return wireCase && wireCase !== "camel" ? wireCase : void 0;
430
+ }
431
+ __name(renamingCase, "renamingCase");
432
+ function wireCaseFor(model, forInput, split, ctx) {
433
+ const input = renamingCase(model.inputCase);
434
+ const output = renamingCase(model.outputCase);
435
+ if (split) return forInput ? input : output;
436
+ if (input && output && input !== output) {
437
+ ctx.warn?.(`Contract '${model.name}' sets format(input=${input}) and format(output=${output}), but nothing about it splits into an Input variant, so one C# record carries both directions and can only spell one set of keys. The generated keys follow the output casing; a request built from this record will send the wrong ones.`);
438
+ return output;
439
+ }
440
+ return output ?? input;
441
+ }
442
+ __name(wireCaseFor, "wireCaseFor");
443
+ function containsInlineObject(type) {
444
+ if (!type) return false;
445
+ switch (type.kind) {
446
+ case "inlineObject":
447
+ return true;
448
+ case "lazy":
449
+ return containsInlineObject(type.inner);
450
+ case "array":
451
+ return containsInlineObject(type.item);
452
+ case "record":
453
+ return containsInlineObject(type.value);
454
+ case "tuple":
455
+ return type.items.some(containsInlineObject);
456
+ case "union":
457
+ case "discriminatedUnion":
458
+ case "intersection":
459
+ return (type.members ?? []).some(containsInlineObject);
460
+ default:
461
+ return false;
462
+ }
463
+ }
464
+ __name(containsInlineObject, "containsInlineObject");
465
+ function warnUncasedNesting(model, fields, wireCase, ctx) {
466
+ if (!wireCase) return;
467
+ if (!fields.some((f) => containsInlineObject(f.type)) && !containsInlineObject(model.type)) return;
468
+ ctx.warn?.(`Contract '${model.name}' is declared format(${model.outputCase ? "output" : "input"}=${wireCase}) and holds an anonymous object. The record hoisted out of that object keeps its declared key names, so its keys will not be ${wireCase}-cased. Name the shape as its own contract to fix it.`);
469
+ }
470
+ __name(warnUncasedNesting, "warnUncasedNesting");
471
+ function generateModel(model, ctx) {
472
+ if (model.type) return generateAliasModel(model, ctx);
473
+ const effective = effectiveFieldsFor(model, ctx);
474
+ const needsSplit = ctx.modelsWithInput.has(model.name) || effective.some((f) => f.visibility !== "normal");
475
+ if (!needsSplit) return generateRecordForModel(model.name, effective, ctx, false, model, false);
476
+ const readFields = effective.filter((f) => f.visibility !== "writeonly");
477
+ const inputFields = effective.filter((f) => f.visibility !== "readonly");
478
+ return [
479
+ ...generateRecordForModel(model.name, readFields, ctx, false, model, true),
480
+ "",
481
+ ...generateRecordForModel(`${model.name}Input`, inputFields, ctx, true, model, true)
482
+ ];
483
+ }
484
+ __name(generateModel, "generateModel");
485
+ function effectiveFieldsFor(model, ctx) {
486
+ if (!model.bases || model.bases.length === 0) return model.fields;
487
+ const { fields, unresolved } = resolveEffectiveFields(model.name, ctx.modelIndex);
488
+ for (const name of unresolved) {
489
+ ctx.warn?.(`Contract '${model.name}' extends '${name}', which is not defined; its fields are missing from the generated record.`);
490
+ }
491
+ return fields;
492
+ }
493
+ __name(effectiveFieldsFor, "effectiveFieldsFor");
494
+ function generateAliasModel(model, ctx) {
495
+ const type = model.type;
496
+ const inner = type.kind === "lazy" ? type.inner : type;
497
+ if (ctx.hoisted?.byNode.has(inner)) return [];
498
+ if (inner.kind === "enum") return generateEnum(model.name, inner.values, ctx, model.description, model.deprecated);
499
+ if (inner.kind === "intersection" || inner.kind === "inlineObject") {
500
+ const { fields, unresolved } = resolveEffectiveFields(inner, ctx.modelIndex);
501
+ for (const name of unresolved) {
502
+ ctx.warn?.(`Contract '${model.name}' references '${name}', which is not defined; its fields are missing from the generated record.`);
503
+ }
504
+ const needsSplit = ctx.modelsWithInput.has(model.name) || fields.some((f) => f.visibility !== "normal");
505
+ if (!needsSplit) return generateRecordForModel(model.name, fields, ctx, false, model, false);
506
+ return [
507
+ ...generateRecordForModel(model.name, fields.filter((f) => f.visibility !== "writeonly"), ctx, false, model, true),
508
+ "",
509
+ ...generateRecordForModel(`${model.name}Input`, fields.filter((f) => f.visibility !== "readonly"), ctx, true, model, true)
510
+ ];
511
+ }
512
+ addAlias(model.name, type, ctx, false);
513
+ if (ctx.modelsWithInput.has(model.name)) addAlias(`${model.name}Input`, type, ctx, true);
514
+ return [];
515
+ }
516
+ __name(generateAliasModel, "generateAliasModel");
517
+ function addAlias(name, type, ctx, forInput) {
518
+ const target = renderCSharpType(type, {
519
+ ...ctx,
520
+ qualify: true
521
+ }, forInput);
522
+ let aliased = target;
523
+ if (aliased.endsWith("?") && !isNullableValueType(type, ctx)) {
524
+ aliased = aliased.slice(0, -1);
525
+ ctx.warn?.(`Contract '${name}' aliases a nullable type, which C# cannot express as a using alias; '${name}' is generated as '${aliased}'. Declare the nullability at each use site instead.`);
526
+ }
527
+ ctx.globalAliases.push(`global using ${name} = ${aliased};`);
528
+ }
529
+ __name(addAlias, "addAlias");
530
+ function isNullableValueType(type, ctx) {
531
+ const inner = type.kind === "lazy" ? type.inner : type;
532
+ if (inner.kind !== "union") return false;
533
+ const nonNull = inner.members.filter((m) => !isNullScalar(m));
534
+ if (nonNull.length !== 1) return false;
535
+ return VALUE_TYPES.has(renderCSharpType(nonNull[0], {
536
+ ...ctx,
537
+ qualify: false
538
+ }, false));
539
+ }
540
+ __name(isNullableValueType, "isNullableValueType");
541
+ var VALUE_TYPES = /* @__PURE__ */ new Set([
542
+ "bool",
543
+ "byte",
544
+ "decimal",
545
+ "double",
546
+ "long",
547
+ "BigInteger",
548
+ "DateOnly",
549
+ "TimeOnly",
550
+ "DateTimeOffset",
551
+ "TimeSpan",
552
+ "Guid",
553
+ "JsonElement"
554
+ ]);
555
+ function enumMemberNames(values) {
556
+ const out = /* @__PURE__ */ new Map();
557
+ const used = /* @__PURE__ */ new Set();
558
+ for (const value of values) out.set(value, uniqueName(toCSharpEnumMemberName(value), used));
559
+ return out;
560
+ }
561
+ __name(enumMemberNames, "enumMemberNames");
562
+ function generateEnum(name, values, ctx, description, deprecated) {
563
+ const entries = enumMemberNames(values);
564
+ const lines = [];
565
+ lines.push(...docLines(description, deprecated, ""));
566
+ lines.push(`[JsonConverter(typeof(JsonStringEnumConverter<${name}>))]`);
567
+ lines.push(`public enum ${name}`);
568
+ lines.push("{");
569
+ values.forEach((value, index) => {
570
+ if (index > 0) lines.push("");
571
+ lines.push(` [JsonStringEnumMemberName(${quoteCSharpString(value)})]`);
572
+ lines.push(` ${entries.get(value)},`);
573
+ });
574
+ lines.push("}");
575
+ if (ctx.modelsWithInput.has(name)) ctx.globalAliases.push(`global using ${name}Input = ${ctx.namespace}.Models.${name};`);
576
+ return lines;
577
+ }
578
+ __name(generateEnum, "generateEnum");
579
+ function supertypesFor(readName, ctx, forInput) {
580
+ const unions = ctx.hoisted?.memberships.get(readName) ?? [];
581
+ return unions.map((union) => {
582
+ const decl = ctx.hoisted?.byName.get(union);
583
+ return forInput && decl?.needsInput ? `${union}Input` : union;
584
+ });
585
+ }
586
+ __name(supertypesFor, "supertypesFor");
587
+ function generateRecordForModel(name, fields, ctx, forInput, model, split) {
588
+ const readName = forInput && name.endsWith("Input") ? name.slice(0, -"Input".length) : name;
589
+ const wireCase = wireCaseFor(model, forInput, split, ctx);
590
+ if (!forInput) warnUncasedNesting(model, fields, wireCase, ctx);
591
+ return renderRecord(name, fields, ctx, forInput, supertypesFor(readName, ctx, forInput), model.description, model.deprecated, wireCase);
592
+ }
593
+ __name(generateRecordForModel, "generateRecordForModel");
594
+ function renderRecord(name, fields, ctx, forInput, supertypes, description, deprecated, wireCase) {
595
+ const lines = [];
596
+ lines.push(...docLines(description, deprecated, ""));
597
+ const implementsClause = supertypes.length > 0 ? ` : ${supertypes.join(", ")}` : "";
598
+ if (fields.length === 0) {
599
+ lines.push(`public sealed record ${name}${implementsClause};`);
600
+ return lines;
601
+ }
602
+ lines.push(`public sealed record ${name}${implementsClause}`);
603
+ lines.push("{");
604
+ fields.forEach((field, index) => {
605
+ if (index > 0) lines.push("");
606
+ lines.push(...renderField(field, ctx, forInput, name, wireCase));
607
+ });
608
+ lines.push("}");
609
+ return lines;
610
+ }
611
+ __name(renderRecord, "renderRecord");
612
+ function renderField(field, ctx, forInput, ownerTypeName, wireCase) {
613
+ const propName = safeMemberName(toCSharpPropertyName(field.name), ownerTypeName);
614
+ const wireName = applyWireCase(field.name, wireCase);
615
+ let typeStr = renderCSharpType(field.type, ctx, forInput);
616
+ if ((field.optional || field.nullable) && !typeStr.endsWith("?")) typeStr += "?";
617
+ let initializer = field.default !== void 0 ? renderDefault(field.default, field.type, ctx) : void 0;
618
+ if (initializer === void 0 && !field.optional && !field.nullable) {
619
+ const inner = field.type.kind === "lazy" ? field.type.inner : field.type;
620
+ if (inner.kind === "literal") initializer = renderDefault(inner.value, inner, ctx);
621
+ }
622
+ const isRequired = !field.optional && initializer === void 0;
623
+ const lines = [];
624
+ lines.push(...docLines(field.description, field.deprecated, " "));
625
+ lines.push(` [JsonPropertyName(${quoteCSharpString(wireName)})]`);
626
+ if (field.optional) lines.push(" [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]");
627
+ const suffix = initializer !== void 0 ? ` = ${initializer};` : "";
628
+ lines.push(` public ${isRequired ? "required " : ""}${typeStr} ${propName} { get; init; }${suffix}`);
629
+ return lines;
630
+ }
631
+ __name(renderField, "renderField");
632
+ function generateHoisted(decl, ctx) {
633
+ const read = generateHoistedVariant(decl, ctx, false);
634
+ if (!decl.needsInput) return read;
635
+ return [
636
+ ...read,
637
+ "",
638
+ ...generateHoistedVariant(decl, ctx, true)
639
+ ];
640
+ }
641
+ __name(generateHoisted, "generateHoisted");
642
+ function generateHoistedVariant(decl, ctx, forInput) {
643
+ const name = forInput ? `${decl.name}Input` : decl.name;
644
+ switch (decl.kind) {
645
+ case "enum":
646
+ return generateEnum(name, decl.values ?? [], ctx, decl.description);
647
+ case "record":
648
+ return renderRecord(name, (decl.fields ?? []).filter((f) => forInput ? f.visibility !== "readonly" : f.visibility !== "writeonly"), ctx, forInput, supertypesFor(decl.name, ctx, forInput), decl.description);
649
+ case "tuple":
650
+ return generateTupleRecord(decl, name, ctx, forInput);
651
+ case "plainUnion":
652
+ return generatePlainUnion(decl, name, ctx, forInput);
653
+ case "discriminatedUnion":
654
+ return generateDiscriminatedUnion(decl, name, ctx, forInput);
655
+ }
656
+ }
657
+ __name(generateHoistedVariant, "generateHoistedVariant");
658
+ function deserializeExpr(type, ctx, forInput) {
659
+ return `element.Deserialize<${renderCSharpType(type, ctx, forInput)}>(options)!`;
660
+ }
661
+ __name(deserializeExpr, "deserializeExpr");
662
+ function generateTupleRecord(decl, name, ctx, forInput) {
663
+ const items = decl.items ?? [];
664
+ const converterName = `${name}Converter`;
665
+ const parameters = items.map((item, index) => `${renderCSharpType(item, ctx, forInput)} Item${index}`).join(", ");
666
+ const lines = [];
667
+ lines.push(...docLines(decl.description, void 0, ""));
668
+ lines.push(`[JsonConverter(typeof(${converterName}))]`);
669
+ lines.push(`public sealed record ${name}(${parameters});`);
670
+ lines.push("");
671
+ lines.push(`/// <summary>Reads and writes <see cref="${name}"/> as a JSON array.</summary>`);
672
+ lines.push(`public sealed class ${converterName} : JsonConverter<${name}>`);
673
+ lines.push("{");
674
+ lines.push(` public override ${name} Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)`);
675
+ lines.push(" {");
676
+ lines.push(" using var document = JsonDocument.ParseValue(ref reader);");
677
+ lines.push(" var array = document.RootElement;");
678
+ lines.push(` if (array.ValueKind != JsonValueKind.Array || array.GetArrayLength() != ${items.length})`);
679
+ lines.push(" {");
680
+ lines.push(` throw new JsonException("Expected a JSON array of ${items.length} elements for ${name}.");`);
681
+ lines.push(" }");
682
+ lines.push("");
683
+ lines.push(` return new ${name}(`);
684
+ items.forEach((item, index) => {
685
+ const expr = `array[${index}].Deserialize<${renderCSharpType(item, ctx, forInput)}>(options)!`;
686
+ lines.push(` ${expr}${index === items.length - 1 ? "" : ","}`);
687
+ });
688
+ lines.push(" );");
689
+ lines.push(" }");
690
+ lines.push("");
691
+ lines.push(` public override void Write(Utf8JsonWriter writer, ${name} value, JsonSerializerOptions options)`);
692
+ lines.push(" {");
693
+ lines.push(" writer.WriteStartArray();");
694
+ items.forEach((_, index) => lines.push(` JsonSerializer.Serialize(writer, value.Item${index}, options);`));
695
+ lines.push(" writer.WriteEndArray();");
696
+ lines.push(" }");
697
+ lines.push("}");
698
+ return lines;
699
+ }
700
+ __name(generateTupleRecord, "generateTupleRecord");
701
+ function generatePlainUnion(decl, name, ctx, forInput) {
702
+ const converterName = `${name}Converter`;
703
+ const members = decl.members ?? [];
704
+ const lines = [];
705
+ lines.push(...docLines(decl.description, void 0, ""));
706
+ lines.push(`[JsonConverter(typeof(${converterName}))]`);
707
+ lines.push(`public abstract record ${name}`);
708
+ lines.push("{");
709
+ lines.push(` private ${name}() { }`);
710
+ for (const member of members) {
711
+ lines.push("");
712
+ lines.push(` public sealed record ${member.wrapperName}(${renderCSharpType(member.type, ctx, forInput)} Value) : ${name};`);
713
+ }
714
+ lines.push("}");
715
+ lines.push("");
716
+ lines.push(`/// <summary>Reads <see cref="${name}"/> by trying each member in declaration order.</summary>`);
717
+ lines.push(`public sealed class ${converterName} : JsonConverter<${name}>`);
718
+ lines.push("{");
719
+ lines.push(` public override ${name} Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)`);
720
+ lines.push(" {");
721
+ lines.push(" using var document = JsonDocument.ParseValue(ref reader);");
722
+ lines.push(" var element = document.RootElement;");
723
+ lines.push("");
724
+ for (const member of members) {
725
+ lines.push(" try");
726
+ lines.push(" {");
727
+ lines.push(` return new ${name}.${member.wrapperName}(${deserializeExpr(member.type, ctx, forInput)});`);
728
+ lines.push(" }");
729
+ lines.push(" catch (JsonException)");
730
+ lines.push(" {");
731
+ lines.push(" // Not this member; fall through to the next.");
732
+ lines.push(" }");
733
+ lines.push("");
734
+ }
735
+ lines.push(` throw new JsonException("No ${name} member matched the payload.");`);
736
+ lines.push(" }");
737
+ lines.push("");
738
+ lines.push(` public override void Write(Utf8JsonWriter writer, ${name} value, JsonSerializerOptions options)`);
739
+ lines.push(" {");
740
+ lines.push(" switch (value)");
741
+ lines.push(" {");
742
+ for (const member of members) {
743
+ lines.push(` case ${name}.${member.wrapperName} member:`);
744
+ lines.push(" JsonSerializer.Serialize(writer, member.Value, options);");
745
+ lines.push(" break;");
746
+ }
747
+ lines.push(" default:");
748
+ lines.push(` throw new JsonException($"Unknown ${name} member {value.GetType().Name}.");`);
749
+ lines.push(" }");
750
+ lines.push(" }");
751
+ lines.push("}");
752
+ return lines;
753
+ }
754
+ __name(generatePlainUnion, "generatePlainUnion");
755
+ function generateDiscriminatedUnion(decl, name, ctx, forInput) {
756
+ const converterName = `${name}Converter`;
757
+ const members = (decl.members ?? []).map((member) => ({
758
+ ...member,
759
+ recordName: memberRecordName(member.typeName, ctx, forInput)
760
+ }));
761
+ const discriminator = decl.discriminator ?? "";
762
+ const lines = [];
763
+ lines.push(...docLines(decl.description, void 0, ""));
764
+ lines.push(`[JsonConverter(typeof(${converterName}))]`);
765
+ lines.push(`public interface ${name}`);
766
+ lines.push("{");
767
+ lines.push("}");
768
+ lines.push("");
769
+ lines.push(`/// <summary>Reads <see cref="${name}"/> by dispatching on its '${discriminator}' tag.</summary>`);
770
+ lines.push(`public sealed class ${converterName} : JsonConverter<${name}>`);
771
+ lines.push("{");
772
+ lines.push(` public override ${name} Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)`);
773
+ lines.push(" {");
774
+ lines.push(" using var document = JsonDocument.ParseValue(ref reader);");
775
+ lines.push(" var element = document.RootElement;");
776
+ lines.push(` var tag = element.TryGetProperty(${quoteCSharpString(discriminator)}, out var tagElement) && tagElement.ValueKind == JsonValueKind.String`);
777
+ lines.push(" ? tagElement.GetString()");
778
+ lines.push(" : null;");
779
+ lines.push("");
780
+ lines.push(" return tag switch");
781
+ lines.push(" {");
782
+ for (const member of members) {
783
+ lines.push(` ${quoteCSharpString(member.tag ?? "")} => element.Deserialize<${member.recordName}>(options)!,`);
784
+ }
785
+ lines.push(` _ => throw new JsonException($"Unknown ${name} ${discriminator}: {tag}"),`);
786
+ lines.push(" };");
787
+ lines.push(" }");
788
+ lines.push("");
789
+ lines.push(` public override void Write(Utf8JsonWriter writer, ${name} value, JsonSerializerOptions options)`);
790
+ lines.push(" {");
791
+ lines.push(" switch (value)");
792
+ lines.push(" {");
793
+ for (const member of members) {
794
+ lines.push(` case ${member.recordName} member:`);
795
+ lines.push(" JsonSerializer.Serialize(writer, member, options);");
796
+ lines.push(" break;");
797
+ }
798
+ lines.push(" default:");
799
+ lines.push(` throw new JsonException($"Unknown ${name} member {value.GetType().Name}.");`);
800
+ lines.push(" }");
801
+ lines.push(" }");
802
+ lines.push("}");
803
+ return lines;
804
+ }
805
+ __name(generateDiscriminatedUnion, "generateDiscriminatedUnion");
806
+ function memberRecordName(typeName, ctx, forInput) {
807
+ if (!forInput) return typeName;
808
+ const decl = ctx.hoisted?.byName.get(typeName);
809
+ if (decl) return decl.needsInput ? `${typeName}Input` : typeName;
810
+ return ctx.modelsWithInput.has(typeName) ? `${typeName}Input` : typeName;
811
+ }
812
+ __name(memberRecordName, "memberRecordName");
813
+ function docLines(description, deprecated, indent) {
814
+ const lines = [];
815
+ if (description) lines.push(...xmlDocLines(description, indent));
816
+ if (deprecated) lines.push(...xmlDocLines("Deprecated in the contract.", indent, "remarks"));
817
+ return lines;
818
+ }
819
+ __name(docLines, "docLines");
820
+ function uniqueName(name, used) {
821
+ if (!used.has(name)) {
822
+ used.add(name);
823
+ return name;
824
+ }
825
+ let n = 2;
826
+ while (used.has(`${name}${n}`)) n++;
827
+ used.add(`${name}${n}`);
828
+ return `${name}${n}`;
829
+ }
830
+ __name(uniqueName, "uniqueName");
831
+
832
+ // src/codegen-client.ts
833
+ import { classifyContentType, observableResponses, resolveModifiers } from "@contractkit/core";
834
+ function clientUsings(namespaceName) {
835
+ return [
836
+ "using System;",
837
+ "using System.Collections.Generic;",
838
+ "using System.Globalization;",
839
+ "using System.Net.Http;",
840
+ "using System.Numerics;",
841
+ "using System.Text.Json;",
842
+ "using System.Text.Json.Serialization;",
843
+ "using System.Threading;",
844
+ "using System.Threading.Tasks;",
845
+ "using System.Xml;",
846
+ `using ${namespaceName}.Models;`,
847
+ `using ${namespaceName}.Runtime;`
848
+ ];
849
+ }
850
+ __name(clientUsings, "clientUsings");
851
+ function hasPublicOperations(root, includeInternal = false) {
852
+ for (const route of root.routes) {
853
+ for (const op of route.operations) {
854
+ if (includeInternal || !resolveModifiers(route, op).includes("internal")) return true;
855
+ }
856
+ }
857
+ return false;
858
+ }
859
+ __name(hasPublicOperations, "hasPublicOperations");
860
+ function deriveClientClassName(file) {
861
+ return `${deriveCSharpFileBase(file)}Client`;
862
+ }
863
+ __name(deriveClientClassName, "deriveClientClassName");
864
+ function deriveClientPropertyName(file) {
865
+ return deriveCSharpFileBase(file);
866
+ }
867
+ __name(deriveClientPropertyName, "deriveClientPropertyName");
868
+ function generateCSharpClient(root, opts) {
869
+ const className = deriveClientClassName(root.file);
870
+ const includeInternal = opts.includeInternal ?? false;
871
+ const ctx = createRenderContext(opts);
872
+ const publicOps = [];
873
+ for (const route of root.routes) {
874
+ for (const op of route.operations) {
875
+ if (!includeInternal && resolveModifiers(route, op).includes("internal")) continue;
876
+ publicOps.push({
877
+ route,
878
+ op
879
+ });
880
+ }
881
+ }
882
+ const shapeLines = [];
883
+ for (const { route, op } of publicOps) {
884
+ const base = methodBase(deriveMethodName(op, route));
885
+ for (const { source, suffix } of [
886
+ {
887
+ source: op.query,
888
+ suffix: "Query"
889
+ },
890
+ {
891
+ source: op.headers,
892
+ suffix: "Headers"
893
+ }
894
+ ]) {
895
+ if (source?.kind !== "params" || source.nodes.length === 0) continue;
896
+ const shapeName = `${base}${suffix}`;
897
+ shapeLines.push("");
898
+ shapeLines.push(...xmlDocLines(`The ${suffix === "Query" ? "query parameters" : "request headers"} declared on ${where(route, op)}.`, ""));
899
+ shapeLines.push(`public sealed record ${shapeName}`);
900
+ shapeLines.push("{");
901
+ source.nodes.forEach((node, index) => {
902
+ if (index > 0) shapeLines.push("");
903
+ const propName = safeMemberName(toCSharpPropertyName(node.name), shapeName);
904
+ let type = renderCSharpType(node.type, ctx, true);
905
+ const optional = Boolean(node.optional) || node.default !== void 0;
906
+ if (optional && !type.endsWith("?")) type += "?";
907
+ shapeLines.push(` [JsonPropertyName(${quoteCSharpString(node.name)})]`);
908
+ if (optional) shapeLines.push(" [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]");
909
+ shapeLines.push(` public ${optional ? "" : "required "}${type} ${propName} { get; init; }`);
910
+ });
911
+ shapeLines.push("}");
912
+ }
913
+ shapeLines.push(...responseDeclarations(route, op, ctx));
914
+ }
915
+ const methodLines = [];
916
+ const seen = /* @__PURE__ */ new Map();
917
+ for (const { route, op } of publicOps) {
918
+ const methodName = deriveMethodName(op, route);
919
+ const clash = seen.get(methodName);
920
+ if (clash) {
921
+ throw new Error(`plugin-csharp: ${where(route, op)} and ${clash} both generate the client method '${methodName}' on ${className}. Give one of them a distinct 'sdk:' name.`);
922
+ }
923
+ seen.set(methodName, where(route, op));
924
+ methodLines.push("");
925
+ methodLines.push(...generateMethod(route, op, ctx, methodName));
926
+ }
927
+ const body = [];
928
+ body.push("");
929
+ body.push(...xmlDocLines(`Operations declared in <c>${root.file.split("/").pop()}</c>.`, ""));
930
+ body.push(`public sealed class ${className}(SdkHttp http)`);
931
+ body.push("{");
932
+ body.push(...methodLines.slice(1).map((l) => l === "" ? "" : ` ${l}`));
933
+ body.push("}");
934
+ body.push(...shapeLines);
935
+ return renderFile(`${opts.namespace}.Clients`, ctx.globalAliases, clientUsings(opts.namespace), body);
936
+ }
937
+ __name(generateCSharpClient, "generateCSharpClient");
938
+ function where(route, op) {
939
+ return `${op.method.toUpperCase()} ${route.path}`;
940
+ }
941
+ __name(where, "where");
942
+ function methodBase(methodName) {
943
+ return methodName.endsWith("Async") ? methodName.slice(0, -"Async".length) : methodName;
944
+ }
945
+ __name(methodBase, "methodBase");
946
+ function responseShape(op) {
947
+ const observable = observableResponses(op);
948
+ if (observable.length > 1) return {
949
+ kind: "multiStatus",
950
+ responses: observable
951
+ };
952
+ const response = observable[0];
953
+ if (response && response.bodies.length > 1) return {
954
+ kind: "multiMime",
955
+ response
956
+ };
957
+ return {
958
+ kind: "simple",
959
+ response
960
+ };
961
+ }
962
+ __name(responseShape, "responseShape");
963
+ function observableOf(shape) {
964
+ if (shape.kind === "multiStatus") return shape.responses;
965
+ return shape.response ? [
966
+ shape.response
967
+ ] : [];
968
+ }
969
+ __name(observableOf, "observableOf");
970
+ function generateMethod(route, op, ctx, methodName) {
971
+ const base = methodBase(methodName);
972
+ const shape = responseShape(op);
973
+ const returnType = returnTypeFor(shape, base, ctx);
974
+ const observable = observableOf(shape);
975
+ const expectStatuses = observable.filter((r) => r.statusCode < 200 || r.statusCode >= 300).map((r) => r.statusCode);
976
+ const params = buildMethodParams(route, op, ctx);
977
+ const signature = [
978
+ ...params.map((p) => `${p.type} ${p.name}${p.optional ? " = null" : ""}`),
979
+ "CancellationToken cancellationToken = default"
980
+ ].join(", ");
981
+ const lines = [];
982
+ lines.push(...methodDoc(route, op, observable));
983
+ if (resolveModifiers(route, op).includes("deprecated")) lines.push('[Obsolete("Deprecated in the contract")]');
984
+ lines.push(`public async ${returnType === "void" ? "Task" : `Task<${returnType}>`} ${methodName}(${signature})`);
985
+ lines.push("{");
986
+ const callArgs = [
987
+ `HttpMethod.${httpMethodConstant(op.method)}`,
988
+ buildPathExpression(route.path, route.params)
989
+ ];
990
+ if (op.query) callArgs.push("query: http.Params(query)");
991
+ if (op.headers) callArgs.push("headers: http.Params(customHeaders)");
992
+ const content = bodyArgument(op);
993
+ if (content) callArgs.push(content);
994
+ if (expectStatuses.length > 0) callArgs.push(`expectStatuses: new[] { ${expectStatuses.join(", ")} }`);
995
+ callArgs.push("cancellationToken: cancellationToken");
996
+ const assignment = returnType === "void" ? "await " : "var response = await ";
997
+ lines.push(` ${assignment}http.ExecuteAsync(`);
998
+ callArgs.forEach((arg, index) => {
999
+ lines.push(` ${arg}${index === callArgs.length - 1 ? ").ConfigureAwait(false);" : ","}`);
1000
+ });
1001
+ lines.push(...returnStatements(shape, base, ctx, where(route, op)));
1002
+ lines.push("}");
1003
+ return lines;
1004
+ }
1005
+ __name(generateMethod, "generateMethod");
1006
+ function returnTypeFor(shape, base, ctx) {
1007
+ if (shape.kind !== "simple") return `${base}Response`;
1008
+ const response = shape.response;
1009
+ const body = response?.bodies[0];
1010
+ const headers = response?.headers ?? [];
1011
+ if (!body) return headers.length > 0 ? `${base}Headers` : "void";
1012
+ const dataType = bodyCSharpType(body, ctx);
1013
+ return headers.length > 0 ? `${base}Result` : dataType;
1014
+ }
1015
+ __name(returnTypeFor, "returnTypeFor");
1016
+ function bodyCSharpType(body, ctx) {
1017
+ switch (classifyContentType(body.contentType)) {
1018
+ case "text":
1019
+ return "string";
1020
+ case "binary":
1021
+ return "byte[]";
1022
+ default:
1023
+ return renderCSharpType(body.bodyType, ctx, false);
1024
+ }
1025
+ }
1026
+ __name(bodyCSharpType, "bodyCSharpType");
1027
+ function bodyReadExpr(body, ctx) {
1028
+ switch (classifyContentType(body.contentType)) {
1029
+ case "text":
1030
+ return "response.Text";
1031
+ case "binary":
1032
+ return "response.Bytes";
1033
+ default:
1034
+ return `http.ReadJson<${renderCSharpType(body.bodyType, ctx, false)}>(response)`;
1035
+ }
1036
+ }
1037
+ __name(bodyReadExpr, "bodyReadExpr");
1038
+ function returnStatements(shape, base, ctx, place) {
1039
+ if (shape.kind === "simple") {
1040
+ const response = shape.response;
1041
+ const body = response?.bodies[0];
1042
+ const headers = response?.headers ?? [];
1043
+ if (headers.length === 0) return body ? [
1044
+ ` return ${bodyReadExpr(body, ctx)};`
1045
+ ] : [];
1046
+ const lines2 = readHeaderLines(headers, `${base}Headers`, ctx, place, " ");
1047
+ return body ? [
1048
+ ...lines2,
1049
+ ` return new ${base}Result(${bodyReadExpr(body, ctx)}, headers);`
1050
+ ] : [
1051
+ ...lines2,
1052
+ " return headers;"
1053
+ ];
1054
+ }
1055
+ if (shape.kind === "multiMime") {
1056
+ const headers = shape.response.headers ?? [];
1057
+ const lines2 = headers.length > 0 ? readHeaderLines(headers, `${base}Headers`, ctx, place, " ") : [];
1058
+ lines2.push(...mimeSwitch(shape.response, base, void 0, ctx, " ", headers.length > 0));
1059
+ return lines2;
1060
+ }
1061
+ const [fallback, ...rest] = shape.responses;
1062
+ const lines = [
1063
+ " switch (response.Status)",
1064
+ " {"
1065
+ ];
1066
+ for (const response of rest) {
1067
+ lines.push(` case ${response.statusCode}:`);
1068
+ lines.push(" {");
1069
+ lines.push(...statusBranch(response, base, response.statusCode, ctx, place, " "));
1070
+ lines.push(" }");
1071
+ lines.push("");
1072
+ }
1073
+ lines.push(" default:");
1074
+ lines.push(" {");
1075
+ lines.push(...statusBranch(fallback, base, fallback.statusCode, ctx, place, " "));
1076
+ lines.push(" }");
1077
+ lines.push(" }");
1078
+ return lines;
1079
+ }
1080
+ __name(returnStatements, "returnStatements");
1081
+ function statusBranch(response, base, statusCode, ctx, place, indent) {
1082
+ const lines = [];
1083
+ const headers = response.headers ?? [];
1084
+ if (headers.length > 0) lines.push(...readHeaderLines(headers, headersRecordName(base, statusCode), ctx, place, indent));
1085
+ lines.push(...mimeSwitch(response, base, statusCode, ctx, indent, headers.length > 0));
1086
+ return lines;
1087
+ }
1088
+ __name(statusBranch, "statusBranch");
1089
+ function mimeSwitch(response, base, statusCode, ctx, indent, hasHeaders) {
1090
+ const bodies = response.bodies;
1091
+ const construct = /* @__PURE__ */ __name((body) => {
1092
+ const args = [];
1093
+ if (body) args.push(bodyReadExpr(body, ctx));
1094
+ if (hasHeaders) args.push("headers");
1095
+ return `new ${base}Response.${leafRecordName(response, body, statusCode)}(${args.join(", ")})`;
1096
+ }, "construct");
1097
+ if (bodies.length <= 1) return [
1098
+ `${indent}return ${construct(bodies[0])};`
1099
+ ];
1100
+ const [fallback, ...rest] = bodies;
1101
+ const lines = [
1102
+ `${indent}switch (response.ContentType)`,
1103
+ `${indent}{`
1104
+ ];
1105
+ for (const body of rest) {
1106
+ lines.push(`${indent} case ${quoteCSharpString(body.contentType)}:`);
1107
+ lines.push(`${indent} return ${construct(body)};`);
1108
+ }
1109
+ lines.push(`${indent} default:`);
1110
+ lines.push(`${indent} return ${construct(fallback)};`);
1111
+ lines.push(`${indent}}`);
1112
+ return lines;
1113
+ }
1114
+ __name(mimeSwitch, "mimeSwitch");
1115
+ function bodyArgument(op) {
1116
+ const body = op.request?.bodies[0];
1117
+ if (!body) return void 0;
1118
+ const mime = quoteCSharpString(body.contentType);
1119
+ switch (classifyContentType(body.contentType)) {
1120
+ case "multipart":
1121
+ return "content: http.MultipartContent(body)";
1122
+ case "urlencoded":
1123
+ return "content: http.FormContent(body)";
1124
+ case "text":
1125
+ return `content: http.TextContent(body, ${mime})`;
1126
+ case "binary":
1127
+ return `content: http.BinaryContent(body, ${mime})`;
1128
+ default:
1129
+ return `content: http.JsonContent(body, ${mime})`;
1130
+ }
1131
+ }
1132
+ __name(bodyArgument, "bodyArgument");
1133
+ function headersRecordName(base, statusCode) {
1134
+ return statusCode === void 0 ? `${base}Headers` : `${base}${statusCode}Headers`;
1135
+ }
1136
+ __name(headersRecordName, "headersRecordName");
1137
+ function leafRecordName(response, body, statusCode) {
1138
+ const statusPart = statusCode === void 0 ? "" : `Status${statusCode}`;
1139
+ if (response.bodies.length <= 1 || !body) return statusPart || "Body";
1140
+ return `${statusPart}${toCSharpTypeName(body.contentType.replace(/[+/.]/g, " "))}`;
1141
+ }
1142
+ __name(leafRecordName, "leafRecordName");
1143
+ function responseDeclarations(route, op, ctx) {
1144
+ const shape = responseShape(op);
1145
+ const base = methodBase(deriveMethodName(op, route));
1146
+ const place = where(route, op);
1147
+ const lines = [];
1148
+ const headerRecord = /* @__PURE__ */ __name((headers, name) => {
1149
+ const parameters = headers.map((header) => {
1150
+ const reader = headerReader(header, place);
1151
+ const type = header.optional ? `${reader.type}?` : reader.type;
1152
+ return `${type} ${safeMemberName(toCSharpPropertyName(header.name), name)}`;
1153
+ }).join(", ");
1154
+ lines.push("");
1155
+ lines.push(...xmlDocLines(`Response headers declared on ${place}.`, ""));
1156
+ lines.push(`public sealed record ${name}(${parameters});`);
1157
+ }, "headerRecord");
1158
+ if (shape.kind === "simple") {
1159
+ const response = shape.response;
1160
+ const headers = response?.headers ?? [];
1161
+ if (headers.length === 0) return lines;
1162
+ headerRecord(headers, headersRecordName(base));
1163
+ const body = response?.bodies[0];
1164
+ if (body) {
1165
+ lines.push("");
1166
+ lines.push(...xmlDocLines(`The body of ${place}, with the response headers the contract declares.`, ""));
1167
+ lines.push(`public sealed record ${base}Result(${bodyCSharpType(body, ctx)} Data, ${headersRecordName(base)} Headers);`);
1168
+ }
1169
+ return lines;
1170
+ }
1171
+ const responses = observableOf(shape);
1172
+ const withStatus = shape.kind === "multiStatus";
1173
+ for (const response of responses) {
1174
+ const headers = response.headers ?? [];
1175
+ if (headers.length > 0) headerRecord(headers, headersRecordName(base, withStatus ? response.statusCode : void 0));
1176
+ }
1177
+ lines.push("");
1178
+ lines.push(...xmlDocLines(`What ${place} returned.
1179
+
1180
+ ` + (withStatus ? "The operation declares several statuses the service produces, so the status is part of the value." : "The status declares several content types, so which one arrived is part of the value."), ""));
1181
+ lines.push(`public abstract record ${base}Response`);
1182
+ lines.push("{");
1183
+ lines.push(` private ${base}Response() { }`);
1184
+ for (const response of responses) {
1185
+ const statusCode = withStatus ? response.statusCode : void 0;
1186
+ const headers = response.headers ?? [];
1187
+ const bodies = response.bodies.length > 0 ? response.bodies : [
1188
+ void 0
1189
+ ];
1190
+ for (const body of bodies) {
1191
+ const name = leafRecordName(response, body, statusCode);
1192
+ const parameters = [];
1193
+ if (body) parameters.push(`${bodyCSharpType(body, ctx)} Data`);
1194
+ if (headers.length > 0) parameters.push(`${headersRecordName(base, statusCode)} Headers`);
1195
+ lines.push("");
1196
+ lines.push(` public sealed record ${name}(${parameters.join(", ")}) : ${base}Response;`);
1197
+ }
1198
+ }
1199
+ lines.push("}");
1200
+ return lines;
1201
+ }
1202
+ __name(responseDeclarations, "responseDeclarations");
1203
+ function headerReader(header, place) {
1204
+ const scalar = header.type.kind === "scalar" ? header.type.name : void 0;
1205
+ switch (scalar) {
1206
+ case "string":
1207
+ case "email":
1208
+ case "url":
1209
+ case "interval":
1210
+ case "unknown":
1211
+ return {
1212
+ type: "string",
1213
+ read: /* @__PURE__ */ __name((raw) => raw, "read")
1214
+ };
1215
+ case "number":
1216
+ return {
1217
+ type: "double",
1218
+ read: /* @__PURE__ */ __name((raw) => `double.Parse(${raw}, CultureInfo.InvariantCulture)`, "read")
1219
+ };
1220
+ case "int":
1221
+ return {
1222
+ type: "long",
1223
+ read: /* @__PURE__ */ __name((raw) => `long.Parse(${raw}, CultureInfo.InvariantCulture)`, "read")
1224
+ };
1225
+ case "bigint":
1226
+ return {
1227
+ type: "BigInteger",
1228
+ read: /* @__PURE__ */ __name((raw) => `BigInteger.Parse(${raw}, CultureInfo.InvariantCulture)`, "read")
1229
+ };
1230
+ case "boolean":
1231
+ return {
1232
+ type: "bool",
1233
+ read: /* @__PURE__ */ __name((raw) => `${raw} == "true"`, "read")
1234
+ };
1235
+ case "uuid":
1236
+ return {
1237
+ type: "Guid",
1238
+ read: /* @__PURE__ */ __name((raw) => `Guid.Parse(${raw})`, "read")
1239
+ };
1240
+ case "date":
1241
+ return {
1242
+ type: "DateOnly",
1243
+ read: /* @__PURE__ */ __name((raw) => `DateOnly.Parse(${raw}, CultureInfo.InvariantCulture)`, "read")
1244
+ };
1245
+ case "time":
1246
+ return {
1247
+ type: "TimeOnly",
1248
+ read: /* @__PURE__ */ __name((raw) => `TimeOnly.Parse(${raw}, CultureInfo.InvariantCulture)`, "read")
1249
+ };
1250
+ case "datetime":
1251
+ return {
1252
+ type: "DateTimeOffset",
1253
+ read: /* @__PURE__ */ __name((raw) => `DateTimeOffset.Parse(${raw}, CultureInfo.InvariantCulture)`, "read")
1254
+ };
1255
+ case "duration":
1256
+ return {
1257
+ type: "TimeSpan",
1258
+ read: /* @__PURE__ */ __name((raw) => `XmlConvert.ToTimeSpan(${raw})`, "read")
1259
+ };
1260
+ default:
1261
+ throw new Error(`plugin-csharp: response header '${header.name}' on ${place} is declared as ${describeHeaderType(header.type)}, which cannot be read from an HTTP header. Header values arrive as strings \u2014 declare it as string, email, url, uuid, date, time, datetime, duration, interval, int, number, boolean or bigint.`);
1262
+ }
1263
+ }
1264
+ __name(headerReader, "headerReader");
1265
+ function describeHeaderType(type) {
1266
+ if (type.kind === "scalar") return `the '${type.name}' scalar`;
1267
+ if (type.kind === "ref") return `the contract '${type.name}'`;
1268
+ return `${type.kind === "array" || type.kind === "inlineObject" ? "an" : "a"} ${type.kind}`;
1269
+ }
1270
+ __name(describeHeaderType, "describeHeaderType");
1271
+ function readHeaderLines(headers, typeName, ctx, place, indent) {
1272
+ const args = headers.map((header) => {
1273
+ const reader = headerReader(header, place);
1274
+ const name = quoteCSharpString(header.name);
1275
+ if (!header.optional) return reader.read(`http.RequireHeader(response, ${name})`);
1276
+ const local = toCSharpParameterName(header.name);
1277
+ return `response.Header(${name}) is { } ${local} ? ${reader.read(local)} : null`;
1278
+ });
1279
+ const lines = [
1280
+ `${indent}var headers = new ${typeName}(`
1281
+ ];
1282
+ args.forEach((arg, index) => lines.push(`${indent} ${arg}${index === args.length - 1 ? ");" : ","}`));
1283
+ return lines;
1284
+ }
1285
+ __name(readHeaderLines, "readHeaderLines");
1286
+ function methodDoc(route, op, observable) {
1287
+ const lines = [];
1288
+ const parts = [];
1289
+ if (op.name) parts.push(op.name);
1290
+ const description = op.description ?? route.description;
1291
+ if (description) parts.push(description);
1292
+ if (parts.length > 0) lines.push(...xmlDocLines(parts.join("\n"), ""));
1293
+ const thrown = op.responses.filter((r) => !observable.includes(r)).map((r) => r.statusCode);
1294
+ if (thrown.length > 0) lines.push(`/// <exception cref="SdkException">On ${thrown.join(", ")}.</exception>`);
1295
+ return lines;
1296
+ }
1297
+ __name(methodDoc, "methodDoc");
1298
+ function httpMethodConstant(method) {
1299
+ const lower = method.toLowerCase();
1300
+ return lower.charAt(0).toUpperCase() + lower.slice(1);
1301
+ }
1302
+ __name(httpMethodConstant, "httpMethodConstant");
1303
+ var PATH_PLACEHOLDER = /\{([a-zA-Z_$][a-zA-Z0-9_$.-]*)\}/g;
1304
+ function buildPathExpression(path, params) {
1305
+ const args = path.split("/").filter(Boolean).map((raw) => {
1306
+ PATH_PLACEHOLDER.lastIndex = 0;
1307
+ const match = PATH_PLACEHOLDER.exec(raw);
1308
+ if (!match || match[0] !== raw) return quoteCSharpString(raw);
1309
+ const value = params && params.kind !== "params" ? `pathParams.${toCSharpPropertyName(match[1])}` : toCSharpParameterName(match[1]);
1310
+ return `http.Segment(${value})`;
1311
+ });
1312
+ return `http.Path(${args.join(", ")})`;
1313
+ }
1314
+ __name(buildPathExpression, "buildPathExpression");
1315
+ function buildMethodParams(route, op, ctx) {
1316
+ const params = [];
1317
+ if (route.params) {
1318
+ if (route.params.kind === "params") {
1319
+ for (const node of route.params.nodes) {
1320
+ params.push({
1321
+ name: toCSharpParameterName(node.name),
1322
+ type: renderCSharpType(node.type, ctx, true),
1323
+ optional: false
1324
+ });
1325
+ }
1326
+ } else {
1327
+ params.push({
1328
+ name: "pathParams",
1329
+ type: renderParamSourceType(route.params, ctx, ""),
1330
+ optional: false
1331
+ });
1332
+ }
1333
+ }
1334
+ const body = op.request?.bodies[0];
1335
+ if (body) {
1336
+ switch (classifyContentType(body.contentType)) {
1337
+ case "multipart":
1338
+ params.push({
1339
+ name: "body",
1340
+ type: "IEnumerable<SdkPart>",
1341
+ optional: false
1342
+ });
1343
+ break;
1344
+ case "binary":
1345
+ params.push({
1346
+ name: "body",
1347
+ type: "byte[]",
1348
+ optional: false
1349
+ });
1350
+ break;
1351
+ case "text":
1352
+ params.push({
1353
+ name: "body",
1354
+ type: "string",
1355
+ optional: false
1356
+ });
1357
+ break;
1358
+ default:
1359
+ params.push({
1360
+ name: "body",
1361
+ type: renderCSharpType(body.bodyType, ctx, true),
1362
+ optional: false
1363
+ });
1364
+ }
1365
+ }
1366
+ const base = methodBase(deriveMethodName(op, route));
1367
+ if (op.query) {
1368
+ params.push({
1369
+ name: "query",
1370
+ type: renderParamSourceType(op.query, ctx, `${base}Query`),
1371
+ optional: allFieldsOptional(op.query)
1372
+ });
1373
+ }
1374
+ if (op.headers) {
1375
+ params.push({
1376
+ name: "customHeaders",
1377
+ type: renderParamSourceType(op.headers, ctx, `${base}Headers`),
1378
+ optional: allFieldsOptional(op.headers)
1379
+ });
1380
+ }
1381
+ const widened = params.map((p) => p.optional && !p.type.endsWith("?") ? {
1382
+ ...p,
1383
+ type: `${p.type}?`
1384
+ } : p);
1385
+ return [
1386
+ ...widened.filter((p) => !p.optional),
1387
+ ...widened.filter((p) => p.optional)
1388
+ ];
1389
+ }
1390
+ __name(buildMethodParams, "buildMethodParams");
1391
+ function allFieldsOptional(source) {
1392
+ if (source.kind !== "params") return true;
1393
+ return source.nodes.every((node) => Boolean(node.optional) || node.default !== void 0);
1394
+ }
1395
+ __name(allFieldsOptional, "allFieldsOptional");
1396
+ function renderParamSourceType(source, ctx, generatedName) {
1397
+ if (source.kind === "ref") return renderCSharpType({
1398
+ kind: "ref",
1399
+ name: source.name
1400
+ }, ctx, true);
1401
+ if (source.kind === "type") return renderCSharpType(source.node, ctx, true);
1402
+ return source.nodes.length > 0 ? generatedName : "IReadOnlyDictionary<string, string>";
1403
+ }
1404
+ __name(renderParamSourceType, "renderParamSourceType");
1405
+ function deriveMethodName(op, route) {
1406
+ if (op.sdk) return `${toCSharpTypeName(op.sdk)}Async`;
1407
+ if (op.name) return `${toCSharpTypeName(op.name)}Async`;
1408
+ return `${inferMethodName(op.method, route.path)}Async`;
1409
+ }
1410
+ __name(deriveMethodName, "deriveMethodName");
1411
+ function inferMethodName(method, path) {
1412
+ const parts = [
1413
+ toCSharpTypeName(method)
1414
+ ];
1415
+ for (const segment of path.split("/").filter(Boolean)) {
1416
+ if (segment.startsWith("{")) parts.push(`By${toCSharpTypeName(segment.slice(1, -1))}`);
1417
+ else parts.push(toCSharpTypeName(segment));
1418
+ }
1419
+ return parts.join("");
1420
+ }
1421
+ __name(inferMethodName, "inferMethodName");
1422
+
1423
+ // src/codegen-sdk.ts
1424
+ function generateSdkCs(namespaceName, sdkName, clients) {
1425
+ const lines = [
1426
+ "// <auto-generated/>",
1427
+ "// Generated by @contractkit/plugin-csharp. Do not edit manually.",
1428
+ "#nullable enable",
1429
+ ""
1430
+ ];
1431
+ lines.push("using System;");
1432
+ if (clients.length > 0) lines.push(`using ${namespaceName}.Clients;`);
1433
+ lines.push(`using ${namespaceName}.Runtime;`);
1434
+ lines.push("");
1435
+ lines.push(`namespace ${namespaceName};`);
1436
+ lines.push("");
1437
+ lines.push(...xmlDocLines("Entry point to the generated SDK.\n\nHolds one SdkHttp, shared by every client, so the SDK keeps a single connection pool.\nDisposing it disposes the underlying HttpClient, unless you supplied your own.", ""));
1438
+ lines.push(`public sealed class ${sdkName} : IDisposable`);
1439
+ lines.push("{");
1440
+ lines.push(` public ${sdkName}(SdkOptions options)`);
1441
+ lines.push(" {");
1442
+ lines.push(" Http = new SdkHttp(options);");
1443
+ for (const client of clients) lines.push(` ${client.propertyName} = new ${client.className}(Http);`);
1444
+ lines.push(" }");
1445
+ lines.push("");
1446
+ lines.push(" public SdkHttp Http { get; }");
1447
+ for (const client of clients) {
1448
+ lines.push("");
1449
+ lines.push(` public ${client.className} ${client.propertyName} { get; }`);
1450
+ }
1451
+ lines.push("");
1452
+ lines.push(" public void Dispose()");
1453
+ lines.push(" {");
1454
+ lines.push(" Http.Dispose();");
1455
+ lines.push(" }");
1456
+ lines.push("}");
1457
+ lines.push("");
1458
+ return lines.join("\n");
1459
+ }
1460
+ __name(generateSdkCs, "generateSdkCs");
1461
+
1462
+ // src/hoist.ts
1463
+ import { collectTypeRefs, resolveEffectiveFields as resolveEffectiveFields2 } from "@contractkit/core";
1464
+ function collectHoistedTypes(roots, opts) {
1465
+ const state = {
1466
+ ...opts,
1467
+ byNode: /* @__PURE__ */ new Map(),
1468
+ byName: /* @__PURE__ */ new Map(),
1469
+ byFile: /* @__PURE__ */ new Map(),
1470
+ memberships: /* @__PURE__ */ new Map(),
1471
+ taken: new Set(roots.flatMap((r) => r.models.map((m) => m.name)))
1472
+ };
1473
+ for (const root of roots) {
1474
+ for (const model of root.models) {
1475
+ if (model.type) {
1476
+ walkType(model.type, model.name, root.file, state, true, model.description);
1477
+ }
1478
+ for (const field of model.fields) {
1479
+ walkType(field.type, `${model.name}${toCSharpTypeName(field.name)}`, root.file, state, false, field.description);
1480
+ }
1481
+ }
1482
+ }
1483
+ return {
1484
+ byNode: state.byNode,
1485
+ byName: state.byName,
1486
+ byFile: state.byFile,
1487
+ memberships: state.memberships
1488
+ };
1489
+ }
1490
+ __name(collectHoistedTypes, "collectHoistedTypes");
1491
+ function walkType(type, path, ownerFile, state, atAliasRoot, description) {
1492
+ switch (type.kind) {
1493
+ case "union":
1494
+ hoistPlainUnion(type, path, ownerFile, state, atAliasRoot, description);
1495
+ return;
1496
+ case "discriminatedUnion":
1497
+ hoistDiscriminatedUnion(type, path, ownerFile, state, atAliasRoot, description);
1498
+ return;
1499
+ case "enum":
1500
+ if (!atAliasRoot) {
1501
+ hoist(type, {
1502
+ kind: "enum",
1503
+ name: claimFor(path, state, false),
1504
+ ownerFile,
1505
+ needsInput: false,
1506
+ values: type.values,
1507
+ description
1508
+ }, state);
1509
+ }
1510
+ return;
1511
+ case "inlineObject":
1512
+ if (!atAliasRoot) hoistRecord(type, type.fields, path, ownerFile, state, description);
1513
+ else type.fields.forEach((f) => walkType(f.type, `${path}${toCSharpTypeName(f.name)}`, ownerFile, state, false, f.description));
1514
+ return;
1515
+ case "intersection": {
1516
+ if (atAliasRoot) {
1517
+ type.members.forEach((m) => walkType(m, path, ownerFile, state, true));
1518
+ return;
1519
+ }
1520
+ const { fields } = resolveEffectiveFields2(type, state.modelIndex);
1521
+ hoistRecord(type, fields, path, ownerFile, state, description);
1522
+ return;
1523
+ }
1524
+ case "tuple":
1525
+ type.items.forEach((item, i) => walkType(item, `${path}Item${i}`, ownerFile, state, false));
1526
+ hoist(type, {
1527
+ kind: "tuple",
1528
+ name: claimFor(path, state, false),
1529
+ ownerFile,
1530
+ needsInput: type.items.some((t) => typeNeedsInput(t, state)),
1531
+ items: type.items,
1532
+ description
1533
+ }, state);
1534
+ return;
1535
+ case "array":
1536
+ walkType(type.item, path, ownerFile, state, false);
1537
+ return;
1538
+ case "record":
1539
+ walkType(type.value, path, ownerFile, state, false);
1540
+ return;
1541
+ case "lazy":
1542
+ walkType(type.inner, path, ownerFile, state, atAliasRoot, description);
1543
+ return;
1544
+ default:
1545
+ return;
1546
+ }
1547
+ }
1548
+ __name(walkType, "walkType");
1549
+ function hoistRecord(node, fields, path, ownerFile, state, description) {
1550
+ const name = claimFor(path, state, false);
1551
+ for (const f of fields) walkType(f.type, `${name}${toCSharpTypeName(f.name)}`, ownerFile, state, false, f.description);
1552
+ hoist(node, {
1553
+ kind: "record",
1554
+ name,
1555
+ ownerFile,
1556
+ needsInput: fields.some((f) => f.visibility !== "normal" || typeNeedsInput(f.type, state)),
1557
+ fields,
1558
+ description
1559
+ }, state);
1560
+ }
1561
+ __name(hoistRecord, "hoistRecord");
1562
+ function hoistPlainUnion(type, path, ownerFile, state, atAliasRoot, description) {
1563
+ const nullable = type.members.some((m) => m.kind === "scalar" && m.name === "null");
1564
+ const members = type.members.filter((m) => !(m.kind === "scalar" && m.name === "null"));
1565
+ if (members.length <= 1) {
1566
+ if (members[0]) walkType(members[0], path, ownerFile, state, false);
1567
+ return;
1568
+ }
1569
+ if (members.every((m) => m.kind === "literal" && typeof m.value === "string")) {
1570
+ const values = members.map((m) => String(m.value));
1571
+ hoist(type, {
1572
+ kind: "enum",
1573
+ name: claimFor(path, state, atAliasRoot),
1574
+ ownerFile,
1575
+ needsInput: false,
1576
+ nullable,
1577
+ values,
1578
+ description
1579
+ }, state);
1580
+ return;
1581
+ }
1582
+ const name = claimFor(path, state, atAliasRoot);
1583
+ const used = /* @__PURE__ */ new Set();
1584
+ const hoisted = [];
1585
+ for (const member of members) {
1586
+ walkType(member, `${name}${toCSharpTypeName(memberLabel(member, state))}`, ownerFile, state, false);
1587
+ const typeName = memberTypeName(member, state);
1588
+ hoisted.push({
1589
+ typeName,
1590
+ wrapperName: uniqueIn(`Of${toCSharpTypeName(memberLabel(member, state))}`, used),
1591
+ type: member
1592
+ });
1593
+ }
1594
+ hoist(type, {
1595
+ kind: "plainUnion",
1596
+ name,
1597
+ ownerFile,
1598
+ needsInput: members.some((m) => typeNeedsInput(m, state)),
1599
+ nullable,
1600
+ members: hoisted,
1601
+ description
1602
+ }, state);
1603
+ }
1604
+ __name(hoistPlainUnion, "hoistPlainUnion");
1605
+ function hoistDiscriminatedUnion(type, path, ownerFile, state, atAliasRoot, description) {
1606
+ const name = claimFor(path, state, atAliasRoot);
1607
+ const members = [];
1608
+ for (const member of type.members) {
1609
+ const { fields } = resolveEffectiveFields2(member, state.modelIndex);
1610
+ const discriminatorField = fields.find((f) => f.name === type.discriminator);
1611
+ const tagType = discriminatorField?.type.kind === "lazy" ? discriminatorField.type.inner : discriminatorField?.type;
1612
+ if (!tagType || tagType.kind !== "literal") {
1613
+ state.warn?.(`Discriminated union '${name}' has a member whose '${type.discriminator}' is not a literal, so its tag is not known at build time; emitting a raw JSON value instead of an interface.`, ownerFile);
1614
+ release(name, state, atAliasRoot);
1615
+ return;
1616
+ }
1617
+ const tag = String(tagType.value);
1618
+ if (member.kind === "ref") {
1619
+ members.push({
1620
+ typeName: member.name,
1621
+ tag,
1622
+ type: member
1623
+ });
1624
+ } else {
1625
+ const memberPath = `${name}${toCSharpTypeName(tag)}`;
1626
+ hoistRecord(member, fields, memberPath, ownerFile, state, void 0);
1627
+ const decl2 = state.byNode.get(member);
1628
+ if (!decl2) {
1629
+ release(name, state, atAliasRoot);
1630
+ return;
1631
+ }
1632
+ members.push({
1633
+ typeName: decl2.name,
1634
+ tag,
1635
+ type: member
1636
+ });
1637
+ }
1638
+ }
1639
+ if (members.length === 0) {
1640
+ release(name, state, atAliasRoot);
1641
+ return;
1642
+ }
1643
+ const decl = {
1644
+ kind: "discriminatedUnion",
1645
+ name,
1646
+ ownerFile,
1647
+ needsInput: type.members.some((m) => typeNeedsInput(m, state)),
1648
+ members,
1649
+ discriminator: type.discriminator,
1650
+ description
1651
+ };
1652
+ hoist(type, decl, state);
1653
+ for (const member of members) {
1654
+ const list = state.memberships.get(member.typeName) ?? [];
1655
+ if (!list.includes(name)) list.push(name);
1656
+ state.memberships.set(member.typeName, list);
1657
+ }
1658
+ }
1659
+ __name(hoistDiscriminatedUnion, "hoistDiscriminatedUnion");
1660
+ function memberLabel(type, state) {
1661
+ switch (type.kind) {
1662
+ case "ref":
1663
+ return type.name;
1664
+ case "scalar":
1665
+ return type.name;
1666
+ case "array":
1667
+ return `${memberLabel(type.item, state)}List`;
1668
+ case "record":
1669
+ return `${memberLabel(type.value, state)}Map`;
1670
+ case "literal":
1671
+ return typeof type.value === "string" ? type.value : String(type.value);
1672
+ case "lazy":
1673
+ return memberLabel(type.inner, state);
1674
+ default: {
1675
+ const decl = state.byNode.get(type);
1676
+ return decl ? decl.name : "Member";
1677
+ }
1678
+ }
1679
+ }
1680
+ __name(memberLabel, "memberLabel");
1681
+ function memberTypeName(type, state) {
1682
+ const decl = state.byNode.get(type);
1683
+ if (decl) return decl.name;
1684
+ if (type.kind === "ref") return type.name;
1685
+ return "";
1686
+ }
1687
+ __name(memberTypeName, "memberTypeName");
1688
+ function typeNeedsInput(type, state) {
1689
+ const refs = /* @__PURE__ */ new Set();
1690
+ collectTypeRefs(type, refs);
1691
+ if ([
1692
+ ...refs
1693
+ ].some((r) => state.modelsWithInput.has(r))) return true;
1694
+ return hasVisibilityField(type);
1695
+ }
1696
+ __name(typeNeedsInput, "typeNeedsInput");
1697
+ function hasVisibilityField(type) {
1698
+ switch (type.kind) {
1699
+ case "inlineObject":
1700
+ return type.fields.some((f) => f.visibility !== "normal" || hasVisibilityField(f.type));
1701
+ case "array":
1702
+ return hasVisibilityField(type.item);
1703
+ case "record":
1704
+ return hasVisibilityField(type.value);
1705
+ case "lazy":
1706
+ return hasVisibilityField(type.inner);
1707
+ case "tuple":
1708
+ return type.items.some(hasVisibilityField);
1709
+ case "union":
1710
+ case "intersection":
1711
+ case "discriminatedUnion":
1712
+ return type.members.some(hasVisibilityField);
1713
+ default:
1714
+ return false;
1715
+ }
1716
+ }
1717
+ __name(hasVisibilityField, "hasVisibilityField");
1718
+ function hoist(node, decl, state) {
1719
+ state.byNode.set(node, decl);
1720
+ state.byName.set(decl.name, decl);
1721
+ const list = state.byFile.get(decl.ownerFile) ?? [];
1722
+ list.push(decl);
1723
+ state.byFile.set(decl.ownerFile, list);
1724
+ }
1725
+ __name(hoist, "hoist");
1726
+ function claimFor(path, state, atAliasRoot) {
1727
+ if (atAliasRoot) return path;
1728
+ return uniqueIn(sanitizeCSharpTypeName(path), state.taken);
1729
+ }
1730
+ __name(claimFor, "claimFor");
1731
+ function release(name, state, atAliasRoot) {
1732
+ if (!atAliasRoot) state.taken.delete(name);
1733
+ }
1734
+ __name(release, "release");
1735
+ function uniqueIn(base, taken) {
1736
+ if (!taken.has(base)) {
1737
+ taken.add(base);
1738
+ return base;
1739
+ }
1740
+ let n = 2;
1741
+ while (taken.has(`${base}${n}`)) n++;
1742
+ taken.add(`${base}${n}`);
1743
+ return `${base}${n}`;
1744
+ }
1745
+ __name(uniqueIn, "uniqueIn");
1746
+
1747
+ // src/runtime.ts
1748
+ function generateRuntimeCs(namespaceName) {
1749
+ return `// <auto-generated/>
1750
+ // Generated by @contractkit/plugin-csharp. Do not edit manually.
1751
+ #nullable enable
1752
+
1753
+ using System;
1754
+ using System.Collections.Generic;
1755
+ using System.Linq;
1756
+ using System.Net;
1757
+ using System.Net.Http;
1758
+ using System.Net.Http.Headers;
1759
+ using System.Text;
1760
+ using System.Text.Json;
1761
+ using System.Threading;
1762
+ using System.Threading.Tasks;
1763
+
1764
+ namespace ${namespaceName}.Runtime;
1765
+
1766
+ /// <summary>
1767
+ /// How to reach the service.
1768
+ /// </summary>
1769
+ public sealed class SdkOptions
1770
+ {
1771
+ /// <summary>Origin, optionally with a path prefix. Operation paths are appended to it.</summary>
1772
+ public required string BaseUrl { get; init; }
1773
+
1774
+ /// <summary>
1775
+ /// Called once per request. Authentication belongs here: returning a fresh map each time lets a
1776
+ /// token be refreshed without rebuilding the SDK.
1777
+ /// </summary>
1778
+ public Func<CancellationToken, ValueTask<IReadOnlyDictionary<string, string>>>? Headers { get; init; }
1779
+
1780
+ /// <summary>
1781
+ /// Supply your own client to control handlers, proxies or retries. When you do, the SDK never
1782
+ /// disposes it. Leave it null and the SDK creates and owns one.
1783
+ /// </summary>
1784
+ public HttpClient? HttpClient { get; init; }
1785
+
1786
+ /// <summary>
1787
+ /// How bodies, query values and headers are serialized. Defaults to <see cref="SdkJson.Options"/>,
1788
+ /// which carries the converters the contract's scalar types need.
1789
+ /// </summary>
1790
+ public JsonSerializerOptions Json { get; init; } = SdkJson.Options;
1791
+ }
1792
+
1793
+ /// <summary>
1794
+ /// A status the contract does not account for.
1795
+ /// </summary>
1796
+ /// <remarks>
1797
+ /// Derives from <see cref="HttpRequestException"/>, so it can be caught alongside any other client
1798
+ /// failure, and passes the status through to <c>StatusCode</c> for callers that catch the base type.
1799
+ /// </remarks>
1800
+ public class SdkException : HttpRequestException
1801
+ {
1802
+ public SdkException(int status, string body, HttpResponseHeaders? responseHeaders = null, string? message = null)
1803
+ : base(message ?? $"Request failed with status {status}", null, ToStatusCode(status))
1804
+ {
1805
+ Status = status;
1806
+ Body = body;
1807
+ ResponseHeaders = responseHeaders;
1808
+ }
1809
+
1810
+ /// <summary>The HTTP status the service returned.</summary>
1811
+ public int Status { get; }
1812
+
1813
+ /// <summary>The raw response body, read as UTF-8 text.</summary>
1814
+ public string Body { get; }
1815
+
1816
+ /// <summary>The response headers, when the failure came from a response rather than a missing header.</summary>
1817
+ public HttpResponseHeaders? ResponseHeaders { get; }
1818
+
1819
+ private bool _jsonParsed;
1820
+ private JsonElement? _json;
1821
+
1822
+ /// <summary>
1823
+ /// The body parsed as JSON, or null when it is not JSON. Error contracts usually are.
1824
+ /// </summary>
1825
+ public JsonElement? Json
1826
+ {
1827
+ get
1828
+ {
1829
+ if (_jsonParsed) return _json;
1830
+ _jsonParsed = true;
1831
+ try
1832
+ {
1833
+ using var document = JsonDocument.Parse(Body);
1834
+ // Clone detaches the element from the document being disposed here.
1835
+ _json = document.RootElement.Clone();
1836
+ }
1837
+ catch (JsonException)
1838
+ {
1839
+ _json = null;
1840
+ }
1841
+ return _json;
1842
+ }
1843
+ }
1844
+
1845
+ /// <summary>
1846
+ /// Read the error body as <typeparamref name="T"/>, or null when it does not parse. Operations
1847
+ /// whose thrown statuses declare a body generate an alias naming the type to use here.
1848
+ /// </summary>
1849
+ public T? TryReadBody<T>(JsonSerializerOptions? options = null)
1850
+ where T : class
1851
+ {
1852
+ try
1853
+ {
1854
+ return JsonSerializer.Deserialize<T>(Body, options ?? SdkJson.Options);
1855
+ }
1856
+ catch (JsonException)
1857
+ {
1858
+ return null;
1859
+ }
1860
+ }
1861
+
1862
+ private static HttpStatusCode? ToStatusCode(int status) =>
1863
+ status is >= 100 and <= 599 ? (HttpStatusCode)status : null;
1864
+ }
1865
+
1866
+ /// <summary>
1867
+ /// One response, with its body already read.
1868
+ /// </summary>
1869
+ /// <remarks>
1870
+ /// Reading the body eagerly is what allows a generated method to check the status, then the content
1871
+ /// type, then decode, which is exactly what an operation declaring several statuses or several
1872
+ /// mimes has to do.
1873
+ /// </remarks>
1874
+ public sealed class SdkResponse
1875
+ {
1876
+ private string? _text;
1877
+
1878
+ public SdkResponse(HttpResponseMessage message, byte[] bytes)
1879
+ {
1880
+ Message = message;
1881
+ Bytes = bytes;
1882
+ }
1883
+
1884
+ public HttpResponseMessage Message { get; }
1885
+
1886
+ /// <summary>The body as raw bytes.</summary>
1887
+ public byte[] Bytes { get; }
1888
+
1889
+ public int Status => (int)Message.StatusCode;
1890
+
1891
+ /// <summary>The response mime without its parameters, so <c>application/json; charset=utf-8</c> matches.</summary>
1892
+ public string ContentType => Message.Content.Headers.ContentType?.MediaType ?? string.Empty;
1893
+
1894
+ /// <summary>The body decoded as UTF-8.</summary>
1895
+ public string Text => _text ??= Encoding.UTF8.GetString(Bytes);
1896
+
1897
+ /// <summary>
1898
+ /// The first value of a response or content header, or null when the service did not send it.
1899
+ /// </summary>
1900
+ public string? Header(string name)
1901
+ {
1902
+ if (Message.Headers.TryGetValues(name, out var values)) return values.FirstOrDefault();
1903
+ if (Message.Content.Headers.TryGetValues(name, out var contentValues)) return contentValues.FirstOrDefault();
1904
+ return null;
1905
+ }
1906
+ }
1907
+
1908
+ /// <summary>
1909
+ /// One part of a multipart request body.
1910
+ /// </summary>
1911
+ public sealed record SdkPart(string Name, HttpContent Content, string? FileName = null)
1912
+ {
1913
+ /// <summary>A plain text field.</summary>
1914
+ public static SdkPart Text(string name, string value) => new(name, new StringContent(value, Encoding.UTF8));
1915
+
1916
+ /// <summary>A file field, sent with a filename and its own content type.</summary>
1917
+ public static SdkPart File(string name, byte[] bytes, string fileName, string contentType = "application/octet-stream")
1918
+ {
1919
+ var content = new ByteArrayContent(bytes);
1920
+ content.Headers.ContentType = new MediaTypeHeaderValue(contentType);
1921
+ return new SdkPart(name, content, fileName);
1922
+ }
1923
+ }
1924
+
1925
+ /// <summary>
1926
+ /// Issues requests and turns anything the contract does not describe into an <see cref="SdkException"/>.
1927
+ /// </summary>
1928
+ /// <remarks>
1929
+ /// Every value bound for a path, query, header or form field is turned into text by serializing it
1930
+ /// the same way it would be serialized into a JSON body. That is what keeps a <c>Guid</c>, a
1931
+ /// <c>DateTimeOffset</c>, a <c>TimeSpan</c> or an enum spelled identically wherever it appears in a
1932
+ /// request, without a line of per-type code in the generator.
1933
+ /// </remarks>
1934
+ public sealed class SdkHttp : IDisposable
1935
+ {
1936
+ private readonly SdkOptions _options;
1937
+ private readonly bool _ownsClient;
1938
+
1939
+ public SdkHttp(SdkOptions options)
1940
+ {
1941
+ _options = options;
1942
+ _ownsClient = options.HttpClient is null;
1943
+ Client = options.HttpClient ?? new HttpClient();
1944
+ Json = options.Json;
1945
+ }
1946
+
1947
+ public HttpClient Client { get; }
1948
+
1949
+ public JsonSerializerOptions Json { get; }
1950
+
1951
+ /// <summary>
1952
+ /// Send one request and read its body.
1953
+ /// </summary>
1954
+ /// <remarks>
1955
+ /// <c>expectStatuses</c> carries the statuses the operation declares as outcomes rather than
1956
+ /// failures, such as a 404 the contract gives a meaning. Everything outside 2xx and that set
1957
+ /// throws.
1958
+ /// </remarks>
1959
+ /// <exception cref="SdkException">On a status the contract does not declare.</exception>
1960
+ public async Task<SdkResponse> ExecuteAsync(
1961
+ HttpMethod method,
1962
+ string path,
1963
+ IEnumerable<KeyValuePair<string, string>>? query = null,
1964
+ IEnumerable<KeyValuePair<string, string>>? headers = null,
1965
+ HttpContent? content = null,
1966
+ IReadOnlyCollection<int>? expectStatuses = null,
1967
+ CancellationToken cancellationToken = default)
1968
+ {
1969
+ using var request = new HttpRequestMessage(method, BuildUrl(path, query));
1970
+ if (content is not null) request.Content = content;
1971
+
1972
+ if (_options.Headers is not null)
1973
+ {
1974
+ foreach (var entry in await _options.Headers(cancellationToken).ConfigureAwait(false))
1975
+ {
1976
+ request.Headers.TryAddWithoutValidation(entry.Key, entry.Value);
1977
+ }
1978
+ }
1979
+
1980
+ if (headers is not null)
1981
+ {
1982
+ foreach (var entry in headers)
1983
+ {
1984
+ request.Headers.TryAddWithoutValidation(entry.Key, entry.Value);
1985
+ }
1986
+ }
1987
+
1988
+ var message = await Client.SendAsync(request, HttpCompletionOption.ResponseContentRead, cancellationToken).ConfigureAwait(false);
1989
+ var bytes = await message.Content.ReadAsByteArrayAsync(cancellationToken).ConfigureAwait(false);
1990
+ var response = new SdkResponse(message, bytes);
1991
+
1992
+ var status = response.Status;
1993
+ if ((status < 200 || status > 299) && (expectStatuses is null || !expectStatuses.Contains(status)))
1994
+ {
1995
+ throw new SdkException(status, response.Text, message.Headers);
1996
+ }
1997
+
1998
+ return response;
1999
+ }
2000
+
2001
+ /// <summary>Decode a JSON response body.</summary>
2002
+ /// <exception cref="SdkException">When the body is JSON null.</exception>
2003
+ public T ReadJson<T>(SdkResponse response)
2004
+ {
2005
+ var value = JsonSerializer.Deserialize<T>(response.Bytes, Json);
2006
+ if (value is null)
2007
+ {
2008
+ throw new SdkException(response.Status, response.Text, response.Message.Headers, "Response body was null");
2009
+ }
2010
+
2011
+ return value;
2012
+ }
2013
+
2014
+ /// <summary>
2015
+ /// Read a response header the contract declares as required.
2016
+ /// </summary>
2017
+ /// <exception cref="SdkException">When the service did not send it, since the caller was promised a value.</exception>
2018
+ public string RequireHeader(SdkResponse response, string name) =>
2019
+ response.Header(name)
2020
+ ?? throw new SdkException(response.Status, response.Text, response.Message.Headers, $"Response is missing the required header '{name}'");
2021
+
2022
+ /// <summary>Join path segments onto the base URL. Each segment is already escaped.</summary>
2023
+ public string Path(params string[] segments) => segments.Length == 0 ? string.Empty : "/" + string.Join("/", segments);
2024
+
2025
+ /// <summary>The escaped text form of a single value, for use as a path segment.</summary>
2026
+ public string Segment<T>(T value) => Uri.EscapeDataString(ScalarText(JsonSerializer.SerializeToElement(value, Json)) ?? string.Empty);
2027
+
2028
+ /// <summary>
2029
+ /// Every property of <paramref name="value"/> as a key and value pair. A list property repeats
2030
+ /// its key; a null property is omitted.
2031
+ /// </summary>
2032
+ public IEnumerable<KeyValuePair<string, string>> Params<T>(T value)
2033
+ {
2034
+ if (value is null) yield break;
2035
+
2036
+ var element = JsonSerializer.SerializeToElement(value, Json);
2037
+ if (element.ValueKind != JsonValueKind.Object) yield break;
2038
+
2039
+ foreach (var property in element.EnumerateObject())
2040
+ {
2041
+ if (property.Value.ValueKind == JsonValueKind.Array)
2042
+ {
2043
+ foreach (var item in property.Value.EnumerateArray())
2044
+ {
2045
+ if (ScalarText(item) is { } itemText) yield return new KeyValuePair<string, string>(property.Name, itemText);
2046
+ }
2047
+ }
2048
+ else if (ScalarText(property.Value) is { } text)
2049
+ {
2050
+ yield return new KeyValuePair<string, string>(property.Name, text);
2051
+ }
2052
+ }
2053
+ }
2054
+
2055
+ /// <summary>A JSON body. Sent as a buffered string so the request carries a Content-Length.</summary>
2056
+ public HttpContent JsonContent<T>(T value, string mediaType) =>
2057
+ new StringContent(JsonSerializer.Serialize(value, Json), Encoding.UTF8, mediaType);
2058
+
2059
+ /// <summary>A form-encoded body, built from the same property walk as the query string.</summary>
2060
+ public HttpContent FormContent<T>(T value) => new FormUrlEncodedContent(Params(value));
2061
+
2062
+ /// <summary>A multipart body.</summary>
2063
+ public HttpContent MultipartContent(IEnumerable<SdkPart> parts)
2064
+ {
2065
+ var content = new MultipartFormDataContent();
2066
+ foreach (var part in parts)
2067
+ {
2068
+ if (part.FileName is null) content.Add(part.Content, part.Name);
2069
+ else content.Add(part.Content, part.Name, part.FileName);
2070
+ }
2071
+
2072
+ return content;
2073
+ }
2074
+
2075
+ /// <summary>A text body with an explicit mime.</summary>
2076
+ public HttpContent TextContent(string value, string mediaType) => new StringContent(value, Encoding.UTF8, mediaType);
2077
+
2078
+ /// <summary>A binary body with an explicit mime.</summary>
2079
+ public HttpContent BinaryContent(byte[] value, string mediaType)
2080
+ {
2081
+ var content = new ByteArrayContent(value);
2082
+ content.Headers.ContentType = new MediaTypeHeaderValue(mediaType);
2083
+ return content;
2084
+ }
2085
+
2086
+ /// <summary>The text a scalar JSON value travels as outside a body, or null when it is absent.</summary>
2087
+ public static string? ScalarText(JsonElement element) =>
2088
+ element.ValueKind switch
2089
+ {
2090
+ JsonValueKind.Null or JsonValueKind.Undefined => null,
2091
+ JsonValueKind.String => element.GetString(),
2092
+ JsonValueKind.True => "true",
2093
+ JsonValueKind.False => "false",
2094
+ _ => element.GetRawText(),
2095
+ };
2096
+
2097
+ public void Dispose()
2098
+ {
2099
+ if (_ownsClient) Client.Dispose();
2100
+ }
2101
+
2102
+ private string BuildUrl(string path, IEnumerable<KeyValuePair<string, string>>? query)
2103
+ {
2104
+ var builder = new StringBuilder(_options.BaseUrl.TrimEnd('/')).Append(path);
2105
+ if (query is null) return builder.ToString();
2106
+
2107
+ var first = true;
2108
+ foreach (var entry in query)
2109
+ {
2110
+ builder.Append(first ? '?' : '&');
2111
+ first = false;
2112
+ builder.Append(Uri.EscapeDataString(entry.Key)).Append('=').Append(Uri.EscapeDataString(entry.Value));
2113
+ }
2114
+
2115
+ return builder.ToString();
2116
+ }
2117
+ }
2118
+ `;
2119
+ }
2120
+ __name(generateRuntimeCs, "generateRuntimeCs");
2121
+
2122
+ // src/runtime-converters.ts
2123
+ function generateConvertersCs(namespaceName) {
2124
+ return `// <auto-generated/>
2125
+ // Generated by @contractkit/plugin-csharp. Do not edit manually.
2126
+ #nullable enable
2127
+
2128
+ using System;
2129
+ using System.Buffers;
2130
+ using System.Globalization;
2131
+ using System.Numerics;
2132
+ using System.Text;
2133
+ using System.Text.Json;
2134
+ using System.Text.Json.Serialization;
2135
+ using System.Xml;
2136
+
2137
+ namespace ${namespaceName}.Runtime;
2138
+
2139
+ /// <summary>
2140
+ /// How the SDK reads and writes JSON.
2141
+ /// </summary>
2142
+ /// <remarks>
2143
+ /// Unknown properties are skipped, which is the default, so a service adding a field does not break
2144
+ /// an older client. Serialize a generated model with these options: three of the contract's scalar
2145
+ /// types need the converters registered here.
2146
+ /// </remarks>
2147
+ public static class SdkJson
2148
+ {
2149
+ public static readonly JsonSerializerOptions Options = CreateOptions();
2150
+
2151
+ private static JsonSerializerOptions CreateOptions()
2152
+ {
2153
+ var options = new JsonSerializerOptions();
2154
+ options.Converters.Add(new BigIntegerConverter());
2155
+ options.Converters.Add(new DecimalStringConverter());
2156
+ options.Converters.Add(new IsoTimeSpanConverter());
2157
+ return options;
2158
+ }
2159
+ }
2160
+
2161
+ /// <summary>
2162
+ /// An arbitrary-precision integer. Written as a plain digit string.
2163
+ /// </summary>
2164
+ /// <remarks>
2165
+ /// Reading accepts a digit string, the <c>123n</c> form the TypeScript SDK writes, and a JSON
2166
+ /// number, so a body written by any ContractKit client reads back here.
2167
+ /// </remarks>
2168
+ public sealed class BigIntegerConverter : JsonConverter<BigInteger>
2169
+ {
2170
+ public override BigInteger Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
2171
+ {
2172
+ if (reader.TokenType == JsonTokenType.String)
2173
+ {
2174
+ var text = reader.GetString() ?? throw new JsonException("Expected a digit string for a bigint.");
2175
+ return BigInteger.Parse(text.TrimEnd('n'), NumberStyles.Integer, CultureInfo.InvariantCulture);
2176
+ }
2177
+
2178
+ if (reader.TokenType == JsonTokenType.Number)
2179
+ {
2180
+ // Read the raw token rather than a long: the value may be wider than any BCL integer,
2181
+ // which is the whole reason the contract called it a bigint.
2182
+ var raw = Encoding.UTF8.GetString(reader.HasValueSequence ? reader.ValueSequence.ToArray() : reader.ValueSpan);
2183
+ return BigInteger.Parse(raw, NumberStyles.Integer, CultureInfo.InvariantCulture);
2184
+ }
2185
+
2186
+ throw new JsonException($"Expected a JSON string or number for a bigint, got {reader.TokenType}.");
2187
+ }
2188
+
2189
+ public override void Write(Utf8JsonWriter writer, BigInteger value, JsonSerializerOptions options)
2190
+ {
2191
+ writer.WriteStringValue(value.ToString(CultureInfo.InvariantCulture));
2192
+ }
2193
+ }
2194
+
2195
+ /// <summary>
2196
+ /// An exact decimal number. Travels as a quoted JSON string, never as a JSON number.
2197
+ /// </summary>
2198
+ /// <remarks>
2199
+ /// A JSON number has already been through a double by the time it reaches this converter, so the
2200
+ /// precision the contract asked for is gone. Reading an unquoted number is rejected for that
2201
+ /// reason, which matches the Kotlin SDK and the server's own schema.
2202
+ /// </remarks>
2203
+ public sealed class DecimalStringConverter : JsonConverter<decimal>
2204
+ {
2205
+ public override decimal Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
2206
+ {
2207
+ if (reader.TokenType != JsonTokenType.String)
2208
+ {
2209
+ throw new JsonException($"Expected a quoted decimal string, got {reader.TokenType}.");
2210
+ }
2211
+
2212
+ var text = reader.GetString() ?? throw new JsonException("Expected a decimal string.");
2213
+ return decimal.Parse(text, NumberStyles.Float, CultureInfo.InvariantCulture);
2214
+ }
2215
+
2216
+ public override void Write(Utf8JsonWriter writer, decimal value, JsonSerializerOptions options)
2217
+ {
2218
+ writer.WriteStringValue(value.ToString(CultureInfo.InvariantCulture));
2219
+ }
2220
+ }
2221
+
2222
+ /// <summary>
2223
+ /// A duration, as an ISO 8601 string such as <c>PT1H30M</c>.
2224
+ /// </summary>
2225
+ /// <remarks>
2226
+ /// System.Text.Json writes a <c>TimeSpan</c> as <c>d.hh:mm:ss</c> by default, which no other
2227
+ /// ContractKit SDK would read, so this converter is not optional.
2228
+ /// </remarks>
2229
+ public sealed class IsoTimeSpanConverter : JsonConverter<TimeSpan>
2230
+ {
2231
+ public override TimeSpan Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
2232
+ {
2233
+ if (reader.TokenType != JsonTokenType.String)
2234
+ {
2235
+ throw new JsonException($"Expected an ISO 8601 duration string, got {reader.TokenType}.");
2236
+ }
2237
+
2238
+ var text = reader.GetString() ?? throw new JsonException("Expected an ISO 8601 duration string.");
2239
+ try
2240
+ {
2241
+ return XmlConvert.ToTimeSpan(text);
2242
+ }
2243
+ catch (FormatException error)
2244
+ {
2245
+ throw new JsonException($"'{text}' is not an ISO 8601 duration.", error);
2246
+ }
2247
+ }
2248
+
2249
+ public override void Write(Utf8JsonWriter writer, TimeSpan value, JsonSerializerOptions options)
2250
+ {
2251
+ writer.WriteStringValue(XmlConvert.ToString(value));
2252
+ }
2253
+ }
2254
+ `;
2255
+ }
2256
+ __name(generateConvertersCs, "generateConvertersCs");
2257
+
2258
+ // src/scaffold.ts
2259
+ var SCAFFOLD_VERSIONS = {
2260
+ targetFramework: "net10.0"
2261
+ };
2262
+ function generateCsproj(namespaceName, sdkName) {
2263
+ return `<!-- Created once by @contractkit/plugin-csharp. Yours to edit: it is never regenerated. -->
2264
+ <Project Sdk="Microsoft.NET.Sdk">
2265
+
2266
+ <PropertyGroup>
2267
+ <TargetFramework>${SCAFFOLD_VERSIONS.targetFramework}</TargetFramework>
2268
+ <Nullable>enable</Nullable>
2269
+ <ImplicitUsings>disable</ImplicitUsings>
2270
+ <RootNamespace>${namespaceName}</RootNamespace>
2271
+ <AssemblyName>${sdkName}</AssemblyName>
2272
+ </PropertyGroup>
2273
+
2274
+ </Project>
2275
+ `;
2276
+ }
2277
+ __name(generateCsproj, "generateCsproj");
2278
+
2279
+ // src/index.ts
2280
+ var CSHARP_CODEGEN_VERSION = "1";
2281
+ var CACHE_MANIFEST_FILENAME = "csharp-manifest.json";
2282
+ var DEFAULT_BASE_DIR = "csharp-sdk";
2283
+ var DEFAULT_NAMESPACE = "ContractKit.Sdk";
2284
+ var DEFAULT_SDK_NAME = "Sdk";
2285
+ var plugin = {
2286
+ name: "csharp-sdk",
2287
+ async generateTargets(inputs, ctx) {
2288
+ const config = ctx.options;
2289
+ await runCSharpCodegen(inputs, ctx, config, ctx.rootDir);
2290
+ }
2291
+ };
2292
+ var index_default = plugin;
2293
+ function createCSharpSdkPlugin(config, rootDir) {
2294
+ return {
2295
+ name: "csharp-sdk",
2296
+ async generateTargets(inputs, ctx) {
2297
+ await runCSharpCodegen(inputs, ctx, config, rootDir);
2298
+ }
2299
+ };
2300
+ }
2301
+ __name(createCSharpSdkPlugin, "createCSharpSdkPlugin");
2302
+ var NAMESPACE_RE = /^[A-Za-z_][A-Za-z0-9_]*(\.[A-Za-z_][A-Za-z0-9_]*)*$/;
2303
+ var SDK_NAME_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
2304
+ function assertValidConfig(config) {
2305
+ const { namespace, sdkName } = config;
2306
+ if (namespace !== void 0) {
2307
+ if (typeof namespace !== "string" || !NAMESPACE_RE.test(namespace)) {
2308
+ throw new Error(`plugin-csharp: namespace '${String(namespace)}' is not a valid C# namespace \u2014 expected dot-separated identifiers, e.g. 'Acme.Sdk'.`);
2309
+ }
2310
+ const keyword = namespace.split(".").find((segment) => CSHARP_KEYWORDS.has(segment));
2311
+ if (keyword) {
2312
+ throw new Error(`plugin-csharp: namespace '${namespace}' contains the C# keyword '${keyword}', which cannot appear in a namespace.`);
2313
+ }
2314
+ }
2315
+ if (sdkName !== void 0) {
2316
+ if (typeof sdkName !== "string" || !SDK_NAME_RE.test(sdkName)) {
2317
+ throw new Error(`plugin-csharp: sdkName '${String(sdkName)}' is not a valid C# class name.`);
2318
+ }
2319
+ if (CSHARP_KEYWORDS.has(sdkName)) {
2320
+ throw new Error(`plugin-csharp: sdkName '${sdkName}' is a C# keyword.`);
2321
+ }
2322
+ }
2323
+ for (const key of [
2324
+ "includeInternal",
2325
+ "scaffold"
2326
+ ]) {
2327
+ const value = config[key];
2328
+ if (value !== void 0 && typeof value !== "boolean") {
2329
+ throw new Error(`plugin-csharp: ${key} must be a boolean \u2014 got ${JSON.stringify(value)}.`);
2330
+ }
2331
+ }
2332
+ }
2333
+ __name(assertValidConfig, "assertValidConfig");
2334
+ async function runCSharpCodegen(inputs, ctx, config, rootDir) {
2335
+ assertValidConfig(config);
2336
+ const { contractRoots } = inputs;
2337
+ const namespaceName = config.namespace ?? DEFAULT_NAMESPACE;
2338
+ const sdkName = config.sdkName ?? DEFAULT_SDK_NAME;
2339
+ const outDir = resolve(rootDir, config.baseDir ?? DEFAULT_BASE_DIR);
2340
+ const manifestPath = resolve(ctx.cacheDir, CACHE_MANIFEST_FILENAME);
2341
+ const allModels = contractRoots.flatMap((root) => root.models);
2342
+ const modelIndex = buildModelIndex2(allModels);
2343
+ const modelsWithInput = resolveModelsWithInput(allModels, inputs.modelsWithInput);
2344
+ const modelsWithInputArray = [
2345
+ ...modelsWithInput
2346
+ ].sort();
2347
+ const hoisted = collectHoistedTypes(contractRoots, {
2348
+ modelIndex,
2349
+ modelsWithInput,
2350
+ warn: /* @__PURE__ */ __name((message, file) => ctx.warn?.(message, file), "warn")
2351
+ });
2352
+ const prevManifest = ctx.cacheEnabled ? readManifest(manifestPath) : emptyIncrementalManifest(CSHARP_CODEGEN_VERSION);
2353
+ const units = [];
2354
+ const clients = [];
2355
+ for (const root of contractRoots) {
2356
+ const relPath = `Models/${deriveCSharpFileBase(root.file)}.cs`;
2357
+ const ownNames = new Set(root.models.map((m) => m.name));
2358
+ const referenced = referencedModelNames(root);
2359
+ const relevantInputModels = modelsWithInputArray.filter((name) => ownNames.has(name) || referenced.has(name));
2360
+ const externalBases = [
2361
+ ...referenced
2362
+ ].filter((name) => !ownNames.has(name)).sort().map((name) => modelIndex.get(name)).filter((m) => m !== void 0);
2363
+ const ownedDeclarations = (hoisted.byFile.get(root.file) ?? []).map((d) => ({
2364
+ kind: d.kind,
2365
+ name: d.name,
2366
+ needsInput: d.needsInput
2367
+ }));
2368
+ const declaredMemberships = [
2369
+ ...ownNames
2370
+ ].sort().map((name) => [
2371
+ name,
2372
+ hoisted.memberships.get(name) ?? []
2373
+ ]).filter(([, unions]) => unions.length > 0);
2374
+ const fingerprint = hashFingerprint({
2375
+ kind: "models",
2376
+ v: CSHARP_CODEGEN_VERSION,
2377
+ relPath,
2378
+ namespace: namespaceName,
2379
+ root,
2380
+ externalBases,
2381
+ modelsWithInput: relevantInputModels,
2382
+ ownedDeclarations,
2383
+ declaredMemberships
2384
+ });
2385
+ units.push({
2386
+ key: `models::${relPath}`,
2387
+ fingerprint,
2388
+ render: /* @__PURE__ */ __name(() => [
2389
+ {
2390
+ relativePath: relPath,
2391
+ content: generateCSharpModels(root, {
2392
+ namespace: namespaceName,
2393
+ modelsWithInput,
2394
+ modelIndex,
2395
+ hoisted,
2396
+ warn: /* @__PURE__ */ __name((message) => ctx.warn?.(message, root.file), "warn")
2397
+ })
2398
+ }
2399
+ ], "render")
2400
+ });
2401
+ }
2402
+ for (const root of inputs.opRoots) {
2403
+ if (!hasPublicOperations(root, config.includeInternal)) continue;
2404
+ const relPath = `Clients/${deriveClientClassName(root.file)}.cs`;
2405
+ clients.push({
2406
+ className: deriveClientClassName(root.file),
2407
+ propertyName: deriveClientPropertyName(root.file)
2408
+ });
2409
+ const referenced = referencedOpModels(root, modelIndex);
2410
+ const relevantInputModels = modelsWithInputArray.filter((name) => referenced.has(name));
2411
+ const referencedModels = [
2412
+ ...referenced
2413
+ ].sort().map((name) => modelIndex.get(name)).filter((m) => m !== void 0);
2414
+ const fingerprint = hashFingerprint({
2415
+ kind: "client",
2416
+ v: CSHARP_CODEGEN_VERSION,
2417
+ relPath,
2418
+ namespace: namespaceName,
2419
+ root,
2420
+ referencedModels,
2421
+ modelsWithInput: relevantInputModels,
2422
+ includeInternal: config.includeInternal ?? false
2423
+ });
2424
+ units.push({
2425
+ key: `client::${relPath}`,
2426
+ fingerprint,
2427
+ render: /* @__PURE__ */ __name(() => [
2428
+ {
2429
+ relativePath: relPath,
2430
+ content: generateCSharpClient(root, {
2431
+ namespace: namespaceName,
2432
+ modelsWithInput,
2433
+ modelIndex,
2434
+ hoisted,
2435
+ includeInternal: config.includeInternal,
2436
+ warn: /* @__PURE__ */ __name((message) => ctx.warn?.(message, root.file), "warn")
2437
+ })
2438
+ }
2439
+ ], "render")
2440
+ });
2441
+ }
2442
+ const globalFiles = [
2443
+ {
2444
+ relativePath: "Runtime/Converters.cs",
2445
+ content: generateConvertersCs(namespaceName)
2446
+ },
2447
+ {
2448
+ relativePath: "Runtime/SdkRuntime.cs",
2449
+ content: generateRuntimeCs(namespaceName)
2450
+ },
2451
+ {
2452
+ relativePath: `${sdkName}.cs`,
2453
+ content: generateSdkCs(namespaceName, sdkName, clients)
2454
+ }
2455
+ ];
2456
+ if (config.scaffold) {
2457
+ globalFiles.push({
2458
+ relativePath: `${sdkName}.csproj`,
2459
+ content: generateCsproj(namespaceName, sdkName),
2460
+ ifAbsent: true
2461
+ });
2462
+ }
2463
+ const result = runIncrementalCodegen({
2464
+ codegenVersion: CSHARP_CODEGEN_VERSION,
2465
+ prevManifest,
2466
+ globalFiles,
2467
+ units,
2468
+ fileExists: /* @__PURE__ */ __name((relPath) => existsSync(resolve(outDir, relPath)), "fileExists")
2469
+ });
2470
+ deleteStalePaths(outDir, result.deletedPaths);
2471
+ for (const { relativePath, content, ifAbsent } of result.filesToWrite) {
2472
+ ctx.emitFile(resolve(outDir, relativePath), content, ifAbsent ? {
2473
+ ifAbsent: true
2474
+ } : void 0);
2475
+ }
2476
+ writeManifest(manifestPath, result.manifest);
2477
+ }
2478
+ __name(runCSharpCodegen, "runCSharpCodegen");
2479
+ function referencedOpModels(root, modelIndex) {
2480
+ const seeds = [];
2481
+ const addParamSource = /* @__PURE__ */ __name((source) => {
2482
+ if (!source) return;
2483
+ if (source.kind === "params") seeds.push(...source.nodes.map((n) => n.type));
2484
+ else if (source.kind === "ref") seeds.push({
2485
+ kind: "ref",
2486
+ name: source.name
2487
+ });
2488
+ else seeds.push(source.node);
2489
+ }, "addParamSource");
2490
+ for (const route of root.routes) {
2491
+ addParamSource(route.params);
2492
+ for (const op of route.operations) {
2493
+ addParamSource(op.query);
2494
+ addParamSource(op.headers);
2495
+ for (const body of op.request?.bodies ?? []) seeds.push(body.bodyType);
2496
+ for (const response of op.responses) {
2497
+ for (const body of response.bodies) seeds.push(body.bodyType);
2498
+ for (const header of response.headers ?? []) seeds.push(header.type);
2499
+ }
2500
+ }
2501
+ }
2502
+ return collectTransitiveModelRefs(seeds, modelIndex);
2503
+ }
2504
+ __name(referencedOpModels, "referencedOpModels");
2505
+ function referencedModelNames(root) {
2506
+ const refs = /* @__PURE__ */ new Set();
2507
+ for (const model of root.models) {
2508
+ if (model.type) collectTypeRefs2(model.type, refs);
2509
+ for (const f of model.fields) collectTypeRefs2(f.type, refs);
2510
+ if (model.bases) for (const base of model.bases) refs.add(base);
2511
+ }
2512
+ return refs;
2513
+ }
2514
+ __name(referencedModelNames, "referencedModelNames");
2515
+ function readManifest(manifestPath) {
2516
+ if (!existsSync(manifestPath)) return emptyIncrementalManifest(CSHARP_CODEGEN_VERSION);
2517
+ try {
2518
+ return parseIncrementalManifest(readFileSync(manifestPath, "utf-8"));
2519
+ } catch {
2520
+ return emptyIncrementalManifest(CSHARP_CODEGEN_VERSION);
2521
+ }
2522
+ }
2523
+ __name(readManifest, "readManifest");
2524
+ function writeManifest(manifestPath, manifest) {
2525
+ try {
2526
+ mkdirSync(dirname(manifestPath), {
2527
+ recursive: true
2528
+ });
2529
+ writeFileSync(manifestPath, serializeIncrementalManifest(manifest), "utf-8");
2530
+ } catch {
2531
+ }
2532
+ }
2533
+ __name(writeManifest, "writeManifest");
2534
+ function deleteStalePaths(outDir, relPaths) {
2535
+ if (relPaths.length === 0) return;
2536
+ const removedDirs = /* @__PURE__ */ new Set();
2537
+ for (const rel of relPaths) {
2538
+ const abs = resolve(outDir, rel);
2539
+ if (existsSync(abs)) {
2540
+ rmSync(abs, {
2541
+ force: true
2542
+ });
2543
+ removedDirs.add(join(abs, ".."));
2544
+ }
2545
+ }
2546
+ for (const dir of removedDirs) {
2547
+ let current = dir;
2548
+ while (current.startsWith(outDir) && current !== outDir) {
2549
+ try {
2550
+ if (readdirSync(current).length === 0) {
2551
+ rmdirSync(current);
2552
+ current = join(current, "..");
2553
+ } else {
2554
+ break;
2555
+ }
2556
+ } catch {
2557
+ break;
2558
+ }
2559
+ }
2560
+ }
2561
+ }
2562
+ __name(deleteStalePaths, "deleteStalePaths");
2563
+ export {
2564
+ CSHARP_CODEGEN_VERSION,
2565
+ assertValidConfig,
2566
+ createCSharpSdkPlugin,
2567
+ index_default as default
2568
+ };
2569
+ //# sourceMappingURL=index.js.map