@voxgig/sdkgen 3.4.7 → 3.5.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 (194) hide show
  1. package/bin/voxgig-sdkgen +78 -10
  2. package/dist/action/action.d.ts +5 -2
  3. package/dist/action/action.js +113 -6
  4. package/dist/action/action.js.map +1 -1
  5. package/dist/action/check.d.ts +15 -0
  6. package/dist/action/check.js +347 -0
  7. package/dist/action/check.js.map +1 -0
  8. package/dist/action/dispatch.d.ts +8 -0
  9. package/dist/action/dispatch.js +74 -0
  10. package/dist/action/dispatch.js.map +1 -0
  11. package/dist/action/docs.d.ts +4 -0
  12. package/dist/action/docs.js +236 -0
  13. package/dist/action/docs.js.map +1 -0
  14. package/dist/action/doctor.d.ts +5 -2
  15. package/dist/action/doctor.js +526 -74
  16. package/dist/action/doctor.js.map +1 -1
  17. package/dist/action/feature.js +96 -17
  18. package/dist/action/feature.js.map +1 -1
  19. package/dist/action/kind.d.ts +30 -0
  20. package/dist/action/kind.js +258 -0
  21. package/dist/action/kind.js.map +1 -0
  22. package/dist/action/package.d.ts +25 -0
  23. package/dist/action/package.js +852 -0
  24. package/dist/action/package.js.map +1 -0
  25. package/dist/action/resolve.d.ts +18 -0
  26. package/dist/action/resolve.js +280 -0
  27. package/dist/action/resolve.js.map +1 -0
  28. package/dist/action/target.d.ts +7 -1
  29. package/dist/action/target.js +240 -97
  30. package/dist/action/target.js.map +1 -1
  31. package/dist/cmp/Docs.d.ts +4 -0
  32. package/dist/cmp/Docs.js +98 -0
  33. package/dist/cmp/Docs.js.map +1 -0
  34. package/dist/cmp/ExternalDocs.d.ts +2 -0
  35. package/dist/cmp/ExternalDocs.js +43 -0
  36. package/dist/cmp/ExternalDocs.js.map +1 -0
  37. package/dist/helpers/definition.d.ts +5 -0
  38. package/dist/helpers/definition.js +72 -0
  39. package/dist/helpers/definition.js.map +1 -0
  40. package/dist/helpers/featureSource.d.ts +8 -2
  41. package/dist/helpers/featureSource.js +61 -12
  42. package/dist/helpers/featureSource.js.map +1 -1
  43. package/dist/helpers/junk.d.ts +11 -0
  44. package/dist/helpers/junk.js +149 -0
  45. package/dist/helpers/junk.js.map +1 -0
  46. package/dist/helpers/manifest.d.ts +50 -0
  47. package/dist/helpers/manifest.js +328 -0
  48. package/dist/helpers/manifest.js.map +1 -0
  49. package/dist/helpers/modelcheck.d.ts +22 -0
  50. package/dist/helpers/modelcheck.js +203 -0
  51. package/dist/helpers/modelcheck.js.map +1 -0
  52. package/dist/helpers/semver.d.ts +4 -0
  53. package/dist/helpers/semver.js +174 -0
  54. package/dist/helpers/semver.js.map +1 -0
  55. package/dist/helpers/shipped.d.ts +4 -0
  56. package/dist/helpers/shipped.js +36 -0
  57. package/dist/helpers/shipped.js.map +1 -0
  58. package/dist/helpers/stdrep.d.ts +9 -1
  59. package/dist/helpers/stdrep.js +22 -0
  60. package/dist/helpers/stdrep.js.map +1 -1
  61. package/dist/sdkgen.d.ts +15 -3
  62. package/dist/sdkgen.js +233 -67
  63. package/dist/sdkgen.js.map +1 -1
  64. package/dist/tsconfig.tsbuildinfo +1 -1
  65. package/dist/types.d.ts +10 -2
  66. package/dist/utility.js +17 -0
  67. package/dist/utility.js.map +1 -1
  68. package/model/sdkgen.aontu +69 -0
  69. package/package.json +3 -2
  70. package/project/.sdk/model/feature/audit.aontu +1 -0
  71. package/project/.sdk/model/feature/cache.aontu +1 -0
  72. package/project/.sdk/model/feature/clienttrack.aontu +1 -0
  73. package/project/.sdk/model/feature/debug.aontu +1 -0
  74. package/project/.sdk/model/feature/idempotency.aontu +1 -0
  75. package/project/.sdk/model/feature/log.aontu +1 -0
  76. package/project/.sdk/model/feature/metrics.aontu +1 -0
  77. package/project/.sdk/model/feature/netsim.aontu +1 -0
  78. package/project/.sdk/model/feature/paging.aontu +1 -0
  79. package/project/.sdk/model/feature/proxy.aontu +1 -0
  80. package/project/.sdk/model/feature/ratelimit.aontu +1 -0
  81. package/project/.sdk/model/feature/rbac.aontu +1 -0
  82. package/project/.sdk/model/feature/retry.aontu +1 -0
  83. package/project/.sdk/model/feature/streaming.aontu +1 -0
  84. package/project/.sdk/model/feature/telemetry.aontu +1 -0
  85. package/project/.sdk/model/feature/test.aontu +1 -0
  86. package/project/.sdk/model/feature/timeout.aontu +1 -0
  87. package/project/.sdk/model/target/c.aontu +1 -0
  88. package/project/.sdk/model/target/clojure.aontu +1 -0
  89. package/project/.sdk/model/target/cpp.aontu +1 -0
  90. package/project/.sdk/model/target/dart.aontu +1 -0
  91. package/project/.sdk/model/target/elixir.aontu +1 -0
  92. package/project/.sdk/model/target/go-cli.aontu +1 -0
  93. package/project/.sdk/model/target/go-mcp.aontu +1 -0
  94. package/project/.sdk/model/target/go.aontu +1 -0
  95. package/project/.sdk/model/target/haskell.aontu +1 -0
  96. package/project/.sdk/model/target/java.aontu +1 -0
  97. package/project/.sdk/model/target/js.aontu +1 -0
  98. package/project/.sdk/model/target/kotlin.aontu +1 -0
  99. package/project/.sdk/model/target/lean.aontu +1 -0
  100. package/project/.sdk/model/target/lua.aontu +1 -0
  101. package/project/.sdk/model/target/ocaml.aontu +1 -0
  102. package/project/.sdk/model/target/perl.aontu +1 -0
  103. package/project/.sdk/model/target/php.aontu +1 -0
  104. package/project/.sdk/model/target/py-data.aontu +1 -0
  105. package/project/.sdk/model/target/py.aontu +1 -0
  106. package/project/.sdk/model/target/rb.aontu +1 -0
  107. package/project/.sdk/model/target/rust.aontu +1 -0
  108. package/project/.sdk/model/target/scala.aontu +1 -0
  109. package/project/.sdk/model/target/seneca-provider.aontu +1 -0
  110. package/project/.sdk/model/target/zig.aontu +1 -0
  111. package/project/.sdk/src/cmp/c/Config_c.ts +44 -0
  112. package/project/.sdk/src/cmp/c/fragment/Main.fragment.c +3 -1
  113. package/project/.sdk/src/cmp/clojure/Config_clojure.ts +37 -42
  114. package/project/.sdk/src/cmp/clojure/utility_clojure.ts +42 -0
  115. package/project/.sdk/src/cmp/cpp/Config_cpp.ts +18 -0
  116. package/project/.sdk/src/cmp/csharp/Config_csharp.ts +125 -52
  117. package/project/.sdk/src/cmp/csharp/fragment/Main.fragment.cs +3 -1
  118. package/project/.sdk/src/cmp/csharp/utility_csharp.ts +94 -6
  119. package/project/.sdk/src/cmp/dart/Config_dart.ts +43 -5
  120. package/project/.sdk/src/cmp/dart/fragment/Config.data.fragment.dart +74 -0
  121. package/project/.sdk/src/cmp/dart/fragment/Config.fragment.dart +6 -9
  122. package/project/.sdk/src/cmp/dart/fragment/Main.fragment.dart +16 -5
  123. package/project/.sdk/src/cmp/dart/utility_dart.ts +18 -0
  124. package/project/.sdk/src/cmp/elixir/Config_elixir.ts +128 -13
  125. package/project/.sdk/src/cmp/elixir/fragment/Main.fragment.ex +3 -1
  126. package/project/.sdk/src/cmp/elixir/utility_elixir.ts +6 -0
  127. package/project/.sdk/src/cmp/haskell/Config_haskell.ts +35 -52
  128. package/project/.sdk/src/cmp/haskell/Package_haskell.ts +1 -0
  129. package/project/.sdk/src/cmp/haskell/Test_haskell.ts +1 -1
  130. package/project/.sdk/src/cmp/haskell/utility_haskell.ts +15 -5
  131. package/project/.sdk/src/cmp/java/Config_java.ts +21 -0
  132. package/project/.sdk/src/cmp/kotlin/Config_kotlin.ts +15 -0
  133. package/project/.sdk/src/cmp/ocaml/Config_ocaml.ts +42 -39
  134. package/project/.sdk/src/cmp/perl/Config_perl.ts +15 -0
  135. package/project/.sdk/src/cmp/perl/fragment/Main.fragment.pm +3 -1
  136. package/project/.sdk/src/cmp/rust/Config_rust.ts +44 -0
  137. package/project/.sdk/src/cmp/rust/fragment/Main.fragment.rs +4 -2
  138. package/project/.sdk/src/cmp/scala/Config_scala.ts +15 -0
  139. package/project/.sdk/src/cmp/swift/Config_swift.ts +17 -0
  140. package/project/.sdk/src/cmp/swift/fragment/Main.fragment.swift +3 -1
  141. package/project/.sdk/src/cmp/zig/Config_zig.ts +44 -0
  142. package/project/.sdk/src/cmp/zig/TestEntity_zig.ts +9 -2
  143. package/project/.sdk/src/cmp/zig/fragment/Main.fragment.zig +3 -1
  144. package/project/.sdk/tm/c/core/sdk.h +1 -0
  145. package/project/.sdk/tm/c/utility/make_options.c +5 -1
  146. package/project/.sdk/tm/clojure/src/sdk/core.clj +51 -3
  147. package/project/.sdk/tm/cpp/core/types.hpp +4 -1
  148. package/project/.sdk/tm/cpp/utility/pipeline.hpp +5 -1
  149. package/project/.sdk/tm/csharp/test/PipelineTest.cs +15 -0
  150. package/project/.sdk/tm/csharp/utility/MakeOptions.cs +67 -1
  151. package/project/.sdk/tm/dart/lib/utility/MakeOptionsUtility.dart +43 -0
  152. package/project/.sdk/tm/elixir/lib/projectname/utility.ex +58 -2
  153. package/project/.sdk/tm/haskell/{test/TestJson.hs → src/SdkJson.hs} +8 -4
  154. package/project/.sdk/tm/haskell/src/SdkRuntime.hs +55 -2
  155. package/project/.sdk/tm/haskell/test/Runner.hs +1 -1
  156. package/project/.sdk/tm/haskell/test/StructCorpus.hs +1 -1
  157. package/project/.sdk/tm/java/core/SdkClient.java +3 -1
  158. package/project/.sdk/tm/java/utility/MakeOptions.java +5 -1
  159. package/project/.sdk/tm/kotlin/core/SdkClient.kt +3 -1
  160. package/project/.sdk/tm/kotlin/utility/MakeOptions.kt +5 -1
  161. package/project/.sdk/tm/ocaml/Makefile +3 -2
  162. package/project/.sdk/tm/ocaml/sdk_json.ml +106 -0
  163. package/project/.sdk/tm/ocaml/sdk_runtime.ml +56 -0
  164. package/project/.sdk/tm/ocaml/test/struct_corpus.ml +4 -86
  165. package/project/.sdk/tm/perl/utility/make_options.pm +5 -1
  166. package/project/.sdk/tm/rust/utility/make_options.rs +79 -1
  167. package/project/.sdk/tm/scala/core/SdkClient.scala +3 -1
  168. package/project/.sdk/tm/scala/utility/Make.scala +5 -1
  169. package/project/.sdk/tm/swift/Sources/ProjectNameSDK/utility/MakeOptions.swift +5 -1
  170. package/project/.sdk/tm/zig/core/utility.zig +85 -1
  171. package/project/sdkgen-package.json +57 -0
  172. package/src/action/action.ts +136 -6
  173. package/src/action/check.ts +495 -0
  174. package/src/action/dispatch.ts +105 -0
  175. package/src/action/docs.ts +310 -0
  176. package/src/action/doctor.ts +692 -81
  177. package/src/action/feature.ts +109 -17
  178. package/src/action/kind.ts +375 -0
  179. package/src/action/package.ts +1173 -0
  180. package/src/action/resolve.ts +362 -0
  181. package/src/action/target.ts +290 -108
  182. package/src/cmp/Docs.ts +125 -0
  183. package/src/cmp/ExternalDocs.ts +54 -0
  184. package/src/helpers/definition.ts +82 -0
  185. package/src/helpers/featureSource.ts +83 -13
  186. package/src/helpers/junk.ts +168 -0
  187. package/src/helpers/manifest.ts +474 -0
  188. package/src/helpers/modelcheck.ts +262 -0
  189. package/src/helpers/semver.ts +211 -0
  190. package/src/helpers/shipped.ts +41 -0
  191. package/src/helpers/stdrep.ts +70 -0
  192. package/src/sdkgen.ts +294 -66
  193. package/src/types.ts +33 -3
  194. package/src/utility.ts +17 -0
