@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
@@ -8,92 +8,10 @@ let nullmark = "__NULL__"
8
8
  let undefmark = "__UNDEF__"
9
9
  let existsmark = "__EXISTS__"
10
10
 
11
- (* ---------------- JSON reader -> value ---------------- *)
12
-
13
- let json_read (s : string) : value =
14
- let n = String.length s in
15
- let pos = ref 0 in
16
- let peek () = if !pos < n then Some s.[!pos] else None in
17
- let adv () = incr pos in
18
- let skip_ws () =
19
- while !pos < n && (match s.[!pos] with ' ' | '\t' | '\n' | '\r' -> true | _ -> false) do incr pos done
20
- in
21
- let rec pval () =
22
- skip_ws ();
23
- match peek () with
24
- | Some '{' -> pobj ()
25
- | Some '[' -> parr ()
26
- | Some '"' -> Str (pstr ())
27
- | Some 't' -> pos := !pos + 4; Bool true
28
- | Some 'f' -> pos := !pos + 5; Bool false
29
- | Some 'n' -> pos := !pos + 4; Null
30
- | _ -> pnum ()
31
- and pobj () =
32
- adv (); skip_ws ();
33
- if peek () = Some '}' then (adv (); empty_map ())
34
- else begin
35
- let m = empty_map () in
36
- let rec loop () =
37
- skip_ws ();
38
- let k = pstr () in
39
- skip_ws (); adv (); (* : *)
40
- let v = pval () in
41
- ignore (setprop m (Str k) v);
42
- skip_ws ();
43
- let c = (match peek () with Some c -> adv (); c | None -> '}') in
44
- if c = ',' then loop () else m
45
- in loop ()
46
- end
47
- and parr () =
48
- adv (); skip_ws ();
49
- if peek () = Some ']' then (adv (); empty_list ())
50
- else begin
51
- let acc = ref [] in
52
- let rec loop () =
53
- let v = pval () in
54
- acc := v :: !acc;
55
- skip_ws ();
56
- let c = (match peek () with Some c -> adv (); c | None -> ']') in
57
- if c = ',' then loop () else lst (List.rev !acc)
58
- in loop ()
59
- end
60
- and pstr () =
61
- adv ();
62
- let b = Buffer.create 16 in
63
- let rec loop () =
64
- let c = s.[!pos] in adv ();
65
- if c = '"' then Buffer.contents b
66
- else if c = '\\' then begin
67
- let e = s.[!pos] in adv ();
68
- (match e with
69
- | '"' -> Buffer.add_char b '"' | '\\' -> Buffer.add_char b '\\'
70
- | '/' -> Buffer.add_char b '/' | 'n' -> Buffer.add_char b '\n'
71
- | 't' -> Buffer.add_char b '\t' | 'r' -> Buffer.add_char b '\r'
72
- | 'b' -> Buffer.add_char b '\b' | 'f' -> Buffer.add_char b '\012'
73
- | 'u' ->
74
- let hex = String.sub s !pos 4 in pos := !pos + 4;
75
- let code = int_of_string ("0x" ^ hex) in
76
- if code < 128 then Buffer.add_char b (Char.chr code)
77
- else if code < 2048 then begin
78
- Buffer.add_char b (Char.chr (0xC0 lor (code lsr 6)));
79
- Buffer.add_char b (Char.chr (0x80 lor (code land 0x3F)))
80
- end else begin
81
- Buffer.add_char b (Char.chr (0xE0 lor (code lsr 12)));
82
- Buffer.add_char b (Char.chr (0x80 lor ((code lsr 6) land 0x3F)));
83
- Buffer.add_char b (Char.chr (0x80 lor (code land 0x3F)))
84
- end
85
- | c -> Buffer.add_char b c);
86
- loop ()
87
- end else (Buffer.add_char b c; loop ())
88
- in loop ()
89
- and pnum () =
90
- let start = !pos in
91
- while !pos < n && (match s.[!pos] with
92
- | '0'..'9' | '-' | '+' | '.' | 'e' | 'E' -> true | _ -> false) do incr pos done;
93
- let tok = String.sub s start (!pos - start) in
94
- Num (float_of_string tok)
95
- in
96
- pval ()
11
+ (* The JSON reader now lives in the runtime (sdk_json.ml) so the SDK's
12
+ * generated config can use it; the corpus runs against that same function
13
+ * rather than a second copy that could drift from it. *)
14
+ let json_read = Sdk_json.json_read
97
15
 
