@voxgig/sdkgen 3.4.6 → 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 (198) 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/cmp/ReadmeTop.js +5 -1
  38. package/dist/cmp/ReadmeTop.js.map +1 -1
  39. package/dist/helpers/definition.d.ts +5 -0
  40. package/dist/helpers/definition.js +72 -0
  41. package/dist/helpers/definition.js.map +1 -0
  42. package/dist/helpers/featureSource.d.ts +8 -2
  43. package/dist/helpers/featureSource.js +61 -12
  44. package/dist/helpers/featureSource.js.map +1 -1
  45. package/dist/helpers/junk.d.ts +11 -0
  46. package/dist/helpers/junk.js +149 -0
  47. package/dist/helpers/junk.js.map +1 -0
  48. package/dist/helpers/manifest.d.ts +50 -0
  49. package/dist/helpers/manifest.js +328 -0
  50. package/dist/helpers/manifest.js.map +1 -0
  51. package/dist/helpers/modelcheck.d.ts +22 -0
  52. package/dist/helpers/modelcheck.js +203 -0
  53. package/dist/helpers/modelcheck.js.map +1 -0
  54. package/dist/helpers/semver.d.ts +4 -0
  55. package/dist/helpers/semver.js +174 -0
  56. package/dist/helpers/semver.js.map +1 -0
  57. package/dist/helpers/shipped.d.ts +4 -0
  58. package/dist/helpers/shipped.js +36 -0
  59. package/dist/helpers/shipped.js.map +1 -0
  60. package/dist/helpers/stdrep.d.ts +9 -1
  61. package/dist/helpers/stdrep.js +22 -0
  62. package/dist/helpers/stdrep.js.map +1 -1
  63. package/dist/sdkgen.d.ts +15 -3
  64. package/dist/sdkgen.js +233 -67
  65. package/dist/sdkgen.js.map +1 -1
  66. package/dist/tsconfig.tsbuildinfo +1 -1
  67. package/dist/types.d.ts +10 -2
  68. package/dist/utility.js +17 -0
  69. package/dist/utility.js.map +1 -1
  70. package/model/sdkgen.aontu +69 -0
  71. package/package.json +3 -2
  72. package/project/.sdk/model/feature/audit.aontu +1 -0
  73. package/project/.sdk/model/feature/cache.aontu +1 -0
  74. package/project/.sdk/model/feature/clienttrack.aontu +1 -0
  75. package/project/.sdk/model/feature/debug.aontu +1 -0
  76. package/project/.sdk/model/feature/idempotency.aontu +1 -0
  77. package/project/.sdk/model/feature/log.aontu +1 -0
  78. package/project/.sdk/model/feature/metrics.aontu +1 -0
  79. package/project/.sdk/model/feature/netsim.aontu +1 -0
  80. package/project/.sdk/model/feature/paging.aontu +1 -0
  81. package/project/.sdk/model/feature/proxy.aontu +1 -0
  82. package/project/.sdk/model/feature/ratelimit.aontu +1 -0
  83. package/project/.sdk/model/feature/rbac.aontu +1 -0
  84. package/project/.sdk/model/feature/retry.aontu +1 -0
  85. package/project/.sdk/model/feature/streaming.aontu +1 -0
  86. package/project/.sdk/model/feature/telemetry.aontu +1 -0
  87. package/project/.sdk/model/feature/test.aontu +1 -0
  88. package/project/.sdk/model/feature/timeout.aontu +1 -0
  89. package/project/.sdk/model/target/c.aontu +1 -0
  90. package/project/.sdk/model/target/clojure.aontu +1 -0
  91. package/project/.sdk/model/target/cpp.aontu +1 -0
  92. package/project/.sdk/model/target/dart.aontu +1 -0
  93. package/project/.sdk/model/target/elixir.aontu +1 -0
  94. package/project/.sdk/model/target/go-cli.aontu +1 -0
  95. package/project/.sdk/model/target/go-mcp.aontu +1 -0
  96. package/project/.sdk/model/target/go.aontu +1 -0
  97. package/project/.sdk/model/target/haskell.aontu +1 -0
  98. package/project/.sdk/model/target/java.aontu +1 -0
  99. package/project/.sdk/model/target/js.aontu +1 -0
  100. package/project/.sdk/model/target/kotlin.aontu +1 -0
  101. package/project/.sdk/model/target/lean.aontu +1 -0
  102. package/project/.sdk/model/target/lua.aontu +1 -0
  103. package/project/.sdk/model/target/ocaml.aontu +1 -0
  104. package/project/.sdk/model/target/perl.aontu +1 -0
  105. package/project/.sdk/model/target/php.aontu +1 -0
  106. package/project/.sdk/model/target/py-data.aontu +1 -0
  107. package/project/.sdk/model/target/py.aontu +1 -0
  108. package/project/.sdk/model/target/rb.aontu +1 -0
  109. package/project/.sdk/model/target/rust.aontu +1 -0
  110. package/project/.sdk/model/target/scala.aontu +1 -0
  111. package/project/.sdk/model/target/seneca-provider.aontu +1 -0
  112. package/project/.sdk/model/target/zig.aontu +1 -0
  113. package/project/.sdk/src/cmp/c/Config_c.ts +44 -0
  114. package/project/.sdk/src/cmp/c/fragment/Main.fragment.c +3 -1
  115. package/project/.sdk/src/cmp/clojure/Config_clojure.ts +37 -42
  116. package/project/.sdk/src/cmp/clojure/utility_clojure.ts +42 -0
  117. package/project/.sdk/src/cmp/cpp/Config_cpp.ts +18 -0
  118. package/project/.sdk/src/cmp/csharp/Config_csharp.ts +125 -52
  119. package/project/.sdk/src/cmp/csharp/fragment/Main.fragment.cs +3 -1
  120. package/project/.sdk/src/cmp/csharp/utility_csharp.ts +94 -6
  121. package/project/.sdk/src/cmp/dart/Config_dart.ts +56 -3
  122. package/project/.sdk/src/cmp/dart/fragment/Config.data.fragment.dart +74 -0
  123. package/project/.sdk/src/cmp/dart/fragment/Config.fragment.dart +6 -9
  124. package/project/.sdk/src/cmp/dart/fragment/Main.fragment.dart +16 -5
  125. package/project/.sdk/src/cmp/dart/utility_dart.ts +18 -0
  126. package/project/.sdk/src/cmp/elixir/Config_elixir.ts +128 -13
  127. package/project/.sdk/src/cmp/elixir/fragment/Main.fragment.ex +3 -1
  128. package/project/.sdk/src/cmp/elixir/utility_elixir.ts +6 -0
  129. package/project/.sdk/src/cmp/haskell/Config_haskell.ts +35 -52
  130. package/project/.sdk/src/cmp/haskell/Package_haskell.ts +1 -0
  131. package/project/.sdk/src/cmp/haskell/Test_haskell.ts +1 -1
  132. package/project/.sdk/src/cmp/haskell/utility_haskell.ts +15 -5
  133. package/project/.sdk/src/cmp/java/Config_java.ts +21 -0
  134. package/project/.sdk/src/cmp/kotlin/Config_kotlin.ts +15 -0
  135. package/project/.sdk/src/cmp/ocaml/Config_ocaml.ts +42 -39
  136. package/project/.sdk/src/cmp/perl/Config_perl.ts +15 -0
  137. package/project/.sdk/src/cmp/perl/fragment/Main.fragment.pm +3 -1
  138. package/project/.sdk/src/cmp/rust/Config_rust.ts +44 -0
  139. package/project/.sdk/src/cmp/rust/fragment/Main.fragment.rs +4 -2
  140. package/project/.sdk/src/cmp/scala/Config_scala.ts +15 -0
  141. package/project/.sdk/src/cmp/swift/Config_swift.ts +17 -0
  142. package/project/.sdk/src/cmp/swift/fragment/Main.fragment.swift +3 -1
  143. package/project/.sdk/src/cmp/zig/Config_zig.ts +44 -0
  144. package/project/.sdk/src/cmp/zig/TestEntity_zig.ts +9 -2
  145. package/project/.sdk/src/cmp/zig/fragment/Main.fragment.zig +31 -18
  146. package/project/.sdk/tm/c/core/sdk.h +1 -0
  147. package/project/.sdk/tm/c/utility/make_options.c +5 -1
  148. package/project/.sdk/tm/clojure/src/sdk/core.clj +51 -3
  149. package/project/.sdk/tm/cpp/core/types.hpp +4 -1
  150. package/project/.sdk/tm/cpp/utility/pipeline.hpp +5 -1
  151. package/project/.sdk/tm/csharp/test/PipelineTest.cs +15 -0
  152. package/project/.sdk/tm/csharp/utility/MakeOptions.cs +67 -1
  153. package/project/.sdk/tm/dart/lib/utility/MakeOptionsUtility.dart +43 -0
  154. package/project/.sdk/tm/elixir/lib/projectname/utility.ex +58 -2
  155. package/project/.sdk/tm/haskell/{test/TestJson.hs → src/SdkJson.hs} +8 -4
  156. package/project/.sdk/tm/haskell/src/SdkRuntime.hs +55 -2
  157. package/project/.sdk/tm/haskell/test/Runner.hs +1 -1
  158. package/project/.sdk/tm/haskell/test/StructCorpus.hs +1 -1
  159. package/project/.sdk/tm/java/core/SdkClient.java +3 -1
  160. package/project/.sdk/tm/java/utility/MakeOptions.java +5 -1
  161. package/project/.sdk/tm/kotlin/core/SdkClient.kt +3 -1
  162. package/project/.sdk/tm/kotlin/utility/MakeOptions.kt +5 -1
  163. package/project/.sdk/tm/ocaml/Makefile +3 -2
  164. package/project/.sdk/tm/ocaml/sdk_json.ml +106 -0
  165. package/project/.sdk/tm/ocaml/sdk_runtime.ml +56 -0
  166. package/project/.sdk/tm/ocaml/test/struct_corpus.ml +4 -86
  167. package/project/.sdk/tm/perl/utility/make_options.pm +5 -1
  168. package/project/.sdk/tm/rust/utility/make_options.rs +79 -1
  169. package/project/.sdk/tm/scala/core/SdkClient.scala +3 -1
  170. package/project/.sdk/tm/scala/utility/Make.scala +5 -1
  171. package/project/.sdk/tm/swift/Sources/ProjectNameSDK/utility/MakeOptions.swift +5 -1
  172. package/project/.sdk/tm/zig/core/utility.zig +85 -1
  173. package/project/.sdk/tm/zig/test/gotcha_test.zig +8 -8
  174. package/project/sdkgen-package.json +57 -0
  175. package/src/action/action.ts +136 -6
  176. package/src/action/check.ts +495 -0
  177. package/src/action/dispatch.ts +105 -0
  178. package/src/action/docs.ts +310 -0
  179. package/src/action/doctor.ts +692 -81
  180. package/src/action/feature.ts +109 -17
  181. package/src/action/kind.ts +375 -0
  182. package/src/action/package.ts +1173 -0
  183. package/src/action/resolve.ts +362 -0
  184. package/src/action/target.ts +290 -108
  185. package/src/cmp/Docs.ts +125 -0
  186. package/src/cmp/ExternalDocs.ts +54 -0
  187. package/src/cmp/ReadmeTop.ts +6 -1
  188. package/src/helpers/definition.ts +82 -0
  189. package/src/helpers/featureSource.ts +83 -13
  190. package/src/helpers/junk.ts +168 -0
  191. package/src/helpers/manifest.ts +474 -0
  192. package/src/helpers/modelcheck.ts +262 -0
  193. package/src/helpers/semver.ts +211 -0
  194. package/src/helpers/shipped.ts +41 -0
  195. package/src/helpers/stdrep.ts +70 -0
  196. package/src/sdkgen.ts +294 -66
  197. package/src/types.ts +33 -3
  198. package/src/utility.ts +17 -0
