@voxgig/sdkgen 4.2.8 → 4.4.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 (63) hide show
  1. package/bin/voxgig-sdkgen +1 -1
  2. package/dist/helpers/naming.d.ts +2 -1
  3. package/dist/helpers/naming.js +50 -12
  4. package/dist/helpers/naming.js.map +1 -1
  5. package/dist/sdkgen.d.ts +2 -2
  6. package/dist/sdkgen.js +4 -3
  7. package/dist/sdkgen.js.map +1 -1
  8. package/dist/tsconfig.tsbuildinfo +1 -1
  9. package/package.json +1 -1
  10. package/project/.sdk/src/cmp/zig/Config_zig.ts +1 -1
  11. package/project/.sdk/tm/c/tests/primary_corpus_test.c +565 -0
  12. package/project/.sdk/tm/c/utility/prepare_method.c +3 -1
  13. package/project/.sdk/tm/c/utility/transform_request.c +4 -3
  14. package/project/.sdk/tm/clojure/src/sdk/core.clj +55 -5
  15. package/project/.sdk/tm/clojure/test/sdk/test/primary.clj +171 -1
  16. package/project/.sdk/tm/clojure/test/sdk/test/struct_corpus.clj +13 -2
  17. package/project/.sdk/tm/clojure/test/sdk/test_runner.clj +2 -0
  18. package/project/.sdk/tm/csharp/test/CustomUtilityTest.cs +104 -0
  19. package/project/.sdk/tm/csharp/utility/MakeOptions.cs +82 -2
  20. package/project/.sdk/tm/elixir/lib/projectname/utility.ex +50 -2
  21. package/project/.sdk/tm/elixir/test/primary_utility_test.exs +18 -200
  22. package/project/.sdk/tm/elixir/test/support/struct_corpus.ex +413 -2
  23. package/project/.sdk/tm/go/test/custom_utility_test.go +103 -0
  24. package/project/.sdk/tm/go/test/feature_corpus_test.go +550 -0
  25. package/project/.sdk/tm/go/utility/make_options.go +7 -1
  26. package/project/.sdk/tm/go/utility/register.go +194 -0
  27. package/project/.sdk/tm/java/test/CustomUtilityTest.java +55 -0
  28. package/project/.sdk/tm/java/test/FeatureCorpusTest.java +463 -0
  29. package/project/.sdk/tm/java/utility/MakeOptions.java +11 -2
  30. package/project/.sdk/tm/java/utility/Register.java +91 -0
  31. package/project/.sdk/tm/js/test/feature/Corpus.test.js +285 -0
  32. package/project/.sdk/tm/kotlin/utility/MakeOptions.kt +56 -2
  33. package/project/.sdk/tm/lua/utility/make_options.lua +22 -2
  34. package/project/.sdk/tm/ocaml/Makefile +15 -4
  35. package/project/.sdk/tm/ocaml/sdk_runtime.ml +8 -1
  36. package/project/.sdk/tm/ocaml/test/corpus_runner.ml +185 -0
  37. package/project/.sdk/tm/ocaml/test/primary_utility_test.ml +238 -0
  38. package/project/.sdk/tm/ocaml/test/struct_corpus.ml +5 -160
  39. package/project/.sdk/tm/perl/t/feature_corpus.t +345 -0
  40. package/project/.sdk/tm/perl/utility/make_options.pm +33 -1
  41. package/project/.sdk/tm/php/core/Context.php +3 -0
  42. package/project/.sdk/tm/php/core/Control.php +13 -0
  43. package/project/.sdk/tm/php/core/Error.php +12 -0
  44. package/project/.sdk/tm/php/test/FeatureCorpusTest.php +376 -0
  45. package/project/.sdk/tm/php/utility/MakeOptions.php +30 -1
  46. package/project/.sdk/tm/py/pkg/utility/make_options.py +42 -1
  47. package/project/.sdk/tm/py/test/test_feature_corpus.py +309 -0
  48. package/project/.sdk/tm/rb/test/feature_corpus_test.rb +281 -0
  49. package/project/.sdk/tm/rb/utility/make_options.rb +32 -1
  50. package/project/.sdk/tm/scala/Makefile +1 -0
  51. package/project/.sdk/tm/scala/sdktest/PrimaryCorpus.scala +294 -0
  52. package/project/.sdk/tm/scala/sdktest/StructCorpus.scala +0 -0
  53. package/project/.sdk/tm/scala/utility/Make.scala +49 -2
  54. package/project/.sdk/tm/scala/utility/Prepare.scala +3 -1
  55. package/project/.sdk/tm/swift/Sources/ProjectNameSDK/utility/MakeOptions.swift +21 -3
  56. package/project/.sdk/tm/ts/src/feature/test/TestFeature.ts +13 -2
  57. package/project/.sdk/tm/ts/test/feature/Corpus.test.ts +287 -0
  58. package/project/.sdk/tm/zig/core/utility.zig +8 -1
  59. package/project/.sdk/tm/zig/test/primary_utility_test.zig +445 -0
  60. package/project/.sdk/tm/zig/test/struct_runner.zig +238 -0
  61. package/project/sdkgen-package.json +1 -1
  62. package/src/helpers/naming.ts +54 -12
  63. package/src/sdkgen.ts +2 -1