98
16
  (* ---------------- fixJSON / equality ---------------- *)
99
17
 
@@ -82,7 +82,11 @@ $REGISTRY{make_options} = sub {
82
82
 
83
83
  my $sys_fetch = ProjectNameHelpers::gpath($opts, 'system.fetch');
84
84
 
85
- my $merged = Voxgig::Struct::merge([{}, $cfgopts, $opts]);
85
+ # CLONE the config side: `config` is a process-wide singleton
86
+ # (ProjectNameConfig::shared_config) and merge uses its nested hashes as merge
87
+ # TARGETS, so without this one client's options (headers, server, ...) are
88
+ # written into the shared config and inherited by every client after it.
89
+ my $merged = Voxgig::Struct::merge([{}, Voxgig::Struct::clone($cfgopts), $opts]);
86
90
  my $validated = Voxgig::Struct::validate($merged, $optspec);
87
91
  $opts = Voxgig::Struct::ismap($validated) ? $validated : {};
88
92
 
@@ -98,18 +98,96 @@ pub fn make_options_util(ctx: &Rc<Context>) -> Value {
98
98
  ]),
99
99
  ),
100
100
  ("clean", jo(vec![("keys", Value::str("key,token,id"))])),
101
+ // Server-variable values for a templated base URL (OpenAPI server
102
+ // variables): {name} placeholders in "base" are substituted from this
103
+ // map at construction. Spec defaults arrive via the generated config;
104
+ // user values override them. Mirrors go's make_options optspec.
105
+ ("server", jo(vec![("`$CHILD`", Value::str(""))])),
101
106
  ]);
102
107
 
103
108
  // Preserve system.fetch before merge/validate (validation strips it).
104
109
  let sys_fetch = getpath(&["system", "fetch"], &opts);
105
110
 
106
- let merged = vs::merge(&ja(vec![Value::empty_map(), cfgopts, opts.clone()]), None);
111
+ // CLONE the config side: `config` is a per-thread singleton
112
+ // (core::config::shared_config) and merge uses its nested maps as merge
113
+ // TARGETS, so without this one client's options (headers, server, ...) are
114
+ // written into the shared config and inherited by every client after it.
115
+ let merged = vs::merge(
116
+ &ja(vec![Value::empty_map(), vs::clone(&cfgopts), opts.clone()]), None);
107
117
  if let Ok(validated) = vs::validate(&merged, &optspec, None) {
108
118
  if let Value::Map(_) = validated {
109
119
  opts = validated;
110
120
  }
111
121
  }
112
122
 