@@ -3,8 +3,11 @@ import {
3
3
  Content,
4
4
  File,
5
5
  cmp,
6
+ configDefinition,
7
+ configReprSetting,
6
8
  each,
7
9
  isAuthActive,
10
+ isConfigData,
8
11
  resolveAuthPrefix,
9
12
  } from '@voxgig/sdkgen'
10
13
 
@@ -17,7 +20,7 @@ import {
17
20
 
18
21
 
19
22
  import {
20
- clean,
23
+ cljStringChunks,
21
24
  formatCljValue,
22
25
  } from './utility_clojure'
23
26
 
@@ -32,52 +35,44 @@ const Config = cmp(async function Config(props: any) {
32
35
 
33
36
  const model: Model = ctx$.model
34
37
 
35
- const entity = getModelPath(model, `main.${KIT}.entity`)
36
- const feature = getModelPath(model, `main.${KIT}.feature`)
38
+ // The canonical config OBJECT and its JSON, from the shared helper. Both
39
+ // representations render from the same `def`, so they cannot describe
40
+ // different configs - and this target picks up `options.server` (the OpenAPI
41
+ // server-variable defaults), which the hand-rolled build here omitted.
42
+ const { def: config, json: configJson } = configDefinition(model)
43
+ const asData = isConfigData(configJson, configReprSetting(model))
37
44
 
38
- const headers = getModelPath(model, `main.${KIT}.config.headers`) || {}
45
+ File({ name: 'config.clj' }, () => {
39
46
 
40
- const authActive = isAuthActive(model)
41
- const authPrefix = resolveAuthPrefix(model)
47
+ // ABOVE THE THRESHOLD: emit the model as DATA.
48
+ //
49
+ // The literal is one nested form the Clojure reader walks and the compiler
50
+ // turns into constructor calls at load; a string is one token, and
51
+ // `sdk.core/json-parse` builds the same LinkedHashMap/ArrayList nodes
52
+ // `vs/jm` and `vs/jt` produce. It is already the SDK's response decoder,
53
+ // so this adds no dependency.
54
+ //
55
+ // JSON.stringify output is a valid Clojure string literal: it escapes
56
+ // every backslash, so the JSON's own `\uXXXX` reaches the reader as
57
+ // `\\uXXXX` and no Clojure escape is ever formed from it.
58
+ if (asData) {
59
+ Content(`;; ${model.const.Name} SDK configuration (generated by @voxgig/sdkgen).
60
+ ;;
61
+ ;; THE API MODEL, EMBEDDED AS DATA (sdkgen rung L1). Emitted only above a size
62
+ ;; threshold, or when main.kit.config.repr pins it: for a small model the
63
+ ;; literal is smaller and far easier to read when debugging.
64
+ (ns sdk.config
65
+ (:require [sdk.core :as core]))
42
66
 
43
- let baseUrl = ''
44
- try { baseUrl = getModelPath(model, `main.${KIT}.info.servers.0.url`) } catch (_e) { }
67
+ (def ^:private config-data
68
+ (str ${cljStringChunks(configJson).map((c: string) => JSON.stringify(c)).join('\n ')}))
45
69
 
46
- const featureConfig: any = {}
47
- each(feature, (f: any) => {
48
- featureConfig[f.name] = f.config || {}
49
- })
50
-
51
- const entityOptions: any = {}
52
- each(entity, (ent: any) => {
53
- entityOptions[ent.name] = {}
54
- })
55
-
56
- const options: any = {
57
- base: baseUrl,
58
- headers,
59
- entity: entityOptions,
60
- }
61
- if (authActive) {
62
- options.auth = { prefix: authPrefix }
63
- }
64
-
65
- const entityConfig = Object.values(entity || {}).reduce((a: any, n: any) => (
66
- a[n.name] = clean({
67
- fields: n.fields,
68
- name: n.name,
69
- op: n.op,
70
- relations: n.relations,
71
- }, true), a), {})
72
-
73
- const config = {
74
- main: { name: model.const.Name },
75
- feature: featureConfig,
76
- options,
77
- entity: entityConfig,
78
- }
70
+ (defn make-config []
71
+ (core/json-parse config-data))
72
+ `)
73
+ return
74
+ }
79
75
 
80
- File({ name: 'config.clj' }, () => {
81
76
  Content(`;; ${model.const.Name} SDK configuration (generated by @voxgig/sdkgen).
82
77
  (ns sdk.config
83
78
  (:require [voxgig.struct :as vs]))
@@ -125,7 +125,49 @@ function clean(o: any, dropDefaults?: boolean): any {
125
125
  }
126
126
 
127
127
 
128
+
129
+ // The JSON as Clojure string literals, CHUNKED, joined at load.
130
+ //
131
+ // A Clojure string literal becomes a JVM constant-pool UTF-8 entry, and those
132
+ // are capped at 65,535 bytes. One constant therefore cannot hold a config
133
+ // large enough to select the data representation at all - the threshold is
134
+ // 256 KB, so every model that reaches this branch would exceed the limit.
135
+ //
136
+ // The failure is AOT-only and so is easy to miss: loading from source is fine,
137
+ // and `clojure -M:test-compile` only `require`s. It appears the moment anyone
138
+ // compiles the SDK, which is the normal path for distribution:
139
+ //
140
+ // Execution error (IllegalArgumentException)
141
+ // at clojure.asm.ByteVector/putUTF8 (ByteVector.java:245)
142
+ //
143
+ // java/kotlin/scala already chunk for exactly this reason (see Config_java's
144
+ // jsonAppendLines). Chunks are measured in UTF-8 BYTES, not characters, with
145
+ // a wide margin: modified UTF-8 encodes a supplementary character as six
146
+ // bytes, so a character count is not a byte count. Surrogate pairs are kept
147
+ // whole, or a cut would emit a lone surrogate.
148
+ function cljStringChunks(json: string, maxBytes: number = 20000): string[] {
149
+ const chunks: string[] = []
150
+ let start = 0
151
+ let bytes = 0
152
+ for (let i = 0; i < json.length; i++) {
153
+ const code = json.charCodeAt(i)
154
+ // Worst case per UTF-16 unit under modified UTF-8.
155
+ bytes += code < 0x80 ? 1 : code < 0x800 ? 2 : 3
156
+ const high = 0xd800 <= code && code <= 0xdbff
157
+ if (maxBytes <= bytes && !high) {
158
+ chunks.push(json.slice(start, i + 1))
159
+ start = i + 1
160
+ bytes = 0
161
+ }
162
+ }
163
+ if (start < json.length) {
164
+ chunks.push(json.slice(start))
165
+ }
166
+ return 0 === chunks.length ? [''] : chunks
167
+ }
168
+
128
169
  export {
170
+ cljStringChunks,
129
171
  clean,
130
172
  formatCljValue,
131
173
  cljString,
@@ -109,6 +109,24 @@ ${cppConfigLiterals(config)};
109
109
 
110
110
  inline Value makeConfig() { return vs::parse_json(config_json()); }
111
111
 
112
+ // SHARED CONFIG (sdkgen rung L2).
113
+ //
114
+ // The SDK reads the config on every request and never writes to it, so one
115
+ // instance is shared by every client rather than rebuilt per client - this is
116
+ // the difference between parsing the embedded JSON once and once per client.
117
+ //
118
+ // A function-local static in an inline function is one object across every
119
+ // translation unit, and its initialisation is thread-safe by the standard.
120
+ // Value holds shared_ptr nodes, so copying the returned Value shares the
121
+ // structure rather than duplicating it.
122
+ //
123
+ // The result is SHARED: treat it as read-only. Callers that need to mutate
124
+ // should use makeConfig, which always parses a fresh copy.
125
+ inline const Value& sharedConfig() {
126
+ static const Value shared = makeConfig();
127
+ return shared;
128
+ }
129
+
112
130
  inline FeaturePtr makeFeature(const std::string& name) {
113
131
  `)
114
132
 
@@ -3,9 +3,10 @@ import {
3
3
  Content,
4
4
  File,
5
5
  cmp,
6
+ configDefinition,
7
+ configReprSetting,
6
8
  each,
7
- isAuthActive,
8
- resolveAuthPrefix,
9
+ isConfigData,
9
10
  } from '@voxgig/sdkgen'
10
11
 
11
12
 
@@ -17,7 +18,7 @@ import {
17
18
 
18
19
 
19
20
  import {
20
- clean,
21
+ csStringLiteral,
21
22
  formatCsMap,
22
23
  } from './utility_csharp'
23
24
 
@@ -32,78 +33,150 @@ const Config = cmp(async function Config(props: any) {
32
33
 
33
34
  const model: Model = ctx$.model
34
35
 
35
- const entity = getModelPath(model, `main.${KIT}.entity`)
36
36
  const feature = getModelPath(model, `main.${KIT}.feature`)
37
37
 
38
- const headers = getModelPath(model, `main.${KIT}.config.headers`) || {}
39
-
40
- const authActive = isAuthActive(model)
41
- // config.auth.prefix override -> spec-derived info.security.prefix -> 'Bearer'
42
- const authPrefix = resolveAuthPrefix(model)
43
-
44
- let baseUrl = ''
45
- try { baseUrl = getModelPath(model, `main.${KIT}.info.servers.0.url`) } catch (_e) { }
46
-
47
- const authBlock = authActive
48
- ? ` ["auth"] = new Dictionary<string, object?>
49
- {
50
- ["prefix"] = "${authPrefix}",
51
- },\n`
52
- : ''
38
+ // The same config as an OBJECT, built by the shared helper so this target's
39
+ // literal and the data that replaces it above the threshold are the same
40
+ // config by construction. The JSON is what the threshold is measured on -
41
+ // emitted source size varies by language, the model does not.
42
+ const { def: configDef, json: configJson } = configDefinition(model)
43
+ const asData = isConfigData(configJson, configReprSetting(model))
53
44
 
54
45
  File({ name: 'Config.' + target.ext }, () => {
55
46
 
56
47
  Content(`// ${model.const.Name} SDK - generated model configuration and feature
57
48
  // factory. GENERATED from the API model - do not edit by hand.
58
-
49
+ ${asData ? '\nusing System.Text.Json;\n' : ''}
59
50
  namespace ${model.const.Name}Sdk;
60
51
 
61
52
  public static class SdkConfig
62
53
  {
63
- public static Dictionary<string, object?> MakeConfig()
54
+ `)
55
+
56
+ // ABOVE THE THRESHOLD: emit the model as DATA.
57
+ //
58
+ // A composite Dictionary literal is a single expression the C# compiler
59
+ // must bind, type and lower node by node, and every entry becomes IL the
60
+ // JIT executes on first call. A string constant is one token, and
61
+ // System.Text.Json builds the same dictionary from it far faster.
62
+ //
63
+ // JSON.stringify output is ALMOST a valid C# string literal: every escape
64
+ // it emits (\\", \\\\, \\b, \\f, \\n, \\r, \\t, \\uXXXX) means the same thing in C#,
65
+ // and it never emits \\/ or \\0, neither of which C# would accept. What it
66
+ // does leave raw is U+0085/U+2028/U+2029, which C# counts as line
67
+ // terminators and forbids inside a quoted literal - hence csStringLiteral.
68
+ if (asData) {
69
+ Content(` // THE API MODEL, EMBEDDED AS DATA (sdkgen rung L1).
70
+ //
71
+ // Emitted only above a size threshold, or when \`main.kit.config.repr\`
72
+ // pins it: for a small model the literal is smaller and far easier to
73
+ // read when debugging.
74
+ private const string ConfigData = ${csStringLiteral(configJson)};
75
+
76
+ // Boxed numerics compare by exact type - (object)5L does not Equals
77
+ // (object)5 - and MakeConfig is public API consumers read numbers out of,
78
+ // so the two representations must not disagree about the type of a whole
79
+ // number just because the model crossed a size threshold. This ladder
80
+ // (int, else long, else double) is exactly what the literal branch emits;
81
+ // see formatCsNumber in the generator.
82
+ //
83
+ // Deliberately NOT SdkUtility.JsonToNative: that helper's conditional
84
+ // operator gives \`TryGetInt64(out var l) ? l : el.GetDouble()\` the common
85
+ // type double, so it boxes every whole number as a double regardless of
86
+ // its own doc comment. Reusing it here would make the data branch
87
+ // disagree with the literal on every integer in the model.
88
+ private static object? ConfigValue(JsonElement el)
64
89
  {
65
- return new Dictionary<string, object?>
90
+ switch (el.ValueKind)
66
91
  {
67
- ["main"] = new Dictionary<string, object?>
92
+ case JsonValueKind.Object:
68
93
  {
69
- ["name"] = "${model.const.Name}",
70
- },
71
- ["feature"] = new Dictionary<string, object?>
94
+ var map = new Dictionary<string, object?>();
95
+ foreach (var prop in el.EnumerateObject())
96
+ {
97
+ map[prop.Name] = ConfigValue(prop.Value);
98
+ }
99
+ return map;
100
+ }
101
+
102
+ case JsonValueKind.Array:
72
103
  {
73
- `)
104
+ var list = new List<object?>();
105
+ foreach (var item in el.EnumerateArray())
106
+ {
107
+ list.Add(ConfigValue(item));
108
+ }
109
+ return list;
110
+ }
74
111
 
75
- each(feature, (f: any) => {
76
- const fconfig = f.config || {}
77
- Content(` ["${f.name}"] = ${formatCsMap(fconfig, 4)},
78
- `)
79
- })
112
+ case JsonValueKind.String:
113
+ return el.GetString();
80
114
 
81
- Content(` },
82
- ["options"] = new Dictionary<string, object?>
83
- {
84
- ["base"] = "${baseUrl}",
85
- ${authBlock} ["headers"] = ${formatCsMap(headers, 4)},
86
- ["entity"] = new Dictionary<string, object?>
115
+ case JsonValueKind.Number:
116
+ if (el.TryGetInt32(out var i))
87
117
  {
88
- `)
118
+ return i;
119
+ }
120
+ if (el.TryGetInt64(out var l))
121
+ {
122
+ return l;
123
+ }
124
+ return el.GetDouble();
125
+
126
+ case JsonValueKind.True:
127
+ return true;
128
+
129
+ case JsonValueKind.False:
130
+ return false;
89
131
 
90
- each(entity, (ent: any) => {
91
- Content(` ["${ent.name}"] = new Dictionary<string, object?>(),
132
+ default:
133
+ return null;
134
+ }
135
+ }
136
+
137
+ // Parses a fresh, fully materialised config dictionary. Every call
138
+ // re-parses, so hold the result if you need it more than once.
139
+ public static Dictionary<string, object?> MakeConfig()
140
+ {
141
+ return ConfigValue(JsonSerializer.Deserialize<JsonElement>(ConfigData))
142
+ as Dictionary<string, object?>
143
+ ?? new Dictionary<string, object?>();
144
+ }
92
145
  `)
93
- })
146
+ }
147
+ else {
148
+ Content(` public static Dictionary<string, object?> MakeConfig()
149
+ {
150
+ return ${formatCsMap(configDef, 2)};
151
+ }
152
+ `)
153
+ }
94
154
 
95
- Content(` },
96
- },
97
- ["entity"] = ${formatCsMap(
98
- Object.values(entity).reduce((a: any, n: any) => (a[n.name] = clean({
99
- fields: n.fields,
100
- name: n.name,
101
- op: n.op,
102
- relations: n.relations,
103
- }, true), a), {}), 3)},
104
- };
155
+ // SHARED CONFIG (sdkgen rung L2).
156
+ //
157
+ // The SDK reads the config on every request and never writes to it, so one
158
+ // instance is shared by every client rather than rebuilt per client. Above
159
+ // the size threshold MakeConfig re-parses the whole embedded JSON, so this
160
+ // is the difference between parsing the model once per process and once
161
+ // per client.
162
+ //
163
+ // Lazy<T> defaults to ExecutionAndPublication, so concurrent first calls
164
+ // build it exactly once - the C# twin of go's sync.Once.
165
+ Content(`
166
+ private static readonly Lazy<Dictionary<string, object?>> SharedConfigVal =
167
+ new(MakeConfig);
168
+
169
+ // The process-wide config, built once on first use.
170
+ //
171
+ // The returned dictionary is SHARED: treat it as read-only. Callers that
172
+ // need to mutate should use MakeConfig, which always returns a fresh copy.
173
+ public static Dictionary<string, object?> SharedConfig()
174
+ {
175
+ return SharedConfigVal.Value;
105
176
  }
177
+ `)
106
178
 
179
+ Content(`
107
180
  public static Feature.BaseFeature MakeFeature(string name)
108
181
  {
109
182
  switch (name)
@@ -27,7 +27,9 @@ public class ProjectNameSDK
27
27
  {
28
28
  _utility = new Utility();
29
29
 
30
- var config = global::ProjectNameSdk.SdkConfig.MakeConfig();
30
+ // The process-wide config (sdkgen rung L2): read-only on the request
31
+ // path, so every client shares one rather than rebuilding it.
32
+ var config = global::ProjectNameSdk.SdkConfig.SharedConfig();
31
33
 
32
34
  _rootctx = _utility.MakeContext(new Dictionary<string, object?>
33
35
  {
@@ -86,13 +86,103 @@ function formatCsMap(obj: any, indent: number = 0): string {
86
86
  }
87
87
 
88
88
 
89
+ // C# NEW-LINE CHARACTERS, which a regular quoted string literal may not
90
+ // contain: U+000D, U+000A, U+0085 (NEL), U+2028 (LINE SEPARATOR) and U+2029
91
+ // (PARAGRAPH SEPARATOR) - C# language spec, "Line terminators".
92
+ //
93
+ // The first two everyone escapes. The other three are the trap: they are
94
+ // ordinary characters in JSON, in every other target language, and to the eye,
95
+ // so a model string carrying one (an OpenAPI description, default or example
96
+ // pasted from a word processor) emits a C# source file that fails to compile
97
+ // with `error CS1010: Newline in constant`. Verified against .NET 8.
98
+ //
99
+ // Written as ESCAPES, not as the characters themselves: U+2028 and U+2029
100
+ // are line terminators in JavaScript too, so a regex literal containing them
101
+ // verbatim does not parse ("Invalid regular expression: missing /") and this
102
+ // module fails to load. The same trap, one language up.
103
+ const CS_NEWLINE = /[\u0085\u2028\u2029]/g
104
+
105
+ function csEscapeNewlines(s: string): string {
106
+ return s.replace(CS_NEWLINE,
107
+ (c) => '\\u' + (c.codePointAt(0) as number).toString(16).padStart(4, '0'))
108
+ }
109
+
110
+
89
111
  function formatCsString(val: string): string {
90
- return '"' + val
112
+ return '"' + csEscapeNewlines(val
91
113
  .replace(/\\/g, '\\\\')
92
114
  .replace(/"/g, '\\"')
93
115
  .replace(/\n/g, '\\n')
94
116
  .replace(/\r/g, '\\r')
95
- .replace(/\t/g, '\\t') + '"'
117
+ .replace(/\t/g, '\\t')) + '"'
118
+ }
119
+
120
+
121
+ // The whole config JSON as a C# string literal.
122
+ //
123
+ // JSON.stringify output is otherwise a valid C# literal - every escape it
124
+ // emits means the same thing in C#, and it never emits \/ or \0 - but it
125
+ // leaves U+0085/U+2028/U+2029 RAW, and those three end a C# string literal.
126
+ function csStringLiteral(s: string): string {
127
+ return csEscapeNewlines(JSON.stringify(s))
128
+ }
129
+
130
+
131
+ // A number as a C# literal whose BOXED TYPE is predictable.
132
+ //
133
+ // C# gives an unsuffixed integer literal the first type of int/uint/long/ulong
134
+ // that fits it, so `3000000000` boxes as uint while `5` boxes as int and
135
+ // `5000000000` as long. Boxed numerics compare by exact type - (object)5L does
136
+ // not Equals (object)5 - so the type a config number lands with is observable
137
+ // to every consumer, and the L1 data branch has to be able to reproduce it.
138
+ // Reproducing a uint band nothing else in the SDK ever produces is not worth
139
+ // it, so the suffix is made explicit instead and the ladder is just int -> long
140
+ // -> double, which SdkConfig.ConfigValue mirrors exactly.
141
+ //
142
+ // THE RANGE TEST IS ON THE EMITTED TEXT, not on the JS number.
143
+ //
144
+ // `String(val)` is exactly what JSON.stringify puts in the data branch, and it
145
+ // prints a double as the SHORTEST decimal that round-trips - which for a value
146
+ // near the 64-bit boundary is not the value's exact decimal expansion. The
147
+ // clearest case is long.MinValue: `String(-9223372036854775808)` is
148
+ // "-9223372036854776000", which is BELOW long.MinValue, so TryGetInt64 refuses
149
+ // it and the data branch yields a double. Deciding from the JS value instead
150
+ // emitted `long.MinValue` and the two branches disagreed at exactly that
151
+ // boundary. Comparing the text as a BigInt makes both sides ask the same
152
+ // question of the same digits.
153
+ //
154
+ // int.MinValue is written by name: `-2147483648` parses as unary minus applied
155
+ // to a literal that does not itself fit in an int, and relying on the special
156
+ // case for that is needless subtlety. There is deliberately no long.MinValue
157
+ // twin - no JS number prints as that text, so it is unreachable.
158
+ //
159
+ // A non-finite value emits `null`, because that is what JSON.stringify does
160
+ // with it - so the two representations still agree.
161
+ const CS_INT_MIN = -2147483648n
162
+ const CS_INT_MAX = 2147483647n
163
+ const CS_LONG_MIN = -9223372036854775808n
164
+ const CS_LONG_MAX = 9223372036854775807n
165
+
166
+ function formatCsNumber(val: number): string {
167
+ if (!Number.isFinite(val)) {
168
+ return 'null'
169
+ }
170
+ const text = String(val)
171
+ // An integer TOKEN, not merely an integral value: `String` switches to
172
+ // exponential form at 1e21, which is not an integer literal on either side.
173
+ if (Number.isInteger(val) && /^-?\d+$/.test(text)) {
174
+ const n = BigInt(text)
175
+ if (CS_INT_MIN <= n && n <= CS_INT_MAX) {
176
+ return CS_INT_MIN === n ? 'int.MinValue' : text
177
+ }
178
+ if (CS_LONG_MIN <= n && n <= CS_LONG_MAX) {
179
+ return text + 'L'
180
+ }
181
+ }
182
+ // Fractional, or outside the long range: a double literal. The `D` suffix is
183
+ // required for the whole-valued case, where the digits alone would be read
184
+ // as an integer literal (and rejected as too large).
185
+ return text + 'D'
96
186
  }
97
187
 
98
188
 
@@ -104,10 +194,7 @@ function formatCsValue(val: any, indent: number = 0): string {
104
194
  return formatCsString(val)
105
195
  }
106
196
  if (typeof val === 'number') {
107
- if (Number.isInteger(val)) {
108
- return String(val)
109
- }
110
- return String(val)
197
+ return formatCsNumber(val)
111
198
  }
112
199
  if (typeof val === 'boolean') {
113
200
  return val ? 'true' : 'false'
@@ -186,6 +273,7 @@ function clean(o: any, dropDefaults?: boolean): any {
186
273
  export {
187
274
  clean,
188
275
  csPascalName,
276
+ csStringLiteral,
189
277
  csVarName,
190
278
  formatCsMap,
191
279
  formatCsValue,
@@ -7,7 +7,10 @@ import {
7
7
  Fragment,
8
8
  Line,
9
9
  cmp,
10
+ configDefinition,
11
+ configReprSetting,
10
12
  each,
13
+ isConfigData,
11
14
  isAuthActive,
12
15
  resolveAuthPrefix,
13
16
  } from '@voxgig/sdkgen'
@@ -23,6 +26,7 @@ import {
23
26
 
24
27
  import {
25
28
  clean,
29
+ dartStringLiteral,
26
30
  dartValue,
27
31
  } from './utility_dart'
28
32
 
@@ -64,8 +68,42 @@ const Config = cmp(async function Config(props: any) {
64
68
  baseUrl = getModelPath(model, `main.${KIT}.info.servers.0.url`)
65
69
  } catch (_e) { }
66
70
 
71
+ // The same config as an OBJECT, built by the shared helper so this target's
72
+ // literal and the data that replaces it above the threshold are the same
73
+ // config by construction. The JSON is what the threshold is measured on -
74
+ // emitted source size varies by language, the model does not.
75
+ const { def: configDef, json: configJson } = configDefinition(model)
76
+ const asData = isConfigData(configJson, configReprSetting(model))
77
+
67
78
  File({ name: 'Config.' + target.ext }, () => {
68
79
 
80
+ // ABOVE THE THRESHOLD: emit the model as DATA.
81
+ //
82
+ // `jsonDecode` yields exactly what the literal declared - Map<String,
83
+ // dynamic> for objects, List<dynamic> for arrays, and int for a whole
84
+ // number where Dart source would also have written an int - so the fields
85
+ // keep their types and callers cannot tell the representations apart.
86
+ if (asData) {
87
+ Fragment({
88
+ from: ff + 'Config.data.fragment.dart',
89
+
90
+ replace: {
91
+ ...ctx$.stdrep,
92
+
93
+ '// #ImportFeatures': () => each(feature, (f: any) => {
94
+ Line(`import 'feature/${f.name}/${nom(f, 'Name')}Feature.dart';`)
95
+ }),
96
+
97
+ '// #FeatureClasses': () => each(feature, (f: any) => {
98
+ Line(` '${f.name}': () => ${nom(f, 'Name')}Feature(),`)
99
+ }),
100
+
101
+ "'CONFIGJSON'": dartStringLiteral(configJson),
102
+ }
103
+ })
104
+ return
105
+ }
106
+
69
107
  Fragment({
70
108
  from: ff + 'Config.fragment.dart',
71
109
 
@@ -77,11 +115,11 @@ const Config = cmp(async function Config(props: any) {
77
115
  // these; this one did not.
78
116
  ...ctx$.stdrep,
79
117
 
80
- "'BASEURL'": JSON.stringify(baseUrl),
81
-
82
- "'AUTHBLOCK'": authBlock,
83
-
84
- "'HEADERS'": dartValue(headers, 2),
118
+ // The whole options map from the canonical definition. Assembling it
119
+ // slot by slot lost `options.server` entirely, so a spec with a
120
+ // templated server URL described a different config either side of the
121
+ // threshold.
122
+ "'OPTIONSMAP'": dartValue(configDef.options, 1),
85
123
 
86
124
  '// #ImportFeatures': () => each(feature, (f: any) => {
87
125
  Line(`import 'feature/${f.name}/${nom(f, 'Name')}Feature.dart';`)