@@ -0,0 +1,463 @@
1
+ package JAVAPACKAGE.sdktest;
2
+
3
+ // ProjectName SDK feature corpus test
4
+ //
5
+ // Feature behaviour, driven by the SHARED corpus.
6
+ //
7
+ // The same route PrimaryUtilityTest takes for the utilities: language-neutral
8
+ // cases in .sdk/test/test.json, executed against THIS generated SDK. The
9
+ // feature is the ordinary class, built by the generated config, installed by
10
+ // the generated constructor, and driven by a real entity operation. Not a
11
+ // miniature of the pipeline - that is what FeatureHarness does, and a
12
+ // miniature can only be as right as the miniature.
13
+ //
14
+ // Everything in a case is data. The one piece java writes for itself is
15
+ // turning scripted responses into a fetcher, through the documented
16
+ // `utility.fetcher` override.
17
+
18
+ import static org.junit.jupiter.api.Assertions.assertEquals;
19
+ import static org.junit.jupiter.api.Assertions.assertNotNull;
20
+ import static org.junit.jupiter.api.Assertions.assertTrue;
21
+ import static org.junit.jupiter.api.Assertions.fail;
22
+ import static org.junit.jupiter.api.Assumptions.assumeTrue;
23
+
24
+ import java.lang.reflect.Field;
25
+ import java.lang.reflect.Method;
26
+ import java.util.ArrayList;
27
+ import java.util.LinkedHashMap;
28
+ import java.util.List;
29
+ import java.util.Map;
30
+ import java.util.TreeMap;
31
+ import java.util.function.Supplier;
32
+ import java.util.regex.Matcher;
33
+ import java.util.regex.Pattern;
34
+
35
+ import org.junit.jupiter.api.Test;
36
+
37
+ import JAVAPACKAGE.core.Entity;
38
+ import JAVAPACKAGE.core.Feature;
39
+ import JAVAPACKAGE.core.ProjectNameSDK;
40
+ import JAVAPACKAGE.core.SdkError;
41
+ import JAVAPACKAGE.core.Utility;
42
+
43
+ @SuppressWarnings({"unchecked"})
44
+ public class FeatureCorpusTest {
45
+
46
+ // Features with a corpus section. A name here with no section is a skip,
47
+ // not a failure: an SDK generated without the feature has nothing to run.
48
+ private static final List<String> FEATURE_CORPUS_NAMES = List.of("cost");
49
+
50
+ // The standard operation names, in the order the runner prefers them.
51
+ private static final List<String> FEATURE_CORPUS_OPS =
52
+ List.of("load", "list", "create", "update", "remove");
53
+
54
+ /** One discovered operation: the client accessor plus the entity method. */
55
+ private static final class Op {
56
+ final String key;
57
+ final Method accessor;
58
+ final Method call;
59
+
60
+ Op(String key, Method accessor, Method call) {
61
+ this.key = key;
62
+ this.accessor = accessor;
63
+ this.call = call;
64
+ }
65
+ }
66
+
67
+ private static Map<String, Object> corpus() {
68
+ return RunnerSupport.loadTestSpec();
69
+ }
70
+
71
+ /**
72
+ * A scripted transport built from a case's `res` list. Responses are
73
+ * consumed in order and the last one repeats, so a case that does not care
74
+ * how many attempts happen need only declare one.
75
+ *
76
+ * <p>Returns the shape the real fetcher returns: the parsed body behind a
77
+ * `json` supplier and `body` as the raw string. A script that only set
78
+ * `body` would look like an empty result, which reads as a feature defect
79
+ * rather than a mis-shaped script.
80
+ */
81
+ private static Utility.FetcherFn scriptedFetcher(Object resRaw) {
82
+ List<Object> res = resRaw instanceof List ? (List<Object>) resRaw : List.of();
83
+ int[] n = {-1};
84
+ return (ctx, fullurl, fetchdef) -> {
85
+ n[0]++;
86
+ Map<String, Object> spec = new LinkedHashMap<>();
87
+ if (!res.isEmpty()) {
88
+ int i = n[0] >= res.size() ? res.size() - 1 : n[0];
89
+ Object entry = res.get(i);
90
+ if (entry instanceof Map) {
91
+ spec = (Map<String, Object>) entry;
92
+ }
93
+ }
94
+
95
+ if (Boolean.TRUE.equals(spec.get("throw"))) {
96
+ throw new RuntimeException("scripted transport failure");
97
+ }
98
+
99
+ Object statusRaw = spec.get("status");
100
+ int status = statusRaw instanceof Number ? ((Number) statusRaw).intValue() : 200;
101
+ Object body = spec.get("body");
102
+ final Object jsonBody = body == null ? new LinkedHashMap<String, Object>() : body;
103
+
104
+ Map<String, Object> headers = new LinkedHashMap<>();
105
+ if (spec.get("headers") instanceof Map) {
106
+ headers.putAll((Map<String, Object>) spec.get("headers"));
107
+ }
108
+
109
+ Map<String, Object> out = new LinkedHashMap<>();
110
+ out.put("status", status);
111
+ out.put("statusText", status < 400 ? "OK" : "ERR");
112
+ out.put("headers", headers);
113
+ out.put("json", (Supplier<Object>) () -> jsonBody);
114
+ out.put("body", RunnerSupport.jsonStr(jsonBody));
115
+ return out;
116
+ };
117
+ }
118
+
119
+ /**
120
+ * Build a client the way a caller would.
121
+ *
122
+ * <p>The plain constructor, not testSDK: the `test` feature is
123
+ * transport: 'base' and REPLACES the transport, so a client in test mode
124
+ * would shadow the script.
125
+ */
126
+ private static ProjectNameSDK buildClient(Map<String, Object> kase) {
127
+ Map<String, Object> utility = new LinkedHashMap<>();
128
+ utility.put("fetcher", scriptedFetcher(kase.get("res")));
129
+
130
+ Map<String, Object> opts = new LinkedHashMap<>();
131
+ opts.put("utility", utility);
132
+ if (kase.get("feature") != null) {
133
+ opts.put("feature", kase.get("feature"));
134
+ }
135
+ return new ProjectNameSDK(opts);
136
+ }
137
+
138
+ /**
139
+ * Every operation this SDK declares, in a stable order.
140
+ *
141
+ * <p>The corpus cannot name an entity - it is shared by SDKs with none in
142
+ * common - so the runner finds them here. An entity accessor is a client
143
+ * method taking one Map and returning something that answers getName().
144
+ */
145
+ private static List<Op> candidates(ProjectNameSDK client) {
146
+ Map<String, Object[]> found = new TreeMap<>();
147
+
148
+ for (Method m : client.getClass().getMethods()) {
149
+ if (1 != m.getParameterCount() || !Map.class.isAssignableFrom(m.getParameterTypes()[0])) {
150
+ continue;
151
+ }
152
+ if (!Entity.class.isAssignableFrom(m.getReturnType())) {
153
+ continue;
154
+ }
155
+ Object ent;
156
+ String entname;
157
+ try {
158
+ ent = m.invoke(client, new Object[] {null});
159
+ entname = ((Entity) ent).getName();
160
+ }
161
+ catch (Exception e) {
162
+ continue;
163
+ }
164
+ if (null == entname || entname.isEmpty()) {
165
+ continue;
166
+ }
167
+ found.put(entname, new Object[] {m, ent});
168
+ }
169
+
170
+ List<Op> out = new ArrayList<>();
171
+ for (Map.Entry<String, Object[]> e : found.entrySet()) {
172
+ Method accessor = (Method) e.getValue()[0];
173
+ Object ent = e.getValue()[1];
174
+ for (String opname : FEATURE_CORPUS_OPS) {
175
+ Method call;
176
+ try {
177
+ call = ent.getClass().getMethod(opname, Map.class, Map.class);
178
+ }
179
+ catch (NoSuchMethodException ex) {
180
+ continue;
181
+ }
182
+ out.add(new Op(e.getKey() + "." + opname, accessor, call));
183
+ }
184
+ }
185
+ return out;
186
+ }
187
+
188
+ private static Object invoke(ProjectNameSDK client, Op op, Map<String, Object> ctrl)
189
+ throws Exception {
190
+ Object ent = op.accessor.invoke(client, new Object[] {null});
191
+ try {
192
+ return op.call.invoke(ent, new LinkedHashMap<String, Object>(), ctrl);
193
+ }
194
+ catch (java.lang.reflect.InvocationTargetException ite) {
195
+ // Unwrap: a reflective call reports the SDK's error as its cause, and a
196
+ // case asserting an error code needs the error itself.
197
+ Throwable cause = ite.getCause();
198
+ if (cause instanceof Exception) {
199
+ throw (Exception) cause;
200
+ }
201
+ throw ite;
202
+ }
203
+ }
204
+
205
+ /**
206
+ * Pick operations by DRIVING them: an op is usable when it completes against
207
+ * a plain 200 with no feature active. Declared operations are not all
208
+ * callable with no arguments (a required path parameter, a body), and a case
209
+ * failing for that reason would read as a feature defect.
210
+ */
211
+ private static List<Op> usableOps(int want) {
212
+ List<Op> picked = new ArrayList<>();
213
+ for (Op cand : candidates(buildClient(Map.of()))) {
214
+ try {
215
+ invoke(buildClient(Map.of()), cand, new LinkedHashMap<>());
216
+ }
217
+ catch (Exception e) {
218
+ continue;
219
+ }
220
+ picked.add(cand);
221
+ if (picked.size() >= want) {
222
+ break;
223
+ }
224
+ }
225
+ return picked;
226
+ }
227
+
228
+ /** Replace #OPn throughout a case, keys included. */
229
+ private static Object resolve(Object node, Map<String, String> tokens) {
230
+ if (node instanceof String) {
231
+ String out = (String) node;
232
+ for (Map.Entry<String, String> t : tokens.entrySet()) {
233
+ out = out.replace(t.getKey(), t.getValue());
234
+ }
235
+ return out;
236
+ }
237
+ if (node instanceof List) {
238
+ List<Object> out = new ArrayList<>();
239
+ for (Object n : (List<Object>) node) {
240
+ out.add(resolve(n, tokens));
241
+ }
242
+ return out;
243
+ }
244
+ if (node instanceof Map) {
245
+ Map<String, Object> out = new LinkedHashMap<>();
246
+ for (Map.Entry<String, Object> e : ((Map<String, Object>) node).entrySet()) {
247
+ out.put((String) resolve(e.getKey(), tokens), resolve(e.getValue(), tokens));
248
+ }
249
+ return out;
250
+ }
251
+ return node;
252
+ }
253
+
254
+ /** The highest #OPn a case mentions. */
255
+ private static int tokensUsed(Object kase) {
256
+ Matcher m = Pattern.compile("#OP(\\d+)").matcher(RunnerSupport.jsonStr(kase));
257
+ int max = 0;
258
+ while (m.find()) {
259
+ max = Math.max(max, Integer.parseInt(m.group(1)));
260
+ }
261
+ return max;
262
+ }
263
+
264
+ /**
265
+ * One named member of the record, whether the record is a map or a POJO.
266
+ *
267
+ * <p>The aggregates are typed classes here and plain maps in the dynamic
268
+ * donors, so the corpus - which knows only field names - reaches both
269
+ * through a public field or a map key.
270
+ */
271
+ private static Object[] member(Object actual, String key) {
272
+ if (null == actual) {
273
+ return new Object[] {null, false};
274
+ }
275
+ if (actual instanceof Map) {
276
+ Map<String, Object> m = (Map<String, Object>) actual;
277
+ return m.containsKey(key)
278
+ ? new Object[] {m.get(key), true}
279
+ : new Object[] {null, false};
280
+ }
281
+ try {
282
+ Field f = actual.getClass().getField(key);
283
+ return new Object[] {f.get(actual), true};
284
+ }
285
+ catch (Exception e) {
286
+ return new Object[] {null, false};
287
+ }
288
+ }
289
+
290
+ /**
291
+ * Assert that `actual` contains `expect`, recursively. Cases assert only the
292
+ * fields they are about, so a full equality check would force every case to
293
+ * restate the whole record.
294
+ */
295
+ private void subset(Object actual, Object expect, String path) {
296
+ if (expect instanceof Map) {
297
+ for (Map.Entry<String, Object> e : ((Map<String, Object>) expect).entrySet()) {
298
+ Object[] got = member(actual, e.getKey());
299
+ assertTrue((Boolean) got[1], path + "." + e.getKey() + ": no such member");
300
+ subset(got[0], e.getValue(), path + "." + e.getKey());
301
+ }
302
+ return;
303
+ }
304
+
305
+ if (expect instanceof Boolean) {
306
+ assertEquals(expect, actual, path);
307
+ return;
308
+ }
309
+
310
+ if (expect instanceof Number) {
311
+ assertTrue(actual instanceof Number, path + ": expected a number, got " + actual);
312
+ // Money is float arithmetic; compare with a tolerance far below any
313
+ // amount a case states.
314
+ assertEquals(((Number) expect).doubleValue(), ((Number) actual).doubleValue(), 1e-9, path);
315
+ return;
316
+ }
317
+
318
+ assertEquals(expect, actual, path);
319
+ }
320
+
321
+ /**
322
+ * The feature's own record, found by NAME.
323
+ *
324
+ * <p>Named, not typed: a project that trimmed the feature has no such class,
325
+ * and a runner that referred to one would not compile there.
326
+ */
327
+ private static Object record(ProjectNameSDK client, String name) {
328
+ for (Feature f : client.features) {
329
+ if (name.equals(f.getName())) {
330
+ return f;
331
+ }
332
+ }
333
+ return null;
334
+ }
335
+
336
+ @Test
337
+ public void corpusCarriesAFeatureSection() {
338
+ // A corpus with no `feature` section is a SKIP, not a failure. Each
339
+ // project carries its OWN materialised copy of .sdk/test/test.json, so a
340
+ // project scaffolded before the section existed legitimately has no cases
341
+ // to run - and a hard assertion here turned that into a red suite in every
342
+ // SDK on the fleet, for a corpus the project had simply not re-pulled yet.
343
+ // The strict check belongs where the corpus is CONTROLLED: sdkgen's own
344
+ // end-to-end lane supplies one and requires the cases to actually run.
345
+ assumeTrue(null != corpus().get("feature"),
346
+ "this project's test.json has no `feature` section - recompile the "
347
+ + "corpus (create-sdkgen .sdk/test/feature/) to run these cases");
348
+ assertNotNull(corpus().get("feature"));
349
+ }
350
+
351
+ // At least one operation, or every case below would skip and this suite
352
+ // would report green having run nothing.
353
+ @Test
354
+ public void sdkHasAnOperationTheCorpusCanDrive() {
355
+ assertTrue(0 < usableOps(2).size(),
356
+ "no declared operation completed against a plain 200 - the corpus "
357
+ + "cannot exercise a feature without one");
358
+ }
359
+
360
+ @Test
361
+ public void featureCorpus() throws Exception {
362
+ Map<String, Object> features =
363
+ (Map<String, Object>) corpus().getOrDefault("feature", Map.of());
364
+
365
+ // Skip rather than run vacuously: with no section this asserts nothing,
366
+ // and a test that passes having checked nothing is the false green the
367
+ // corpus exists to prevent.
368
+ assumeTrue(!features.isEmpty(),
369
+ "this project's test.json has no `feature` section - recompile the "
370
+ + "corpus (create-sdkgen .sdk/test/feature/) to run these cases");
371
+
372
+ for (String name : FEATURE_CORPUS_NAMES) {
373
+ Object sectionRaw = features.get(name);
374
+ if (!(sectionRaw instanceof Map)) {
375
+ continue;
376
+ }
377
+ Map<String, Object> section = (Map<String, Object>) sectionRaw;
378
+
379
+ Object basic = section.get("basic");
380
+ List<Object> cases = basic instanceof Map
381
+ ? (List<Object>) ((Map<String, Object>) basic).getOrDefault("set", List.of())
382
+ : List.of();
383
+ assertTrue(0 < cases.size(),
384
+ "corpus section feature." + name + " ran ZERO cases - a renamed "
385
+ + "section or an emptied fixture must fail loudly");
386
+
387
+ // Probed by ACTIVATING it: the feature defaults to inactive, so an idle
388
+ // client never builds it and its absence says nothing.
389
+ ProjectNameSDK probe = buildClient(Map.of("feature",
390
+ List.of(Map.of("name", name, "active", true))));
391
+ if (null == record(probe, name)) {
392
+ continue;
393
+ }
394
+
395
+ List<Op> ops = usableOps(2);
396
+ Map<String, Op> byKey = new LinkedHashMap<>();
397
+ for (Op o : ops) {
398
+ byKey.put(o.key, o);
399
+ }
400
+
401
+ int ran = 0;
402
+ for (Object rawCase : cases) {
403
+ int need = tokensUsed(rawCase);
404
+ if (need > ops.size()) {
405
+ continue;
406
+ }
407
+
408
+ Map<String, String> tokens = new LinkedHashMap<>();
409
+ for (int i = 0; i < need; i++) {
410
+ tokens.put("#OP" + (i + 1), ops.get(i).key);
411
+ }
412
+ Map<String, Object> kase = (Map<String, Object>) resolve(rawCase, tokens);
413
+
414
+ ProjectNameSDK client = buildClient(kase);
415
+ String label = String.valueOf(kase.get("name"));
416
+
417
+ List<Object> steps = kase.get("op") instanceof List
418
+ ? (List<Object>) kase.get("op") : List.of();
419
+ for (Object stepRaw : steps) {
420
+ Map<String, Object> step = (Map<String, Object>) stepRaw;
421
+ Op op = byKey.get(step.get("op"));
422
+ assertNotNull(op, label + ": no operation " + step.get("op"));
423
+ Map<String, Object> ctrl = step.get("ctrl") instanceof Map
424
+ ? new LinkedHashMap<>((Map<String, Object>) step.get("ctrl"))
425
+ : new LinkedHashMap<>();
426
+ Object wanterr = step.get("err");
427
+
428
+ try {
429
+ invoke(client, op, ctrl);
430
+ if (null != wanterr) {
431
+ fail(label + ": " + step.get("op") + " was expected to fail, and did not");
432
+ }
433
+ }
434
+ catch (org.opentest4j.AssertionFailedError e) {
435
+ throw e;
436
+ }
437
+ catch (Exception err) {
438
+ assertNotNull(wanterr,
439
+ label + ": " + step.get("op") + " failed unexpectedly: " + err);
440
+ if (wanterr instanceof String) {
441
+ // The CODE, not the message: makeError prefixes and humanises
442
+ // the text, so matching it would pass on any error that
443
+ // happened to mention the word.
444
+ String code = err instanceof SdkError ? ((SdkError) err).code : null;
445
+ assertEquals(wanterr, code, label + ": wrong error code (" + err + ")");
446
+ }
447
+ }
448
+ }
449
+
450
+ subset(record(client, name), kase.get("out"), label + ": _" + name);
451
+ ran++;
452
+ }
453
+
454
+ assertTrue(0 < ran, "every feature." + name + " case was skipped");
455
+ // Say how many ran. A partial run is legitimate (an SDK with one
456
+ // operation skips the cases needing two) but it should be visible
457
+ // rather than inferred from a green tick.
458
+ System.err.println(String.format(
459
+ "feature.%s: ran %d of %d case(s) against %d operation(s)",
460
+ name, ran, cases.size(), ops.size()));
461
+ }
462
+ }
463
+ }
@@ -20,11 +20,20 @@ final class MakeOptions {
20
20
  options = new LinkedHashMap<>();
21
21
  }