@@ -1,12 +1,20 @@
1
1
  // ProjectName SDK utility: makeOptions - merge, validate and derive the
2
2
  // client options.
3
3
 
4
+ using System.Text.RegularExpressions;
5
+
4
6
  using Voxgig.Struct;
5
7
 
6
8
  namespace ProjectNameSdk.Util;
7
9
 
8
10
  public static partial class SdkUtility
9
11
  {
12
+ // {name} placeholders in a templated server URL (OpenAPI server
13
+ // variables). Compiled once: MakeOptions runs per client construction.
14
+ private static readonly Regex ServerVarRe =
15
+ new(@"\{([A-Za-z0-9_]+)\}", RegexOptions.Compiled);
16
+
17
+
10
18
  internal static Dictionary<string, object?> MakeOptionsUtil(Context ctx)
11
19
  {
12
20
  var options = ctx.Options ?? new Dictionary<string, object?>();
@@ -73,6 +81,14 @@ public static partial class SdkUtility
73
81
  {
74
82
  ["`$CHILD`"] = "`$STRING`",
75
83
  },
84
+ // OpenAPI server-variable defaults, carried by the generated
85
+ // config whenever the spec's server URL is templated. Accepted
86
+ // here so validation does not reject the SDK's own config; the
87
+ // {name} substitution into base is a separate concern.
88
+ ["server"] = new Dictionary<string, object?>
89
+ {
90
+ ["`$CHILD`"] = "",
91
+ },
76
92
  ["allow"] = new Dictionary<string, object?>
77
93
  {
78
94
  ["method"] = "GET,PUT,POST,PATCH,DELETE,OPTIONS",
@@ -118,12 +134,62 @@ public static partial class SdkUtility
118
134
  var merged = StructUtils.Merge(new List<object?>
119
135
  {
120
136
  new Dictionary<string, object?>(),
121
- cfgopts,
137
+ // CLONE the config side. `config` is a process-wide singleton
138
+ // (SdkConfig.SharedConfig), and Merge uses its nested maps as
139
+ // merge TARGETS - without this, one client's options (headers,
140
+ // server, ...) are written into the shared config and inherited by
141
+ // every client constructed afterwards.
142
+ StructUtils.Clone(cfgopts),
122
143
  opts,
123
144
  });
124
145
  var validated = StructUtils.Validate(merged, optspec);
125
146
  opts = validated as Dictionary<string, object?> ?? new Dictionary<string, object?>();
126
147
 
148
+ // Resolve a templated base URL (e.g. https://{tenant_id}.hanko.io).
149
+ // Every placeholder must resolve to a non-empty value: from
150
+ // options.server (user), else the Config default. A placeholder that
151
+ // resolves to "" is a construction ERROR in live mode - the URL cannot
152
+ // work - but in test mode substitutes the deterministic value
153
+ // "test-<name>" so offline tests need no configuration. The SDK
154
+ // constructor has no error return, so a missing required variable
155
+ // THROWS: this is construction-time misconfiguration.
156
+ if (opts.TryGetValue("base", out var baseRaw) && baseRaw is string baseUrl &&
157
+ baseUrl.Contains('{'))
158
+ {
159
+ var testmode =
160
+ StructUtils.GetPath(opts, StructUtils.Jt("test", "active")) is bool ta && ta ||
161
+ StructUtils.GetPath(opts, StructUtils.Jt("feature", "test", "active"))
162
+ is bool fa && fa;
163
+
164
+ var server = opts.TryGetValue("server", out var sv) &&
165
+ sv is Dictionary<string, object?> svm
166
+ ? svm : new Dictionary<string, object?>();
167
+
168
+ var sdkname = StructUtils.GetPath(config, StructUtils.Jt("main", "name"))
169
+ is string mn && mn != "" ? mn : "SDK";
170
+
171
+ opts["base"] = ServerVarRe.Replace(baseUrl, m =>
172
+ {
173
+ var name = m.Groups[1].Value;
174
+ var val = server.TryGetValue(name, out var v) && v is string s ? s : "";
175
+ if (val == "")
176
+ {
177
+ if (testmode)
178
+ {
179
+ return "test-" + name;
180
+ }
181
+ throw new ProjectNameError("server_var_required",
182
+ sdkname + ": the server variable '" + name + "' is required: " +
183
+ "the API base URL is '" + baseUrl + "' - pass " +
184
+ "new Dictionary<string, object?> { [\"server\"] = new " +
185
+ "Dictionary<string, object?> { [\"" + name + "\"] = \"...\" } } " +
186
+ "in the SDK options",
187
+ ctx);
188
+ }
189
+ return val;
190
+ });
191
+ }
192
+
127
193
  // Restore system.fetch.
128
194
  if (sysFetch != null)
129
195
  {
@@ -1,3 +1,5 @@
1
+ import '../ProjectNameError.dart';
2
+
1
3
  import 'voxgig_struct.dart' as vs;
2
4
 
3
5
  import 'FetcherUtility.dart';
@@ -55,6 +57,11 @@ dynamic makeOptions(dynamic ctx) {
55
57
  'clean': {
56
58
  'keys': 'key,token,id',
57
59
  },
60
+ // Server-variable values for a templated base URL (OpenAPI server
61
+ // variables): {name} placeholders in "base" are substituted from this
62
+ // map at construction. Spec defaults arrive via the generated config;
63
+ // user values override them. Mirrors go's make_options optspec.
64
+ 'server': {'`\$CHILD`': ''},
58
65
  };
59
66
 
60
67
  // Dart specific: preserve the (function-valued) system.fetch across
@@ -95,6 +102,42 @@ dynamic makeOptions(dynamic ctx) {
95
102
 
96
103
  opts = vs.validate(opts, optspec);
97
104
 
105
+ // Resolve a templated base URL (e.g. https://{tenant_id}.hanko.io).
106
+ // Every placeholder must resolve to a non-empty value: from options.server
107
+ // (user), else the Config default. A placeholder that resolves to '' is a
108
+ // construction ERROR in live mode — the URL cannot work — but in test mode
109
+ // substitutes the deterministic value `test-<name>` so offline tests need no
110
+ // configuration. The SDK constructor has no error return, so a missing
111
+ // required variable THROWS: construction-time misconfiguration.
112
+ final baseVal = vs.getprop(opts, 'base');
113
+ if (baseVal is String && baseVal.contains('{')) {
114
+ final testmode = true == vs.getpath(opts, 'test.active') ||
115
+ true == vs.getpath(opts, 'feature.test.active');
116
+ final server = vs.getprop(opts, 'server') ?? {};
117
+ final sdkname = (vs.getpath(config, 'main.name') ?? 'SDK').toString();
118
+
119
+ opts['base'] = baseVal.replaceAllMapped(
120
+ RegExp(r'\{([A-Za-z0-9_]+)\}'),
121
+ (m) {
122
+ final name = m.group(1) as String;
123
+ final raw = vs.getprop(server, name);
124
+ final val = raw is String ? raw : '';
125
+ if ('' == val) {
126
+ if (testmode) {
127
+ return 'test-' + name;
128
+ }
129
+ throw ProjectNameError(
130
+ 'server_var_required',
131
+ "$sdkname: the server variable '$name' is required: the API base "
132
+ "URL is '$baseVal' — pass { 'server': { '$name': '...' } } in the "
133
+ 'SDK options',
134
+ );
135
+ }
136
+ return val;
137
+ },
138
+ );
139
+ }
140
+
98
141
  final sys = vs.getprop(opts, 'system');
99
142
  if (sys is Map) {
100
143
  sys['fetch'] = sysFetch;
@@ -263,15 +263,71 @@ defmodule ProjectName.Utility do
263
263
  "utility" => %{},
264
264
  "system" => %{},
265
265
  "test" => %{"active" => false, "entity" => %{"`$OPEN`" => true}},
266
- "clean" => %{"keys" => "key,token,id"}
266
+ "clean" => %{"keys" => "key,token,id"},
267
+ # Server-variable values for a templated base URL (OpenAPI server
268
+ # variables): {name} placeholders in "base" are substituted from this
269
+ # map at construction. Spec defaults arrive via the generated config;
270
+ # user values override them. Mirrors go's make_options optspec - elixir
271
+ # was the only target validating options that did not accept it, so a
272
+ # spec with a templated server URL failed validation outright.
273
+ "server" => %{"`$CHILD`" => ""}
267
274
  })
268
275
 
269
276
  sys_fetch = S.getpath(opts0, "system.fetch")
270
277
 
271
- merged = S.merge(S.jt([S.jm([]), cfgopts, opts0]))
278
+ # CLONE the config side: `config` is a process-wide singleton
279
+ # (ProjectName.Config.shared_config) and merge uses its nested nodes as
280
+ # merge TARGETS, so without this one client's options (headers, server,
281
+ # ...) are written into the shared config and inherited by every client
282
+ # constructed afterwards.
283
+ merged = S.merge(S.jt([S.jm([]), S.clone(cfgopts), opts0]))
272
284
  validated = S.validate(merged, optspec)
273
285
  opts = if S.ismap(validated), do: validated, else: S.jm([])
274
286
 
287
+ # Resolve a templated base URL (e.g. https://{tenant_id}.hanko.io).
288
+ # Every placeholder must resolve to a non-empty value: from options.server
289
+ # (user), else the Config default. A placeholder that resolves to "" is a
290
+ # construction ERROR in live mode - the URL cannot work - but in test mode
291
+ # substitutes the deterministic value "test-<name>" so offline tests need
292
+ # no configuration. The SDK constructor has no error return, so a missing
293
+ # required variable RAISES: construction-time misconfiguration.
294
+ base = S.getprop(opts, "base")
295
+
296
+ if is_binary(base) and String.contains?(base, "{") do
297
+ testmode =
298
+ true == S.getpath(opts, "test.active") or
299
+ true == S.getpath(opts, "feature.test.active")
300
+
301
+ server = H.or_(S.getprop(opts, "server"), S.jm([]))
302
+ mn = S.getpath(config, "main.name")
303
+ sdkname = if is_binary(mn) and mn != "", do: mn, else: "SDK"
304
+
305
+ resolved =
306
+ Regex.replace(~r/\{([A-Za-z0-9_]+)\}/, base, fn _match, name ->
307
+ val = S.getprop(server, name)
308
+ val = if is_binary(val), do: val, else: ""
309
+
310
+ cond do
311
+ val != "" ->
312
+ val
313
+
314
+ testmode ->
315
+ "test-" <> name
316
+
317
+ true ->
318
+ raise ProjectName.Error,
319
+ code: "server_var_required",
320
+ sdk: "ProjectName",
321
+ msg:
322
+ "#{sdkname}: the server variable '#{name}' is required: the API " <>
323
+ "base URL is '#{base}' - pass %{\"server\" => %{\"#{name}\" => " <>
324
+ "\"...\"}} in the SDK options"
325
+ end
326
+ end)
327
+
328
+ S.setprop(opts, "base", resolved)
329
+ end
330
+
275
331
  if sys_fetch != nil do
276
332
  sysnode = S.getprop(opts, "system")
277
333
 
@@ -1,11 +1,15 @@
1
1
  -- Self-contained JSON reader building struct `Value` nodes directly (no
2
- -- third-party dependency beyond the `array` boot library). Shared by the
3
- -- struct corpus runner and the entity-fixture tests. Uses O(1) array indexing
4
- -- so large corpora parse in linear time.
2
+ -- third-party dependency beyond the `array` boot library). Uses O(1) array
3
+ -- indexing so large corpora parse in linear time.
4
+ --
5
+ -- Lives in src/ rather than test/ because SdkConfig needs it: above a size
6
+ -- threshold the API model is embedded as a JSON string constant and parsed
7
+ -- once at load (sdkgen rung L1) instead of being built as a CV literal. It is
8
+ -- still what the struct corpus runner and the entity-fixture tests use.
5
9
 
6
10
  {-# LANGUAGE LambdaCase #-}
7
11
 
8
- module TestJson (jsonRead) where
12
+ module SdkJson (jsonRead) where
9
13
 
10
14
  import Data.Array (Array, listArray, (!))
11
15
  import Data.Char (chr)
@@ -13,7 +13,7 @@ module SdkRuntime where
13
13
  import Control.Exception (throwIO)
14
14
  import Control.Monad (forM_, when)
15
15
  import Data.Bits ((.&.))
16
- import Data.Char (toUpper)
16
+ import Data.Char (isAlphaNum, toUpper)
17
17
  import Data.IORef
18
18
  import qualified Data.Map.Strict as Map
19
19
  import System.IO.Unsafe (unsafePerformIO)
@@ -1016,6 +1016,11 @@ optSpecValue :: IO Value
1016
1016
  optSpecValue = do
1017
1017
  auth <- jo [("prefix", VStr "")]
1018
1018
  hdrs <- jo [("`$CHILD`", VStr "`$STRING`")]
1019
+ -- OpenAPI server-variable defaults, carried by the generated config whenever
1020
+ -- the spec's server URL is templated. Accepted here so validation does not
1021
+ -- reject the SDK's own config; the {name} substitution into base is a
1022
+ -- separate concern.
1023
+ srv <- jo [("`$CHILD`", VStr "")]
1019
1024
  allow <- jo [("method", VStr "GET,PUT,POST,PATCH,DELETE,OPTIONS"), ("op", VStr "create,update,load,list,remove,command,direct,graphql")]
1020
1025
  entChild <- do a <- emptyMap; jo [("`$OPEN`", VBool True), ("active", VBool False), ("alias", a)]
1021
1026
  ent <- jo [("`$CHILD`", entChild)]
@@ -1027,7 +1032,7 @@ optSpecValue = do
1027
1032
  test <- jo [("active", VBool False), ("entity", testEnt)]
1028
1033
  clean <- jo [("keys", VStr "key,token,id")]
1029
1034
  jo [ ("apikey", VStr ""), ("base", VStr "http://localhost:8000"), ("prefix", VStr ""), ("suffix", VStr "")
1030
- , ("auth", auth), ("headers", hdrs), ("allow", allow), ("entity", ent), ("feature", feat)
1035
+ , ("auth", auth), ("headers", hdrs), ("server", srv), ("allow", allow), ("entity", ent), ("feature", feat)
1031
1036
  , ("utility", utilm), ("system", sysm), ("test", test), ("clean", clean) ]
1032
1037
 
1033
1038
  makeOptionsUtil :: Context -> IO Value
@@ -1079,6 +1084,54 @@ makeOptionsUtil ctx = do
1079
1084
  merged <- merge mlist
1080
1085
  validated <- validate INone merged optspec
1081
1086
  opts <- case validated of VMap _ -> pure validated; _ -> emptyMap
1087
+ -- Resolve a templated base URL (e.g. https://{tenant_id}.hanko.io).
1088
+ -- Every placeholder must resolve to a non-empty value: from options.server
1089
+ -- (user), else the Config default. A placeholder that resolves to "" is a
1090
+ -- construction ERROR in live mode - the URL cannot work - but in test mode
1091
+ -- substitutes the deterministic value "test-<name>" so offline tests need no
1092
+ -- configuration. The SDK constructor has no error return, so a missing
1093
+ -- required variable THROWS: construction-time misconfiguration.
1094
+ baseV <- getp opts "base"
1095
+ case baseV of
1096
+ VStr base | '{' `elem` base -> do
1097
+ taV <- getpathS opts "test.active"
1098
+ faV <- getpathS opts "feature.test.active"
1099
+ -- Value has no Eq instance: match the constructor.
1100
+ let isTrue v = case v of VBool b -> b; _ -> False
1101
+ testmode = isTrue taV || isTrue faV
1102
+ server <- getp opts "server"
1103
+ mnV <- getpathS config "main.name"
1104
+ let sdkname = case mnV of VStr s | not (null s) -> s; _ -> "SDK"
1105
+
1106
+ -- Scanned by hand: a placeholder is `{` followed by [A-Za-z0-9_]+ and
1107
+ -- `}`; anything else is literal text, so a stray brace is left alone.
1108
+ let resolve [] = pure []
1109
+ resolve ('{' : rest) =
1110
+ let (name, tail') = span (\c -> isAlphaNum c || '_' == c) rest
1111
+ in case tail' of
1112
+ ('}' : more) | not (null name) -> do
1113
+ valV <- case server of VMap _ -> getp server name; _ -> pure VNoval
1114
+ let val = case valV of VStr s -> s; _ -> ""
1115
+ sub <- if not (null val)
1116
+ then pure val
1117
+ else if testmode
1118
+ then pure ("test-" ++ name)
1119
+ else do
1120
+ e <- jo [ ("code", VStr "server_var_required")
1121
+ , ("msg", VStr (sdkname ++ ": the server variable '" ++
1122
+ name ++ "' is required: the API base URL is '" ++
1123
+ base ++ "' - pass server = { \"" ++ name ++
1124
+ "\": \"...\" } in the SDK options"))
1125
+ , ("sdk", VStr "ProjectName") ]
1126
+ throwIO (SdkException e)
1127
+ more' <- resolve more
1128
+ pure (sub ++ more')
1129
+ _ -> do r <- resolve rest; pure ('{' : r)
1130
+ resolve (c : rest) = do r <- resolve rest; pure (c : r)
1131
+ resolved <- resolve base
1132
+ setp opts "base" (VStr resolved)
1133
+ _ -> pure ()
1134
+
1082
1135
  when (not (isNoval sysFetch)) $ do
1083
1136
  sys <- getp opts "system"
1084
1137
  case sys of
@@ -10,7 +10,7 @@ import Data.IORef
10
10
  import System.Exit (exitFailure)
11
11
 
12
12
  import Testutil
13
- import TestJson (jsonRead)
13
+ import SdkJson (jsonRead)
14
14
  import StructCorpus (runStructCorpus)
15
15
  import qualified TPipeline
16
16
  import qualified TFeature
@@ -14,7 +14,7 @@ import Data.List (intercalate, isPrefixOf)
14
14
 
15
15
  import VoxgigStruct
16
16
  import qualified Vregex
17
- import TestJson (jsonRead)
17
+ import SdkJson (jsonRead)
18
18
 
19
19
  nullmark, undefmark, existsmark :: String
20
20
  nullmark = "__NULL__"
@@ -27,7 +27,9 @@ public abstract class SdkClient {
27
27
  protected SdkClient(Map<String, Object> options) {
28
28
  this.utility = new Utility();
29
29
 
30
- Map<String, Object> config = Config.makeConfig();
30
+ // The process-wide config (sdkgen rung L2): read-only on the request path,
31
+ // so every client shares one rather than rebuilding it.
32
+ Map<String, Object> config = Config.sharedConfig();
31
33
 
32
34
  Map<String, Object> ctxmap = new LinkedHashMap<>();
33
35
  ctxmap.put("client", this);
@@ -93,7 +93,11 @@ final class MakeOptions {
93
93
 
94
94
  List<Object> mergeList = new ArrayList<>();
95
95
  mergeList.add(new LinkedHashMap<String, Object>());
96
- mergeList.add(cfgopts);
96
+ // CLONE the config side: `config` is a process-wide singleton
97
+ // (Config.sharedConfig) and merge uses its nested maps as merge TARGETS, so
98
+ // without this one client's options (headers, server, ...) are written into
99
+ // the shared config and inherited by every client constructed afterwards.
100
+ mergeList.add(Struct.clone(cfgopts));
97
101
  mergeList.add(opts);
98
102
  Object merged = Struct.merge(mergeList);
99
103
 
@@ -21,7 +21,9 @@ abstract class SdkClient(sdkopts: MutableMap<String, Any?>?) {
21
21
  protected val rootctx: Context
22
22
 
23
23
  init {
24
- val config = Config.makeConfig()
24
+ // The process-wide config (sdkgen rung L2): read-only on the request path,
25
+ // so every client shares one rather than rebuilding it.
26
+ val config = Config.sharedConfig()
25
27
 
26
28
  val ctxmap = linkedMapOf<String, Any?>()
27
29
  ctxmap["client"] = this
@@ -84,7 +84,11 @@ fun makeOptions(ctx: Context): MutableMap<String, Any?> {
84
84
 
85
85
  val mergeList = mutableListOf<Any?>()
86
86
  mergeList.add(linkedMapOf<String, Any?>())
87
- mergeList.add(cfgopts)
87
+ // CLONE the config side: `config` is a process-wide singleton
88
+ // (Config.sharedConfig) and merge uses its nested maps as merge TARGETS, so
89
+ // without this one client's options (headers, server, ...) are written into
90
+ // the shared config and inherited by every client constructed afterwards.
91
+ mergeList.add(Struct.clone(cfgopts))
88
92
  mergeList.add(opts)
89
93
  val merged = Struct.merge(mergeList)
90
94
 
@@ -14,7 +14,7 @@ INC = -I utility -I . -I test
14
14
  # The shared JSON corpus lives in the sibling .sdk workspace.
15
15
  CORPUS = ../.sdk/test/test.json
16
16
 
17
- RUNTIME = utility/vregex.ml utility/voxgig_struct.ml \
17
+ RUNTIME = utility/vregex.ml utility/voxgig_struct.ml sdk_json.ml \
18
18
  sdk_types.ml sdk_helpers.ml sdk_runtime.ml sdk_features.ml
19
19
 
20
20
  ENTITIES = $(sort $(wildcard sdk_entity_*.ml))
@@ -33,7 +33,8 @@ SDK_TEST = $(RUNTIME) sdk_config.ml $(ENTITIES) sdk_client.ml sdk_error.ml \
33
33
  test/t_pipeline.ml test/t_feature.ml test/custom_utility.ml \
34
34
  $(ENTITY_TESTS) $(DOC_TESTS) test/t_main.ml
35
35
 
36
- CORPUS_SRC = utility/vregex.ml utility/voxgig_struct.ml test/struct_corpus.ml
36
+ CORPUS_SRC = utility/vregex.ml utility/voxgig_struct.ml sdk_json.ml \
37
+ test/struct_corpus.ml
37
38
 
38
39
  .PHONY: test test-sdk test-corpus build lint clean
39
40
 
@@ -0,0 +1,106 @@
1
+ (* JSON text -> voxgig struct value.
2
+ *
3
+ * A dependency-free reader, so the SDK can parse JSON with no opam packages -
4
+ * the same position every other target is in (elixir's ProjectName.Json,
5
+ * clojure's sdk.core/json-parse, the json_parse in c/rust/zig).
6
+ *
7
+ * It lives in the RUNTIME rather than the test tree because the generated
8
+ * config uses it: above a size threshold the API model is emitted as a JSON
9
+ * string constant and parsed here (sdkgen rung L1) instead of as a literal the
10
+ * compiler has to walk. `test/struct_corpus.ml` uses this same function rather
11
+ * than its own copy, so the corpus and the SDK cannot disagree about what a
12
+ * given JSON text means.
13
+ *
14
+ * Numbers are read as Num (float), which is exactly what formatOcamlValue
15
+ * emits for the literal form - so the two representations cannot disagree
16
+ * about the type of a whole number.
17
+ *)
18
+
19
+ open Voxgig_struct
20
+
21
+ (* ---------------- JSON reader -> value ---------------- *)
22
+
23
+ let json_read (s : string) : value =
24
+ let n = String.length s in
25
+ let pos = ref 0 in
26
+ let peek () = if !pos < n then Some s.[!pos] else None in
27
+ let adv () = incr pos in
28
+ let skip_ws () =
29
+ while !pos < n && (match s.[!pos] with ' ' | '\t' | '\n' | '\r' -> true | _ -> false) do incr pos done
30
+ in
31
+ let rec pval () =
32
+ skip_ws ();
33
+ match peek () with
34
+ | Some '{' -> pobj ()
35
+ | Some '[' -> parr ()
36
+ | Some '"' -> Str (pstr ())
37
+ | Some 't' -> pos := !pos + 4; Bool true
38
+ | Some 'f' -> pos := !pos + 5; Bool false
39
+ | Some 'n' -> pos := !pos + 4; Null
40
+ | _ -> pnum ()
41
+ and pobj () =
42
+ adv (); skip_ws ();
43
+ if peek () = Some '}' then (adv (); empty_map ())
44
+ else begin
45
+ let m = empty_map () in
46
+ let rec loop () =
47
+ skip_ws ();
48
+ let k = pstr () in
49
+ skip_ws (); adv (); (* : *)
50
+ let v = pval () in
51
+ ignore (setprop m (Str k) v);
52
+ skip_ws ();
53
+ let c = (match peek () with Some c -> adv (); c | None -> '}') in
54
+ if c = ',' then loop () else m
55
+ in loop ()
56
+ end
57
+ and parr () =
58
+ adv (); skip_ws ();
59
+ if peek () = Some ']' then (adv (); empty_list ())
60
+ else begin
61
+ let acc = ref [] in
62
+ let rec loop () =
63
+ let v = pval () in
64
+ acc := v :: !acc;
65
+ skip_ws ();
66
+ let c = (match peek () with Some c -> adv (); c | None -> ']') in
67
+ if c = ',' then loop () else lst (List.rev !acc)
68
+ in loop ()
69
+ end
70
+ and pstr () =
71
+ adv ();
72
+ let b = Buffer.create 16 in
73
+ let rec loop () =
74
+ let c = s.[!pos] in adv ();
75
+ if c = '"' then Buffer.contents b
76
+ else if c = '\\' then begin
77
+ let e = s.[!pos] in adv ();
78
+ (match e with
79
+ | '"' -> Buffer.add_char b '"' | '\\' -> Buffer.add_char b '\\'
80
+ | '/' -> Buffer.add_char b '/' | 'n' -> Buffer.add_char b '\n'
81
+ | 't' -> Buffer.add_char b '\t' | 'r' -> Buffer.add_char b '\r'
82
+ | 'b' -> Buffer.add_char b '\b' | 'f' -> Buffer.add_char b '\012'
83
+ | 'u' ->
84
+ let hex = String.sub s !pos 4 in pos := !pos + 4;
85
+ let code = int_of_string ("0x" ^ hex) in
86
+ if code < 128 then Buffer.add_char b (Char.chr code)
87
+ else if code < 2048 then begin
88
+ Buffer.add_char b (Char.chr (0xC0 lor (code lsr 6)));
89
+ Buffer.add_char b (Char.chr (0x80 lor (code land 0x3F)))
90
+ end else begin
91
+ Buffer.add_char b (Char.chr (0xE0 lor (code lsr 12)));
92
+ Buffer.add_char b (Char.chr (0x80 lor ((code lsr 6) land 0x3F)));
93
+ Buffer.add_char b (Char.chr (0x80 lor (code land 0x3F)))
94
+ end
95
+ | c -> Buffer.add_char b c);
96
+ loop ()
97
+ end else (Buffer.add_char b c; loop ())
98
+ in loop ()
99
+ and pnum () =
100
+ let start = !pos in
101
+ while !pos < n && (match s.[!pos] with
102
+ | '0'..'9' | '-' | '+' | '.' | 'e' | 'E' -> true | _ -> false) do incr pos done;
103
+ let tok = String.sub s start (!pos - start) in
104
+ Num (float_of_string tok)
105
+ in
106
+ pval ()
@@ -905,6 +905,11 @@ let opt_spec_value () : value =
905
905
  ("system", empty_map ());
906
906
  ("test", jo [("active", Bool false); ("entity", jo [("`$OPEN`", Bool true)])]);
907
907
  ("clean", jo [("keys", Str "key,token,id")]);
908
+ (* Server-variable values for a templated base URL (OpenAPI server
909
+ * variables): {name} placeholders in "base" are substituted from this
910
+ * map at construction. Spec defaults arrive via the generated config;
911
+ * user values override them. Mirrors go's make_options optspec. *)
912
+ ("server", jo [("`$CHILD`", Str "")]);
908
913
  ]
909
914
 
910
915
  let make_options_util (ctx : ctx) : value =
@@ -946,6 +951,57 @@ let make_options_util (ctx : ctx) : value =
946
951
  let merged = merge (ja [empty_map (); cfgopts; opts]) in
947
952
  let validated = validate merged optspec in
948
953
  let opts = match validated with Map _ as m -> m | _ -> empty_map () in
954
+ (* Resolve a templated base URL (e.g. https://{tenant_id}.hanko.io).
955
+ Every placeholder must resolve to a non-empty value: from options.server
956
+ (user), else the Config default. A placeholder that resolves to "" is a
957
+ construction ERROR in live mode - the URL cannot work - but in test mode
958
+ substitutes the deterministic value "test-<name>" so offline tests need no
959
+ configuration. The SDK constructor has no error return, so a missing
960
+ required variable RAISES: construction-time misconfiguration.
961
+
962
+ Scanned by hand: a placeholder is `{` followed by [A-Za-z0-9_]+ and `}`;
963
+ anything else is literal text, so a stray brace is left alone. *)
964
+ (match getp opts "base" with
965
+ | Str base when String.contains base '{' ->
966
+ let is_true v = match v with Bool b -> b | _ -> false in
967
+ let testmode =
968
+ is_true (getpath_s opts "test.active")
969
+ || is_true (getpath_s opts "feature.test.active") in
970
+ let server = match getp opts "server" with Map _ as m -> m | _ -> empty_map () in
971
+ let sdkname =
972
+ match getpath_s config "main.name" with Str s when s <> "" -> s | _ -> "SDK" in
973
+ let namechar c =
974
+ (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')
975
+ || (c >= '0' && c <= '9') || c = '_' in
976
+ let buf = Buffer.create (String.length base) in
977
+ let n = String.length base in
978
+ let i = ref 0 in
979
+ while !i < n do
980
+ if base.[!i] <> '{' then begin
981
+ Buffer.add_char buf base.[!i];
982
+ incr i
983
+ end else begin
984
+ let j = ref (!i + 1) in
985
+ while !j < n && namechar base.[!j] do incr j done;
986
+ if !j >= n || base.[!j] <> '}' || !j = !i + 1 then begin
987
+ Buffer.add_char buf base.[!i];
988
+ incr i
989
+ end else begin
990
+ let name = String.sub base (!i + 1) (!j - !i - 1) in
991
+ let value = match getp server name with Str s -> s | _ -> "" in
992
+ if value <> "" then Buffer.add_string buf value
993
+ else if testmode then Buffer.add_string buf ("test-" ^ name)
994
+ else
995
+ raise (Sdk_error_exc (ctx_make_error ctx "server_var_required"
996
+ (sdkname ^ ": the server variable '" ^ name ^ "' is required: the API "
997
+ ^ "base URL is '" ^ base ^ "' - pass ~server:[(\"" ^ name
998
+ ^ "\", \"...\")] in the SDK options")));
999
+ i := !j + 1
1000
+ end
1001
+ end
1002
+ done;
1003
+ setp opts "base" (Str (Buffer.contents buf))
1004
+ | _ -> ());
949
1005
  (if not (is_noval sys_fetch) then
950
1006
  match getp opts "system" with
951
1007
  | Map _ as sys -> setp sys "fetch" sys_fetch