123
+ // Resolve a templated base URL (e.g. https://{tenant_id}.hanko.io).
124
+ // Every placeholder must resolve to a non-empty value: from options.server
125
+ // (user), else the Config default. A placeholder that resolves to "" is a
126
+ // construction ERROR in live mode - the URL cannot work - but in test mode
127
+ // substitutes the deterministic value "test-<name>" so offline tests need
128
+ // no configuration. The SDK constructor has no error return, so a missing
129
+ // required variable PANICS: construction-time misconfiguration.
130
+ //
131
+ // Scanned by hand rather than with vs::re_replace, whose replacement is a
132
+ // fixed string and cannot vary per placeholder.
133
+ if let Value::Str(base) = getp(&opts, "base") {
134
+ if base.contains('{') {
135
+ let testmode = matches!(getpath(&["test", "active"], &opts), Value::Bool(true))
136
+ || matches!(
137
+ getpath(&["feature", "test", "active"], &opts),
138
+ Value::Bool(true)
139
+ );
140
+ let server = getp(&opts, "server");
141
+ let sdkname = match getpath(&["main", "name"], &config) {
142
+ Value::Str(s) if !s.is_empty() => s,
143
+ _ => "SDK".to_string(),
144
+ };
145
+
146
+ let mut resolved = String::with_capacity(base.len());
147
+ let bytes: Vec<char> = base.chars().collect();
148
+ let mut i = 0usize;
149
+ while i < bytes.len() {
150
+ if '{' != bytes[i] {
151
+ resolved.push(bytes[i]);
152
+ i += 1;
153
+ continue;
154
+ }
155
+ // A placeholder only when it closes and the name is
156
+ // [A-Za-z0-9_]+; anything else is literal text.
157
+ let mut j = i + 1;
158
+ while j < bytes.len() && (bytes[j].is_ascii_alphanumeric() || '_' == bytes[j]) {
159
+ j += 1;
160
+ }
161
+ if j >= bytes.len() || '}' != bytes[j] || j == i + 1 {
162
+ resolved.push(bytes[i]);
163
+ i += 1;
164
+ continue;
165
+ }
166
+ let name: String = bytes[i + 1..j].iter().collect();
167
+ let val = match getp(&server, &name) {
168
+ Value::Str(s) => s,
169
+ _ => String::new(),
170
+ };
171
+ if val.is_empty() {
172
+ if testmode {
173
+ resolved.push_str(&format!("test-{}", name));
174
+ } else {
175
+ panic!(
176
+ "{}: the server variable '{}' is required: the API base \
177
+ URL is '{}' - pass server: {{ \"{}\": \"...\" }} in the \
178
+ SDK options",
179
+ sdkname, name, base, name
180
+ );
181
+ }
182
+ } else {
183
+ resolved.push_str(&val);
184
+ }
185
+ i = j + 1;
186
+ }
187
+ setp(&opts, "base", Value::str(&resolved));
188
+ }
189
+ }
190
+
113
191
  // Restore system.fetch.
114
192
  if !sys_fetch.is_noval() {
115
193
  let sys = getp(&opts, "system");
@@ -16,7 +16,9 @@ abstract class SdkClient(options0: JMap[String, Object]) {
16
16
  protected var rootctx: Context = null
17
17
 
18
18
  locally {
19
- val config = Config.makeConfig()
19
+ // The process-wide config (sdkgen rung L2): read-only on the request path,
20
+ // so every client shares one rather than rebuilding it.
21
+ val config = Config.sharedConfig()
20
22
 
21
23
  val ctxmap = new LinkedHashMap[String, Object]()
22
24
  ctxmap.put("client", this)
@@ -216,7 +216,11 @@ object MakeOptions {
216
216
 
217
217
  val mergeList = new ArrayList[Object]()
218
218
  mergeList.add(new LinkedHashMap[String, Object]())
219
- mergeList.add(cfgopts)
219
+ // CLONE the config side: `config` is a process-wide singleton
220
+ // (Config.sharedConfig) and merge uses its nested maps as merge TARGETS, so
221
+ // without this one client's options (headers, server, ...) are written into
222
+ // the shared config and inherited by every client constructed afterwards.
223
+ mergeList.add(Struct.clone(cfgopts))
220
224
  mergeList.add(opts)
221
225
  val merged = Struct.merge(mergeList)
222
226
 
@@ -98,7 +98,11 @@ func makeOptionsUtil(_ ctx: Context) -> VMap {
98
98
  // validation reshapes the system block).
99
99
  let sysFetch = gpath(opts, "system", "fetch")
100
100
 
101
- let merged = merge(.list([.map(VMap()), .map(cfgopts), .map(opts)]))
101
+ // CLONE the config side: `config` is a process-wide singleton
102
+ // (SdkConfig.sharedConfig) and merge uses its nested maps as merge TARGETS,
103
+ // so without this one client's options (headers, server, ...) are written
104
+ // into the shared config and inherited by every client built afterwards.
105
+ let merged = merge(.list([.map(VMap()), clone(.map(cfgopts)), .map(opts)]))
102
106
  let validated = validate(merged, optspec)
103
107
  let result = validated.asMap ?? VMap()
104
108
 
@@ -35,6 +35,14 @@ fn fmt(comptime f: []const u8, args: anytype) []const u8 {
35
35
  return std.fmt.allocPrint(h.A(), f, args) catch "";
36
36
  }
37
37
 
38
+ // Boolean-or-absent: an option that is unset, null or a non-boolean is false.
39
+ fn is_true(v: Value) bool {
40
+ return switch (v) {
41
+ .bool => |b| b,
42
+ else => false,
43
+ };
44
+ }
45
+
38
46
  // ============================================================================
39
47
  // Utility bundle
40
48
  // ============================================================================
@@ -368,17 +376,93 @@ pub fn make_options_util(ctx: *Context) Value {
368
376
  .{ "entity", h.jo(&.{.{ "`$OPEN`", h.vbool(true) }}) },
369
377
  }) },
370
378
  .{ "clean", h.jo(&.{.{ "keys", h.vstr("key,token,id") }}) },
379
+ // Server-variable values for a templated base URL (OpenAPI server
380
+ // variables): {name} placeholders in "base" are substituted from this
381
+ // map at construction. Spec defaults arrive via the generated config;
382
+ // user values override them. Mirrors go's make_options optspec.
383
+ .{ "server", h.jo(&.{.{ "`$CHILD`", h.vstr("") }}) },
371
384
  });
372
385
 
373
386
  // Preserve system.fetch before merge/validate (validation strips it).
374
387
  const sys_fetch = h.getpath(&.{ "system", "fetch" }, opts);
375
388
 
376
- const merged = vs.merge(h.A(), h.ja(&.{ h.omap(), cfgopts, opts }), vs.MAXDEPTH) catch opts;
389
+ // CLONE the config side: `config` is a process-wide singleton
390
+ // (config.shared_config) and merge uses its nested maps as merge TARGETS,
391
+ // so without this one client's options (headers, server, ...) are written
392
+ // into the shared config and inherited by every client built after it.
393
+ const merged = vs.merge(h.A(), h.ja(&.{ h.omap(), h.clone(cfgopts), opts }), vs.MAXDEPTH) catch opts;
377
394
  const vres = vs.validate(h.A(), merged, optspec) catch null;
378
395
  if (vres) |vr| {
379
396
  if (vr.err == null and vr.out == .object) opts = vr.out;
380
397
  }
381
398
 
399
+ // Resolve a templated base URL (e.g. https://{tenant_id}.hanko.io).
400
+ // Every placeholder must resolve to a non-empty value: from options.server
401
+ // (user), else the Config default. A placeholder that resolves to "" is a
402
+ // construction ERROR in live mode - the URL cannot work - but in test mode
403
+ // substitutes the deterministic value "test-<name>" so offline tests need
404
+ // no configuration. The SDK constructor has no error return, so a missing
405
+ // required variable PANICS: construction-time misconfiguration.
406
+ //
407
+ // Scanned by hand: a placeholder is `{` followed by [A-Za-z0-9_]+ and `}`;
408
+ // anything else is literal text, so a stray brace in a URL is left alone.
409
+ switch (h.getp(opts, "base")) {
410
+ .string => |base| {
411
+ if (null != std.mem.indexOfScalar(u8, base, '{')) {
412
+ const testmode =
413
+ is_true(h.getpath(&.{ "test", "active" }, opts)) or
414
+ is_true(h.getpath(&.{ "feature", "test", "active" }, opts));
415
+ const server = h.getp(opts, "server");
416
+ const sdkname: []const u8 = switch (h.getpath(&.{ "main", "name" }, config)) {
417
+ .string => |s| if (0 < s.len) s else "SDK",
418
+ else => "SDK",
419
+ };
420
+
421
+ var out = std.ArrayList(u8).init(h.A());
422
+ var i: usize = 0;
423
+ while (i < base.len) {
424
+ if ('{' != base[i]) {
425
+ out.append(base[i]) catch {};
426
+ i += 1;
427
+ continue;
428
+ }
429
+ var j = i + 1;
430
+ while (j < base.len and (std.ascii.isAlphanumeric(base[j]) or '_' == base[j])) {
431
+ j += 1;
432
+ }
433
+ if (j >= base.len or '}' != base[j] or j == i + 1) {
434
+ out.append(base[i]) catch {};
435
+ i += 1;
436
+ continue;
437
+ }
438
+ const name = base[i + 1 .. j];
439
+ const val: []const u8 = switch (h.getp(server, name)) {
440
+ .string => |s| s,
441
+ else => "",
442
+ };
443
+ if (0 == val.len) {
444
+ if (testmode) {
445
+ out.appendSlice("test-") catch {};
446
+ out.appendSlice(name) catch {};
447
+ } else {
448
+ std.debug.panic(
449
+ "{s}: the server variable '{s}' is required: the API " ++
450
+ "base URL is '{s}' - pass .server = .{{ .{s} = \"...\" }} " ++
451
+ "in the SDK options",
452
+ .{ sdkname, name, base, name },
453
+ );
454
+ }
455
+ } else {
456
+ out.appendSlice(val) catch {};
457
+ }
458
+ i = j + 1;
459
+ }
460
+ h.setp(opts, "base", h.vstr(out.toOwnedSlice() catch base));
461
+ }
462
+ },
463
+ else => {},
464
+ }
465
+
382
466
  // Restore system.fetch.
383
467
  if (!h.is_noval(sys_fetch)) {
384
468
  const sysv = h.getp(opts, "system");
@@ -100,7 +100,7 @@ test "gotcha8: custom utility callable survives" {
100
100
  const fn_val = h.callable(@ptrCast(&probe_dummy), probeCall);
101
101
  const opts = h.jo(&.{.{ "utility", h.jo(&.{.{ "probe", fn_val }}) }});
102
102
  const client = sdk.SDK.new(opts);
103
- const stored = h.getp(client.utility.custom, "probe");
103
+ const stored = h.getp(client.sdkUtility.custom, "probe");
104
104
  try testing.expect(stored == .function);
105
105
  const r = h.call_vfn(stored, h.vstr("x"));
106
106
  try testing.expect(r == .string and std.mem.eql(u8, r.string, "x"));
@@ -139,10 +139,10 @@ test "gotcha3: many features" {
139
139
 
140
140
  test "gotcha2: make_point surfaces out.point error" {
141
141
  const client = sdk.new();
142
- const ctx = client.utility.make_context(.{ .opname = "list" }, client.get_root_ctx());
142
+ const ctx = client.sdkUtility.make_context(.{ .opname = "list" }, client.get_root_ctx());
143
143
  const e = ctx.make_error("rbac_denied", "denied");
144
144
  ctx.out_set("point", sdk.OutVal{ .err = e });
145
- try testing.expectError(error.Sdk, client.utility.make_point(ctx));
145
+ try testing.expectError(error.Sdk, client.sdkUtility.make_point(ctx));
146
146
  try testing.expect(ctx.pending_err != null);
147
147
  try testing.expect(std.mem.eql(u8, ctx.pending_err.?.code, "rbac_denied"));
148
148
  }
@@ -152,8 +152,8 @@ test "gotcha2: make_point surfaces out.point error" {
152
152
  test "gotcha4: featureAdd before ordering" {
153
153
  const client = sdk.new();
154
154
  const ctx = client.get_root_ctx();
155
- client.utility.feature_add(ctx, OrderFeat.make("aaa", vnull()));
156
- client.utility.feature_add(ctx, OrderFeat.make("bbb", h.jo(&.{.{ "__before__", h.vstr("aaa") }})));
155
+ client.sdkUtility.feature_add(ctx, OrderFeat.make("aaa", vnull()));
156
+ client.sdkUtility.feature_add(ctx, OrderFeat.make("bbb", h.jo(&.{.{ "__before__", h.vstr("aaa") }})));
157
157
  var ai: usize = 999;
158
158
  var bi: usize = 999;
159
159
  for (client.features.items, 0..) |f, i| {
@@ -176,7 +176,7 @@ test "gotcha6: netsim offline error code" {
176
176
  });
177
177
  const client = sdk.SDK.new(opts);
178
178
  const ctx = client.get_root_ctx();
179
- const r = client.utility.fetch(ctx, "http://x", h.jo(&.{.{ "url", h.vstr("http://x") }}));
179
+ const r = client.sdkUtility.fetch(ctx, "http://x", h.jo(&.{.{ "url", h.vstr("http://x") }}));
180
180
  try testing.expectError(error.Sdk, r);
181
181
  try testing.expect(std.mem.eql(u8, ctx.pending_err.?.code, "netsim_offline"));
182
182
  }
@@ -192,7 +192,7 @@ test "gotcha6: netsim conn error code" {
192
192
  });
193
193
  const client = sdk.SDK.new(opts);
194
194
  const ctx = client.get_root_ctx();
195
- const r = client.utility.fetch(ctx, "http://x", h.jo(&.{.{ "url", h.vstr("http://x") }}));
195
+ const r = client.sdkUtility.fetch(ctx, "http://x", h.jo(&.{.{ "url", h.vstr("http://x") }}));
196
196
  try testing.expectError(error.Sdk, r);
197
197
  try testing.expect(std.mem.eql(u8, ctx.pending_err.?.code, "netsim_conn"));
198
198
  }
@@ -215,7 +215,7 @@ test "gotcha6: netsim seeded latency is deterministic" {
215
215
  const ctx = client.get_root_ctx();
216
216
  var i: usize = 0;
217
217
  while (i < 4) : (i += 1) {
218
- _ = client.utility.fetch(ctx, "http://x", h.jo(&.{.{ "url", h.vstr("http://x") }})) catch {};
218
+ _ = client.sdkUtility.fetch(ctx, "http://x", h.jo(&.{.{ "url", h.vstr("http://x") }})) catch {};
219
219
  }
220
220
  return sleep_log.toOwnedSlice() catch &.{};
221
221
  }
@@ -0,0 +1,57 @@
1
+ {
2
+ "sdkgen": {
3
+ "package": 1
4
+ },
5
+ "name": "@voxgig/sdkgen",
6
+ "version": "3.5.0",
7
+ "provides": {
8
+ "target": [
9
+ "c",
10
+ "clojure",
11
+ "cpp",
12
+ "csharp",
13
+ "dart",
14
+ "elixir",
15
+ "go",
16
+ "go-cli",
17
+ "go-mcp",
18
+ "haskell",
19
+ "java",
20
+ "js",
21
+ "kotlin",
22
+ "lean",
23
+ "lua",
24
+ "ocaml",
25
+ "perl",
26
+ "php",
27
+ "py",
28
+ "py-data",
29
+ "rb",
30
+ "rust",
31
+ "scala",
32
+ "seneca-provider",
33
+ "swift",
34
+ "ts",
35
+ "zig"
36
+ ],
37
+ "feature": [
38
+ "audit",
39
+ "cache",
40
+ "clienttrack",
41
+ "debug",
42
+ "idempotency",
43
+ "log",
44
+ "metrics",
45
+ "netsim",
46
+ "paging",
47
+ "proxy",
48
+ "ratelimit",
49
+ "rbac",
50
+ "retry",
51
+ "streaming",
52
+ "telemetry",
53
+ "test",
54
+ "timeout"
55
+ ]
56
+ }
57
+ }
@@ -12,6 +12,40 @@ import type {
12
12
  } from '../types'
13
13
 
14
14
 
15
+ const indexEntry = (name: string) => `@"${name}.aontu"`
16
+
17
+
18
+ // An index line that is an ACTIVE include, and the name it includes — or
19
+ // undefined for a blank line, a comment, or anything else.
20
+ //
21
+ // Parsed rather than compared as a string, because both spellings around an
22
+ // include are legal aontu and mean opposite things:
23
+ //
24
+ // @"go.aontu" -> active, name 'go'
25
+ // @"go.aontu" # pinned -> active, name 'go' (trailing comment)
26
+ // @"go.aontu" -> active, name 'go' (indented)
27
+ // # @"go.aontu" -> NOT active
28
+ //
29
+ // A substring test (what this used to be) reads the commented-out form as
30
+ // present, so `target add go` on a project that had switched the target off
31
+ // by hand appended nothing and reported success while the target stayed
32
+ // absent from the model. A whole-line equality test fixes that but then
33
+ // misses the trailing-comment form, and appends a SECOND active include.
34
+ const INDEX_ENTRY_RE = /^\s*@"([^"]+)\.aontu"\s*(?:#.*)?$/
35
+
36
+ function indexEntryName(line: string): string | undefined {
37
+ const m = line.match(INDEX_ENTRY_RE)
38
+ return null == m ? undefined : m[1]
39
+ }
40
+
41
+
42
+ // Is this name already included by the index?
43
+ function hasIndexEntry(content: string, name: string): boolean {
44
+ return content.split('\n')
45
+ .some((line: string) => indexEntryName(line) === name)
46
+ }
47
+
48
+
15
49
  // Append `@"<name>.aontu"` import lines for each name not already present in
16
50
  // the index content. Checking against the accumulating result (not the
17
51
  // original) means duplicate names in the same call are added at most once.
@@ -19,9 +53,8 @@ function appendIndexEntries(content: string, names: string[]): string {
19
53
  let out = content
20
54
 
21
55
  for (const n of names) {
22
- const entry = `@"${n}.aontu"`
23
- if (!out.includes(entry)) {
24
- out += '\n' + entry
56
+ if (!hasIndexEntry(out, n)) {
57
+ out += '\n' + indexEntry(n)
25
58
  }
26
59
  }
27
60
 
@@ -29,6 +62,25 @@ function appendIndexEntries(content: string, names: string[]): string {
29
62
  }
30
63
 
31
64
 
65
+ // Drop the `@"<name>.aontu"` line for each name — the inverse of
66
+ // appendIndexEntries, matching line-exact for the same reasons.
67
+ //
68
+ // Nothing calls this yet: a `remove` action is the fast-follow this exists
69
+ // for (see docs/design/sdkgen-packages.md), and it is written here beside its
70
+ // inverse so the two cannot drift on how an entry is recognised.
71
+ function removeIndexEntries(content: string, names: string[]): string {
72
+ const drop = new Set(names)
73
+
74
+ return content
75
+ .split('\n')
76
+ .filter((line: string) => {
77
+ const name = indexEntryName(line)
78
+ return undefined === name || !drop.has(name)
79
+ })
80
+ .join('\n')
81
+ }
82
+
83
+
32
84
  const UpdateIndex = cmp(function UpdateIndex(props: any) {
33
85
  Content(appendIndexEntries(props.content, props.names))
34
86
  })
@@ -45,7 +97,20 @@ function parseAddNames(args: any[]): string[] {
45
97
  }
46
98
 
47
99
 
48
- function loadContent(actx: ActionContext, which: string | string[]) {
100
+ // The current index file for each kind, which `UpdateIndex` appends to.
101
+ //
102
+ // `seed` IS THE UPGRADE PATH. A project scaffolded before a kind existed has
103
+ // no `model/<kind>/<kind>-index.aontu` — every project alive today is in
104
+ // exactly that position for `docs` — and reading it unguarded made the FIRST
105
+ // `docs add` in any existing project fail on ENOENT before it wrote anything.
106
+ //
107
+ // Seeded per call rather than defaulted for every kind: a missing
108
+ // `target-index.aontu` in a scaffolded project is a broken project, and
109
+ // quietly recreating it would hide that. A kind the project has never used is
110
+ // a different thing, and only its own action knows which case it is in.
111
+ function loadContent(
112
+ actx: ActionContext, which: string | string[], seed?: Record<string, string>,
113
+ ) {
49
114
  which = Array.isArray(which) ? which : [which]
50
115
 
51
116
  const content: any = {}
@@ -55,8 +120,9 @@ function loadContent(actx: ActionContext, which: string | string[]) {
55
120
 
56
121
  which.map((w: string) => {
57
122
  const indexfile = Path.join(modelfolder, w, w + '-index.aontu')
58
- const indexcontent = fs.readFileSync(indexfile, 'utf8')
59
- content[`${w}_index`] = indexcontent
123
+
124
+ content[`${w}_index`] = (null != seed?.[w] && !fs.existsSync(indexfile)) ?
125
+ seed[w] : fs.readFileSync(indexfile, 'utf8')
60
126
  })
61
127
 
62
128
  return content
@@ -64,9 +130,73 @@ function loadContent(actx: ActionContext, which: string | string[]) {
64
130
 
65
131
 
66
132
 
133
+ // ENSURE THE PROJECT'S OWN MODEL INCLUDES A KIND'S INDEX.
134
+ //
135
+ // `model/sdk.aontu` is written once, by create-sdkgen, and includes the
136
+ // indexes of the kinds that existed then. So a project scaffolded before a
137
+ // kind existed — which is every project alive today, for `docs` — never
138
+ // includes its index, and the item's model file is an orphan: it is on disk,
139
+ // `<kind>-index.aontu` includes it, and NOTHING includes that. `main.kit.docs`
140
+ // is then absent from the compiled model, so `package list`, `package update`
141
+ // and `doctor` cannot see the item at all.
142
+ //
143
+ // Appending one include line is additive and idempotent, and it is the only
144
+ // way an existing project can adopt a new kind without hand-editing. The
145
+ // entry is parsed, not substring-matched, for the reason `hasIndexEntry`
146
+ // documents: a commented-out include means the project switched it OFF, and
147
+ // re-adding it would override that.
148
+ function ensureModelInclude(actx: ActionContext, kind: string): boolean {
149
+ const fs = actx.fs()
150
+ const url = actx.url
151
+
152
+ if (!fs.existsSync(url)) {
153
+ // Said out loud rather than skipped silently: without the include the
154
+ // item is invisible to the next model compile, and a quiet no-op here
155
+ // would look exactly like success.
156
+ actx.log.warn({
157
+ point: 'model-include-absent', kind, file: url,
158
+ note: url + ' not found, so ' + indexEntry(kind + '/' + kind + '-index') +
159
+ ' could not be added — add it by hand, or nothing will see any ' +
160
+ kind + ' item'
161
+ })
162
+ return false
163
+ }
164
+
165
+ const name = kind + '/' + kind + '-index'
166
+ const content = String(fs.readFileSync(url, 'utf8'))
167
+
168
+ if (hasIndexEntry(content, name)) {
169
+ return false
170
+ }
171
+
172
+ if (actx.opts?.dryrun) {
173
+ actx.log.info({
174
+ point: 'model-include-dryrun', kind, file: url, entry: indexEntry(name),
175
+ note: '** DRY RUN ** would add ' + indexEntry(name) + ' to ' + url
176
+ })
177
+ return false
178
+ }
179
+
180
+ fs.writeFileSync(url,
181
+ content + (content.endsWith('\n') ? '' : '\n') +
182
+ indexEntry(name) + '\n')
183
+
184
+ actx.log.info({
185
+ point: 'model-include-added', kind, file: url, entry: indexEntry(name),
186
+ note: url + ': added ' + indexEntry(name) +
187
+ ' — without it the project model never sees any ' + kind + ' item'
188
+ })
189
+
190
+ return true
191
+ }
192
+
193
+
67
194
  export {
68
195
  UpdateIndex,
196
+ ensureModelInclude,
69
197
  appendIndexEntries,
198
+ removeIndexEntries,
199
+ hasIndexEntry,
70
200
  parseAddNames,
71
201
  loadContent
72
202
  }