22
22
 
23
- // Merge custom utility overrides onto the utility object.
23
+ // Merge utility overrides from options onto the utility object.
24
24
  // Read from original options before clone for parity with the donors.
25
+ //
26
+ // A key naming a real utility member REPLACES it (Register.overrideUtil);
27
+ // anything else is attached as a custom extra. Shelving everything in
28
+ // `custom` - a map nothing reads - made `utility: {"fetcher": ...}`, the
29
+ // documented transport seam, a silent no-op here while ts honoured it.
25
30
  Map<String, Object> customUtils = Helpers.toMapAny(options.get("utility"));
26
31
  if (customUtils != null && ctx.utility != null) {
27
- ctx.utility.custom.putAll(customUtils);
32
+ for (Map.Entry<String, Object> cu : customUtils.entrySet()) {
33
+ if (!Register.overrideUtil(ctx.utility, cu.getKey(), cu.getValue())) {
34
+ ctx.utility.custom.put(cu.getKey(), cu.getValue());
35
+ }
36
+ }
28
37
  }
29
38
 
30
39
  Map<String, Object> opts = (Map<String, Object>) Struct.clone(options);
@@ -40,4 +40,95 @@ public final class Register {
40
40
  u.transformRequest = TransformRequest::transformRequest;
41
41
  u.transformResponse = TransformResponse::transformResponse;
42
42
  }
43
+
44
+ /**
45
+ * Replaces one utility member from {@code options.utility}, matching the ts
46
+ * reference: a key naming a real utility member REPLACES it, and any other
47
+ * key is attached as a custom extra.
48
+ *
49
+ * <p>Without this the override was a no-op here. {@code makeOptions} put
50
+ * every entry in {@code u.custom}, which nothing reads, so a caller passing
51
+ * {@code utility: {"fetcher": myTransport}} - the documented way to script
52
+ * the transport, and the seam the shared feature corpus runs on - was
53
+ * silently ignored while ts and js honoured it. The custom-utility test did
54
+ * not catch it because it asserted the side map rather than the behaviour.
55
+ *
56
+ * <p>Returns false when the key names no member, or when the value is not
57
+ * that member's interface; the caller then keeps it in {@code custom}. A
58
+ * wrong shape is deliberately NOT an error: ts attaches whatever it is
59
+ * given, so a typed port that rejected it outright would diverge in the
60
+ * other direction.
61
+ *
62
+ * <p>Erasure limits how much of a shape java can check: the {@code CtxFn}
63
+ * members all erase to the same interface, so the key decides which field a
64
+ * {@code CtxFn} lands on and the cast is unchecked. That is the same
65
+ * guarantee the dynamic donors give.
66
+ *
67
+ * <p>Keep this list in step with registerAll above - a utility added to one
68
+ * and not the other is overridable in ts and not here, which is the
69
+ * divergence this exists to remove.
70
+ */
71
+ @SuppressWarnings({"unchecked"})
72
+ public static boolean overrideUtil(Utility u, String key, Object val) {
73
+ switch (key) {
74
+ case "clean":
75
+ if (val instanceof Utility.CleanFn) { u.clean = (Utility.CleanFn) val; return true; }
76
+ return false;
77
+ case "makeError":
78
+ if (val instanceof Utility.MakeErrorFn) { u.makeError = (Utility.MakeErrorFn) val; return true; }
79
+ return false;
80
+ case "featureAdd":
81
+ if (val instanceof Utility.FeatureFn) { u.featureAdd = (Utility.FeatureFn) val; return true; }
82
+ return false;
83
+ case "featureHook":
84
+ if (val instanceof Utility.HookFn) { u.featureHook = (Utility.HookFn) val; return true; }
85
+ return false;
86
+ case "featureInit":
87
+ if (val instanceof Utility.FeatureFn) { u.featureInit = (Utility.FeatureFn) val; return true; }
88
+ return false;
89
+ case "fetcher":
90
+ if (val instanceof Utility.FetcherFn) { u.fetcher = (Utility.FetcherFn) val; return true; }
91
+ return false;
92
+ case "makeContext":
93
+ if (val instanceof Utility.MakeContextFn) { u.makeContext = (Utility.MakeContextFn) val; return true; }
94
+ return false;
95
+ case "param":
96
+ if (val instanceof Utility.ParamFn) { u.param = (Utility.ParamFn) val; return true; }
97
+ return false;
98
+ default:
99
+ break;
100
+ }
101
+
102
+ if (!(val instanceof Utility.CtxFn)) {
103
+ return false;
104
+ }
105
+ Utility.CtxFn<Object> fn = (Utility.CtxFn<Object>) val;
106
+
107
+ switch (key) {
108
+ case "done": u.done = fn; return true;
109
+ case "makeFetchDef": u.makeFetchDef = (Utility.CtxFn) fn; return true;
110
+ case "makeOptions": u.makeOptions = (Utility.CtxFn) fn; return true;
111
+ case "makeRequest": u.makeRequest = (Utility.CtxFn) fn; return true;
112
+ case "makeResponse": u.makeResponse = (Utility.CtxFn) fn; return true;
113
+ case "makeResult": u.makeResult = (Utility.CtxFn) fn; return true;
114
+ case "makePoint": u.makePoint = (Utility.CtxFn) fn; return true;
115
+ case "makeSpec": u.makeSpec = (Utility.CtxFn) fn; return true;
116
+ case "makeUrl": u.makeUrl = (Utility.CtxFn) fn; return true;
117
+ case "prepareAuth": u.prepareAuth = (Utility.CtxFn) fn; return true;
118
+ case "prepareBody": u.prepareBody = fn; return true;
119
+ case "prepareHeaders": u.prepareHeaders = (Utility.CtxFn) fn; return true;
120
+ case "prepareMethod": u.prepareMethod = (Utility.CtxFn) fn; return true;
121
+ case "prepareParams": u.prepareParams = (Utility.CtxFn) fn; return true;
122
+ case "preparePath": u.preparePath = (Utility.CtxFn) fn; return true;
123
+ case "prepareQuery": u.prepareQuery = (Utility.CtxFn) fn; return true;
124
+ case "graphqlBody": u.graphqlBody = fn; return true;
125
+ case "graphqlErrors": u.graphqlErrors = (Utility.CtxFn) fn; return true;
126
+ case "resultBasic": u.resultBasic = (Utility.CtxFn) fn; return true;
127
+ case "resultBody": u.resultBody = (Utility.CtxFn) fn; return true;
128
+ case "resultHeaders": u.resultHeaders = (Utility.CtxFn) fn; return true;
129
+ case "transformRequest": u.transformRequest = fn; return true;
130
+ case "transformResponse": u.transformResponse = fn; return true;
131
+ default: return false;
132
+ }
133
+ }
43
134
  }