@voxgig/sdkgen 4.23.0 → 4.24.1

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 (163) hide show
  1. package/bin/voxgig-sdkgen +18 -2
  2. package/dist/action/doctor.d.ts +1 -1
  3. package/dist/action/doctor.js +12 -10
  4. package/dist/action/doctor.js.map +1 -1
  5. package/dist/action/edition.js +4 -0
  6. package/dist/action/edition.js.map +1 -1
  7. package/dist/action/feature.js +5 -1
  8. package/dist/action/feature.js.map +1 -1
  9. package/dist/action/remove.d.ts +16 -0
  10. package/dist/action/remove.js +345 -0
  11. package/dist/action/remove.js.map +1 -0
  12. package/dist/action/target.js +6 -1
  13. package/dist/action/target.js.map +1 -1
  14. package/dist/cmp/Deploy.js +9 -6
  15. package/dist/cmp/Deploy.js.map +1 -1
  16. package/dist/helpers/semver.js +3 -1
  17. package/dist/helpers/semver.js.map +1 -1
  18. package/dist/tsconfig.tsbuildinfo +1 -1
  19. package/package.json +2 -2
  20. package/project/.sdk/tm/c/core/context.c +3 -1
  21. package/project/.sdk/tm/c/feature/paging.c +37 -3
  22. package/project/.sdk/tm/c/feature/test.c +51 -27
  23. package/project/.sdk/tm/c/utility/prepare_query.c +2 -1
  24. package/project/.sdk/tm/c/utility/transform_request.c +25 -3
  25. package/project/.sdk/tm/clojure/src/sdk/core.clj +21 -7
  26. package/project/.sdk/tm/clojure/src/sdk/features.clj +68 -18
  27. package/project/.sdk/tm/cpp/core/types.hpp +1 -1
  28. package/project/.sdk/tm/cpp/feature/paging.hpp +36 -3
  29. package/project/.sdk/tm/cpp/feature/test.hpp +41 -13
  30. package/project/.sdk/tm/cpp/utility/pipeline.hpp +16 -4
  31. package/project/.sdk/tm/csharp/core/Context.cs +1 -1
  32. package/project/.sdk/tm/csharp/feature/PagingFeature.cs +58 -4
  33. package/project/.sdk/tm/csharp/feature/TestFeature.cs +13 -21
  34. package/project/.sdk/tm/csharp/utility/Fetcher.cs +19 -12
  35. package/project/.sdk/tm/csharp/utility/PrepareQuery.cs +1 -1
  36. package/project/.sdk/tm/csharp/utility/TransformRequest.cs +23 -3
  37. package/project/.sdk/tm/elixir/lib/projectname/context.ex +1 -1
  38. package/project/.sdk/tm/elixir/lib/projectname/feature/paging.ex +52 -10
  39. package/project/.sdk/tm/elixir/lib/projectname/feature/test.ex +47 -17
  40. package/project/.sdk/tm/elixir/lib/projectname/utility.ex +30 -9
  41. package/project/.sdk/tm/go/core/context.go +13 -13
  42. package/project/.sdk/tm/go/feature/paging_feature.go +40 -2
  43. package/project/.sdk/tm/go/feature/test_feature.go +3 -15
  44. package/project/.sdk/tm/go/test/feature_corpus_test.go +1 -0
  45. package/project/.sdk/tm/go/test/feature_test.go +54 -0
  46. package/project/.sdk/tm/go/test/pipeline_test.go +163 -57
  47. package/project/.sdk/tm/go/test/primary_utility_test.go +53 -0
  48. package/project/.sdk/tm/go/utility/fetcher.go +41 -20
  49. package/project/.sdk/tm/go/utility/prepare_query.go +1 -1
  50. package/project/.sdk/tm/go/utility/transform_request.go +22 -3
  51. package/project/.sdk/tm/java/core/Context.java +2 -1
  52. package/project/.sdk/tm/java/feature/PagingFeature.java +46 -4
  53. package/project/.sdk/tm/java/feature/TestFeature.java +10 -13
  54. package/project/.sdk/tm/java/test/FeatureCorpusTest.java +3 -0
  55. package/project/.sdk/tm/java/test/FeatureTest.java +42 -0
  56. package/project/.sdk/tm/java/test/PipelineTest.java +141 -48
  57. package/project/.sdk/tm/java/test/PrimaryUtilityTest.java +67 -6
  58. package/project/.sdk/tm/java/utility/Fetcher.java +36 -28
  59. package/project/.sdk/tm/java/utility/PrepareQuery.java +1 -1
  60. package/project/.sdk/tm/java/utility/TransformRequest.java +18 -3
  61. package/project/.sdk/tm/js/src/Context.js +7 -2
  62. package/project/.sdk/tm/js/src/feature/paging/PagingFeature.js +24 -6
  63. package/project/.sdk/tm/js/src/feature/test/TestFeature.js +7 -5
  64. package/project/.sdk/tm/js/src/utility/PrepareQueryUtility.js +1 -1
  65. package/project/.sdk/tm/js/test/feature/Corpus.test.js +4 -0
  66. package/project/.sdk/tm/js/test/pipeline.test.js +88 -22
  67. package/project/.sdk/tm/js/test/utility/PrimaryUtility.test.js +47 -1
  68. package/project/.sdk/tm/kotlin/core/Context.kt +1 -1
  69. package/project/.sdk/tm/kotlin/feature/PagingFeature.kt +36 -4
  70. package/project/.sdk/tm/kotlin/feature/TestFeature.kt +11 -14
  71. package/project/.sdk/tm/kotlin/utility/Fetcher.kt +33 -24
  72. package/project/.sdk/tm/kotlin/utility/Prepare.kt +1 -1
  73. package/project/.sdk/tm/kotlin/utility/ResultTransform.kt +18 -3
  74. package/project/.sdk/tm/lua/core/context.lua +8 -1
  75. package/project/.sdk/tm/lua/core/control.lua +3 -0
  76. package/project/.sdk/tm/lua/feature/paging_feature.lua +44 -5
  77. package/project/.sdk/tm/lua/feature/test_feature.lua +32 -14
  78. package/project/.sdk/tm/lua/test/omni.lua +8 -1
  79. package/project/.sdk/tm/lua/test/pipeline_test.lua +107 -28
  80. package/project/.sdk/tm/lua/test/primary_utility_test.lua +83 -3
  81. package/project/.sdk/tm/lua/utility/prepare_query.lua +2 -1
  82. package/project/.sdk/tm/lua/utility/transform_request.lua +19 -3
  83. package/project/.sdk/tm/ocaml/sdk_features.ml +72 -21
  84. package/project/.sdk/tm/ocaml/sdk_runtime.ml +23 -8
  85. package/project/.sdk/tm/perl/core/context.pm +2 -1
  86. package/project/.sdk/tm/perl/feature/paging_feature.pm +31 -5
  87. package/project/.sdk/tm/perl/feature/test_feature.pm +46 -16
  88. package/project/.sdk/tm/perl/feature/timeout_feature.pm +17 -13
  89. package/project/.sdk/tm/perl/t/feature_corpus.t +14 -1
  90. package/project/.sdk/tm/perl/t/omni.pm +7 -1
  91. package/project/.sdk/tm/perl/t/pipeline.t +124 -27
  92. package/project/.sdk/tm/perl/t/primary_utility.t +73 -0
  93. package/project/.sdk/tm/perl/utility/fetcher.pm +15 -1
  94. package/project/.sdk/tm/perl/utility/prepare_query.pm +1 -0
  95. package/project/.sdk/tm/perl/utility/transform_request.pm +13 -3
  96. package/project/.sdk/tm/php/core/Context.php +5 -1
  97. package/project/.sdk/tm/php/core/Control.php +5 -0
  98. package/project/.sdk/tm/php/feature/PagingFeature.php +26 -5
  99. package/project/.sdk/tm/php/feature/TestFeature.php +27 -42
  100. package/project/.sdk/tm/php/test/FeatureCorpusTest.php +17 -1
  101. package/project/.sdk/tm/php/test/FeatureTest.php +44 -0
  102. package/project/.sdk/tm/php/test/PipelineTest.php +143 -26
  103. package/project/.sdk/tm/php/test/PrimaryUtilityTest.php +80 -2
  104. package/project/.sdk/tm/php/utility/Fetcher.php +17 -3
  105. package/project/.sdk/tm/php/utility/PrepareQuery.php +1 -1
  106. package/project/.sdk/tm/php/utility/TransformRequest.php +15 -3
  107. package/project/.sdk/tm/py/pkg/core/context.py +2 -1
  108. package/project/.sdk/tm/py/pkg/feature/paging_feature.py +29 -4
  109. package/project/.sdk/tm/py/pkg/feature/test_feature.py +29 -12
  110. package/project/.sdk/tm/py/pkg/utility/fetcher.py +63 -65
  111. package/project/.sdk/tm/py/pkg/utility/prepare_query.py +2 -1
  112. package/project/.sdk/tm/py/pkg/utility/transform_request.py +11 -3
  113. package/project/.sdk/tm/py/test/test_feature.py +27 -0
  114. package/project/.sdk/tm/py/test/test_feature_corpus.py +11 -1
  115. package/project/.sdk/tm/py/test/test_pipeline.py +108 -31
  116. package/project/.sdk/tm/py/test/test_primary_utility.py +76 -1
  117. package/project/.sdk/tm/rb/core/context.rb +1 -1
  118. package/project/.sdk/tm/rb/feature/paging_feature.rb +27 -5
  119. package/project/.sdk/tm/rb/feature/test_feature.rb +33 -7
  120. package/project/.sdk/tm/rb/test/feature_corpus_test.rb +12 -1
  121. package/project/.sdk/tm/rb/test/feature_test.rb +38 -0
  122. package/project/.sdk/tm/rb/test/pipeline_test.rb +100 -24
  123. package/project/.sdk/tm/rb/test/primary_utility_test.rb +59 -1
  124. package/project/.sdk/tm/rb/utility/fetcher.rb +49 -3
  125. package/project/.sdk/tm/rb/utility/prepare_query.rb +1 -1
  126. package/project/.sdk/tm/rb/utility/transform_request.rb +10 -3
  127. package/project/.sdk/tm/rust/core/context.rs +2 -6
  128. package/project/.sdk/tm/rust/feature/paging.rs +44 -3
  129. package/project/.sdk/tm/rust/feature/secrets.rs +1 -1
  130. package/project/.sdk/tm/rust/feature/test.rs +66 -33
  131. package/project/.sdk/tm/rust/utility/fetcher.rs +41 -14
  132. package/project/.sdk/tm/rust/utility/prepare_query.rs +1 -1
  133. package/project/.sdk/tm/rust/utility/transform_request.rs +31 -5
  134. package/project/.sdk/tm/scala/core/Context.scala +2 -1
  135. package/project/.sdk/tm/scala/feature/PagingFeature.scala +41 -4
  136. package/project/.sdk/tm/scala/feature/TestFeature.scala +11 -12
  137. package/project/.sdk/tm/scala/utility/Fetcher.scala +34 -13
  138. package/project/.sdk/tm/scala/utility/Prepare.scala +1 -1
  139. package/project/.sdk/tm/scala/utility/Transform.scala +19 -3
  140. package/project/.sdk/tm/swift/Sources/ProjectNameSDK/core/Context.swift +1 -1
  141. package/project/.sdk/tm/swift/Sources/ProjectNameSDK/feature/PagingFeature.swift +31 -4
  142. package/project/.sdk/tm/swift/Sources/ProjectNameSDK/feature/TestFeature.swift +12 -9
  143. package/project/.sdk/tm/swift/Sources/ProjectNameSDK/utility/Fetcher.swift +22 -3
  144. package/project/.sdk/tm/swift/Sources/ProjectNameSDK/utility/Prepare.swift +1 -1
  145. package/project/.sdk/tm/swift/Sources/ProjectNameSDK/utility/ResultUtil.swift +14 -3
  146. package/project/.sdk/tm/ts/src/Context.ts +7 -2
  147. package/project/.sdk/tm/ts/src/feature/paging/PagingFeature.ts +19 -3
  148. package/project/.sdk/tm/ts/src/feature/test/TestFeature.ts +3 -0
  149. package/project/.sdk/tm/ts/src/utility/PrepareQueryUtility.ts +1 -1
  150. package/project/.sdk/tm/ts/test/feature/Corpus.test.ts +1 -0
  151. package/project/.sdk/tm/ts/test/pipeline.test.ts +52 -11
  152. package/project/.sdk/tm/zig/core/context.zig +1 -1
  153. package/project/.sdk/tm/zig/core/utility.zig +19 -5
  154. package/project/.sdk/tm/zig/feature/paging.zig +34 -4
  155. package/project/.sdk/tm/zig/feature/test.zig +54 -26
  156. package/project/sdkgen-package.json +1 -1
  157. package/src/action/doctor.ts +12 -9
  158. package/src/action/edition.ts +8 -0
  159. package/src/action/feature.ts +7 -1
  160. package/src/action/remove.ts +492 -0
  161. package/src/action/target.ts +8 -1
  162. package/src/cmp/Deploy.ts +11 -6
  163. package/src/helpers/semver.ts +3 -1
@@ -228,19 +228,12 @@ public class TestFeature extends BaseFeature {
228
228
  Object args = buildArgs(ctx, op, updateMatch);
229
229
  List<Object> found = Struct.select(entmap, args);
230
230
  Object ent = Struct.getelem(found, 0);
231
- if (ent == null && entmap != null) {
232
- for (Object e : entmap.values()) {
233
- if (e instanceof Map) {
234
- ent = e;
235
- break;
236
- }
237
- }
238
- }
239
231
  if (ent == null) {
232
+ // update miss: 404, never another record
240
233
  return respond(ctx, 404, null, extra("statusText", "Not found"));
241
234
  }
242
235
  if (ent instanceof Map && ctx.reqdata != null) {
243
- ((Map<String, Object>) ent).putAll(ctx.reqdata);
236
+ Struct.merge(Struct.jt(ent, ctx.reqdata));
244
237
  }
245
238
  Struct.delprop(ent, "$KEY");
246
239
  Object out = Struct.clone(ent);
@@ -374,10 +367,14 @@ public class TestFeature extends BaseFeature {
374
367
  }
375
368
  }
376
369
 
377
- // Get required params.
378
- Object paramsPath = Struct.getpath(point, List.of("args", "params"));
379
- Object reqdParams = Struct.select(paramsPath, Struct.jm("reqd", true));
380
- Object reqd = Struct.transform(reqdParams,
370
+ // Path AND query: a path-only read misses a query-addressed record
371
+ // (e.g. GET /result?trace_id=), which has no path param at all.
372
+ List<Object> reqdArgs = Struct.jt();
373
+ for (String kind : new String[] { "params", "query" }) {
374
+ Object argsPath = Struct.getpath(point, List.of("args", kind));
375
+ reqdArgs.addAll(Struct.select(argsPath, Struct.jm("reqd", true)));
376
+ }
377
+ Object reqd = Struct.transform(reqdArgs,
381
378
  Struct.jt("`$EACH`", "", "`$KEY.name`"));
382
379
 
383
380
  List<Object> qand = Struct.jt();
@@ -405,6 +405,9 @@ public class FeatureCorpusTest {
405
405
  ProjectNameSDK probe = buildClient(Map.of("feature",
406
406
  List.of(Map.of("name", name, "active", true))));
407
407
  if (null == record(probe, name)) {
408
+ // The one line every runner prints for an inert section.
409
+ System.err.println(String.format(
410
+ "feature.%s: inert (this SDK does not generate the feature)", name));
408
411
  continue;
409
412
  }
410
413
 
@@ -867,6 +867,48 @@ public class FeatureTest {
867
867
  assertEquals(true, res.result.paging.get("hasMore"), "expected hasMore");
868
868
  }
869
869
 
870
+ @Test
871
+ public void paging_snakeCaseSignalsAndCtrlWriteBack() {
872
+ assumeFeatures("paging");
873
+ FhRecorder rec = new FhRecorder();
874
+ rec.reply = (n, fetchdef) -> 1 == n
875
+ ? fhResponse(200, fhMap("has_more", true, "next_cursor", "c2"), null)
876
+ : fhResponse(200, fhMap("has_more", false), null);
877
+ FhHarness h = fhMake(rec::fetch, fhF(new PagingFeature(), null));
878
+ Map<String, Object> pg = new LinkedHashMap<>();
879
+ Map<String, Object> ctrl = fhMap("paging", pg);
880
+ FhOpResult res = h.op(fhOp("list").path("/w").ctrl(ctrl));
881
+ assertEquals(true, res.result.paging.get("hasMore"), "expected has_more read");
882
+ assertEquals("c2", res.result.paging.get("cursor"), "expected next_cursor read");
883
+ assertEquals("c2", pg.get("cursor"), "expected record written back into ctrl");
884
+ assertEquals(true, pg.get("hasMore"), "expected record written back into ctrl");
885
+ h.op(fhOp("list").path("/w").ctrl(ctrl));
886
+ assertTrue(rec.url(1).contains("cursor=c2"),
887
+ "expected written-back cursor sent: " + rec.url(1));
888
+ assertEquals(false, pg.get("hasMore"), "expected ctrl to carry the last record");
889
+ assertNull(pg.get("cursor"), "expected ctrl to carry the last record");
890
+ }
891
+
892
+ @Test
893
+ public void paging_continuesFromWrittenBackNextPage() {
894
+ assumeFeatures("paging");
895
+ FhRecorder rec = new FhRecorder();
896
+ rec.reply = (n, fetchdef) -> fhResponse(200,
897
+ 1 == n ? fhMap("next_page", 2) : fhMap(),
898
+ fhMap("x-page", String.valueOf(n)));
899
+ FhHarness h = fhMake(rec::fetch, fhF(new PagingFeature(), null));
900
+ Map<String, Object> pg = new LinkedHashMap<>();
901
+ Map<String, Object> ctrl = fhMap("paging", pg);
902
+ h.op(fhOp("list").path("/w").ctrl(ctrl));
903
+ assertEquals(1, pg.get("page"), "expected x-page written back");
904
+ assertEquals(2, pg.get("nextPage"), "expected next_page written back");
905
+ assertEquals(true, pg.get("hasMore"), "expected hasMore inferred from next_page");
906
+ h.op(fhOp("list").path("/w").ctrl(ctrl));
907
+ assertTrue(rec.url(1).contains("page=2"),
908
+ "expected written-back next page sent: " + rec.url(1));
909
+ assertEquals(false, pg.get("hasMore"), "expected no more pages");
910
+ }
911
+
870
912
  @Test
871
913
  public void paging_nonListNotPaged() {
872
914
  assumeFeatures("paging");
@@ -11,7 +11,6 @@ package JAVAPACKAGE.sdktest;
11
11
  import static org.junit.jupiter.api.Assertions.assertEquals;
12
12
  import static org.junit.jupiter.api.Assertions.assertFalse;
13
13
  import static org.junit.jupiter.api.Assertions.assertNotNull;
14
- import static org.junit.jupiter.api.Assertions.assertNull;
15
14
  import static org.junit.jupiter.api.Assertions.assertTrue;
16
15
  import static org.junit.jupiter.api.Assertions.fail;
17
16
 
@@ -23,6 +22,7 @@ import java.util.ArrayList;
23
22
  import java.util.LinkedHashMap;
24
23
  import java.util.List;
25
24
  import java.util.Map;
25
+ import java.util.regex.Pattern;
26
26
 
27
27
  import org.junit.jupiter.api.Test;
28
28
 
@@ -529,13 +529,98 @@ public class PipelineTest {
529
529
 
530
530
  // --- prepareAuth --------------------------------------------------------------
531
531
 
532
- static Spec authSpec(Map<String, Object> headers) {
532
+ // A cookie credential as prepareAuth writes it: `<scheme>=K` for the probe
533
+ // key, with no scheme prefix and nothing else in the bag.
534
+ static final Pattern COOKIE_PAIR = Pattern.compile("^[^=;]+=K$");
535
+
536
+ static Spec authSpec() {
533
537
  Map<String, Object> m = new LinkedHashMap<>();
534
- m.put("headers", headers == null ? new LinkedHashMap<>() : headers);
538
+ m.put("headers", new LinkedHashMap<String, Object>());
539
+ m.put("query", new LinkedHashMap<String, Object>());
535
540
  m.put("step", "s");
536
541
  return new Spec(m);
537
542
  }
538
543
 
544
+ // Where this SDK's prepareAuth actually puts the credential. The name is
545
+ // the API's OWN scheme name, not always "authorization", so it is
546
+ // discovered rather than asserted. `pair` is the `<scheme>=` lead-in of a
547
+ // COOKIE credential, which rides the header bag under the key "cookie"
548
+ // instead of taking a header of its own.
549
+ record AuthCred(String where, String name, Object value, String pair) {
550
+ }
551
+
552
+ // A cookie credential rides the header bag, because a cookie IS a header.
553
+ static Map<String, Object> authBag(Spec spec, String where) {
554
+ return "query".equals(where) ? spec.query : spec.headers;
555
+ }
556
+
557
+ // `basic: false` is explicit: an HTTP Basic API's generated config carries
558
+ // `auth.basic: true`, testSDK merges it in, and the branch it selects needs
559
+ // a secret as well. With none supplied that branch deliberately writes
560
+ // nothing, which the probe below would read as a public API.
561
+ static Map<String, Object> authBlock(String prefix) {
562
+ return fhMap("prefix", prefix, "basic", Boolean.FALSE);
563
+ }
564
+
565
+ // Run prepareAuth with both containers present and see which one the
566
+ // generated utility writes to, and under what name. Null means this SDK
567
+ // places no credential at all - a public API - which is a legitimate
568
+ // shape, and the tests below assert exactly that instead.
569
+ static AuthCred authProbe(Map<String, Object> sdkopts) {
570
+ ProjectNameSDK client = plClient(sdkopts);
571
+ Utility utility = client.getUtility();
572
+ Context ctx = plCtx(client, utility, null);
573
+ ctx.spec = authSpec();
574
+ utility.prepareAuth.apply(ctx);
575
+ for (String where : new String[] { "headers", "query" }) {
576
+ for (Map.Entry<String, Object> entry : authBag(ctx.spec, where).entrySet()) {
577
+ Object value = entry.getValue();
578
+ String pair = "";
579
+ if ("headers".equals(where) && "cookie".equals(entry.getKey())
580
+ && value instanceof String
581
+ && COOKIE_PAIR.matcher((String) value).matches()) {
582
+ String text = (String) value;
583
+ pair = text.substring(0, text.length() - 1);
584
+ }
585
+ return new AuthCred(where, entry.getKey(), value, pair);
586
+ }
587
+ }
588
+ return null;
589
+ }
590
+
591
+ static AuthCred authCredential() {
592
+ return authProbe(fhMap("apikey", "K", "auth", authBlock("Bearer")));
593
+ }
594
+
595
+ // Every credential this SDK could possibly place: both credentials and
596
+ // Basic switched on, so whichever branch the API has, something lands
597
+ // unless the API is public.
598
+ static AuthCred authAnyCredential() {
599
+ return authProbe(fhMap(
600
+ "apikey", "K",
601
+ "secret", "S",
602
+ "auth", fhMap("prefix", "Bearer", "basic", Boolean.TRUE)));
603
+ }
604
+
605
+ // Runs prepareAuth over a fresh spec, seeding the credential slot first
606
+ // when seed is non-null, and reports what is left there.
607
+ static Object[] authPlaced(Map<String, Object> sdkopts, Object seed) {
608
+ AuthCred cred = authCredential();
609
+ ProjectNameSDK client = plClient(sdkopts);
610
+ Utility utility = client.getUtility();
611
+ Context ctx = plCtx(client, utility, null);
612
+ ctx.spec = authSpec();
613
+ if (cred != null && seed != null) {
614
+ authBag(ctx.spec, cred.where()).put(cred.name(), seed);
615
+ }
616
+ utility.prepareAuth.apply(ctx);
617
+ if (cred == null) {
618
+ return new Object[] { null, Boolean.FALSE };
619
+ }
620
+ Map<String, Object> bag = authBag(ctx.spec, cred.where());
621
+ return new Object[] { bag.get(cred.name()), bag.containsKey(cred.name()) };
622
+ }
623
+
539
624
  @Test
540
625
  public void prepareAuth_guardsMissingSpec() {
541
626
  ProjectNameSDK client = plClient(fhMap("apikey", "K"));
@@ -545,74 +630,82 @@ public class PipelineTest {
545
630
  assertEquals("auth_no_spec", errCodeOf(() -> utility.prepareAuth.apply(ctx)));
546
631
  }
547
632
 
633
+ // Without this the cases below cannot fail for an SDK whose credential the
634
+ // probe misses: every one of them takes the public-API path instead.
548
635
  @Test
549
- public void prepareAuth_apikeyWithPrefixSpaceJoined() {
550
- ProjectNameSDK client = plClient(fhMap(
551
- "apikey", "K",
552
- "auth", fhMap("prefix", "Bearer")));
553
- Utility utility = client.getUtility();
554
- Context ctx = plCtx(client, utility, null);
555
- ctx.spec = authSpec(null);
556
- utility.prepareAuth.apply(ctx);
557
- assertEquals("Bearer K", ctx.spec.headers.get("authorization"));
636
+ public void prepareAuth_probeFindsTheCredentialThisSdkPlaces() {
637
+ assertEquals(null == authCredential(), null == authAnyCredential(),
638
+ "the probe missed a credential this SDK places, so every case "
639
+ + "takes the public-API path");
640
+ }
641
+
642
+ @Test
643
+ public void prepareAuth_placedWhereThisApiPutsIt() {
644
+ AuthCred cred = authCredential();
645
+ if (cred == null) {
646
+ // A public API places nothing, and that is the whole assertion.
647
+ Object[] got = authPlaced(fhMap(
648
+ "apikey", "K",
649
+ "auth", authBlock("Bearer")), null);
650
+ assertFalse((Boolean) got[1], "expected no credential placed at all");
651
+ return;
652
+ }
653
+ assertTrue("headers".equals(cred.where()) || "query".equals(cred.where()),
654
+ "unexpected credential container " + cred.where());
655
+ if (!"".equals(cred.pair())) {
656
+ // A cookie credential is a `<scheme>=<key>` pair, and the scheme name
657
+ // leaves no room for the option's prefix.
658
+ assertTrue(COOKIE_PAIR.matcher(String.valueOf(cred.value())).matches(),
659
+ "expected a cookie pair, got " + cred.value());
660
+ return;
661
+ }
662
+ // A header credential is prefix-joined; a query credential is the raw
663
+ // key, because a query parameter has nowhere to put a scheme name.
664
+ assertEquals("query".equals(cred.where()) ? "K" : "Bearer K", cred.value());
558
665
  }
559
666
 
560
667
  @Test
561
668
  public void prepareAuth_rawApikeyEmptyPrefixAsIs() {
562
- ProjectNameSDK client = plClient(fhMap(
669
+ Object[] got = authPlaced(fhMap(
563
670
  "apikey", "K",
564
- "auth", fhMap("prefix", "")));
565
- Utility utility = client.getUtility();
566
- Context ctx = plCtx(client, utility, null);
567
- ctx.spec = authSpec(null);
568
- utility.prepareAuth.apply(ctx);
569
- assertEquals("K", ctx.spec.headers.get("authorization"));
671
+ "auth", authBlock("")), null);
672
+ AuthCred cred = authCredential();
673
+ if (cred == null) {
674
+ assertFalse((Boolean) got[1], "expected no credential placed at all");
675
+ return;
676
+ }
677
+ assertEquals(cred.pair() + "K", got[0]);
570
678
  }
571
679
 
572
680
  @Test
573
- public void prepareAuth_emptyApikeyDropsHeader() {
574
- ProjectNameSDK client = plClient(fhMap(
681
+ public void prepareAuth_emptyApikeyDropsCredential() {
682
+ Object[] got = authPlaced(fhMap(
575
683
  "apikey", "",
576
- "auth", fhMap("prefix", "Bearer")));
577
- Utility utility = client.getUtility();
578
- Context ctx = plCtx(client, utility, null);
579
- ctx.spec = authSpec(fhMap("authorization", "stale"));
580
- utility.prepareAuth.apply(ctx);
581
- assertFalse(ctx.spec.headers.containsKey("authorization"),
582
- "expected authorization dropped");
684
+ "auth", authBlock("Bearer")), "stale");
685
+ assertFalse((Boolean) got[1], "expected the credential dropped");
583
686
  }
584
687
 
585
688
  @Test
586
- public void prepareAuth_missingApikeyDropsHeader() {
587
- ProjectNameSDK client = plClient(fhMap(
588
- "auth", fhMap("prefix", "Bearer")));
589
- Map<String, Object> options = client.optionsMap();
590
- Object apikey = options.get("apikey");
689
+ public void prepareAuth_missingApikeyDropsCredential() {
690
+ Map<String, Object> sdkopts = fhMap("auth", authBlock("Bearer"));
691
+ Object apikey = plClient(sdkopts).optionsMap().get("apikey");
591
692
  if (apikey instanceof String && !"".equals(apikey)) {
592
693
  // SDK options carry a configured apikey; case not reproducible here.
593
694
  return;
594
695
  }
595
- Utility utility = client.getUtility();
596
- Context ctx = plCtx(client, utility, null);
597
- ctx.spec = authSpec(fhMap("authorization", "stale"));
598
- utility.prepareAuth.apply(ctx);
599
- assertFalse(ctx.spec.headers.containsKey("authorization"),
600
- "expected authorization dropped");
696
+ Object[] got = authPlaced(sdkopts, "stale");
697
+ assertFalse((Boolean) got[1], "expected the credential dropped");
601
698
  }
602
699
 
603
700
  @Test
604
- public void prepareAuth_publicApiNoAuthBlockDropsHeader() {
605
- ProjectNameSDK client = plClient(fhMap("apikey", "K"));
606
- Map<String, Object> options = client.optionsMap();
607
- if (options.get("auth") != null) {
701
+ public void prepareAuth_publicApiNoAuthBlockDropsCredential() {
702
+ Map<String, Object> sdkopts = fhMap("apikey", "K");
703
+ if (plClient(sdkopts).optionsMap().get("auth") != null) {
608
704
  // Option validation supplies an auth shape for this SDK, so a truly
609
705
  // auth-less client cannot be constructed here.
610
706
  return;
611
707
  }
612
- Utility utility = client.getUtility();
613
- Context ctx = plCtx(client, utility, null);
614
- ctx.spec = authSpec(fhMap("authorization", "stale"));
615
- utility.prepareAuth.apply(ctx);
616
- assertNull(ctx.spec.headers.get("authorization"), "expected authorization dropped");
708
+ Object[] got = authPlaced(sdkopts, "stale");
709
+ assertFalse((Boolean) got[1], "expected the credential dropped");
617
710
  }
618
711
  }
@@ -100,6 +100,44 @@ public class PrimaryUtilityTest {
100
100
  run.runset(basic, subject);
101
101
  }
102
102
 
103
+ // Rename the corpus's `headers` bag to the real container, and the
104
+ // `authorization` key inside it to the real credential name. Applied only
105
+ // to the prepareAuth section, so real header assertions elsewhere are
106
+ // untouched.
107
+ @SuppressWarnings("unchecked")
108
+ static Object retargetAuth(Object node, PipelineTest.AuthCred cred) {
109
+ if (node instanceof List) {
110
+ List<Object> out = new ArrayList<>();
111
+ for (Object child : (List<Object>) node) {
112
+ out.add(retargetAuth(child, cred));
113
+ }
114
+ return out;
115
+ }
116
+ if (!(node instanceof Map)) {
117
+ return node;
118
+ }
119
+
120
+ Map<String, Object> out = new LinkedHashMap<>();
121
+ for (Map.Entry<String, Object> entry : ((Map<String, Object>) node).entrySet()) {
122
+ if ("headers".equals(entry.getKey())) {
123
+ Map<String, Object> bag = new LinkedHashMap<>();
124
+ Map<String, Object> inner = Helpers.toMapAny(entry.getValue());
125
+ if (inner != null) {
126
+ for (Map.Entry<String, Object> bagentry : inner.entrySet()) {
127
+ String name = "authorization".equals(bagentry.getKey())
128
+ ? cred.name() : bagentry.getKey();
129
+ bag.put(name, retargetAuth(bagentry.getValue(), cred));
130
+ }
131
+ }
132
+ out.put(cred.where(), bag);
133
+ }
134
+ else {
135
+ out.put(entry.getKey(), retargetAuth(entry.getValue(), cred));
136
+ }
137
+ }
138
+ return out;
139
+ }
140
+
103
141
  // Helper: create basic test context.
104
142
  static Context makeTestCtx(ProjectNameSDK client, Utility utility,
105
143
  Map<String, Object> overrides) {
@@ -684,15 +722,38 @@ public class PrimaryUtilityTest {
684
722
  ProjectNameSDK authClient = ProjectNameSDK.testSDK(null, setupOpts);
685
723
  Utility authUtility = authClient.getUtility();
686
724
 
687
- runsection("prepareAuth", (args) -> {
688
- Context ctx = OmniResolver.omniCtx(args[0], authClient, authUtility);
725
+ // The corpus writes the credential as `headers.authorization`: a
726
+ // PLACEHOLDER each runner points at the container and name this API
727
+ // actually uses. PipelineTest.authCredential (same package) discovers
728
+ // both by running prepareAuth once.
729
+ PipelineTest.AuthCred cred = PipelineTest.authCredential();
730
+ assertNotNull(cred, "prepareAuth placed no credential in headers or query");
731
+
732
+ // An absent section is runsection's report to make.
733
+ Map<String, Object> section = Helpers.toMapAny(run().spec.get("prepareAuth"));
734
+ boolean swap = section != null
735
+ && (!"headers".equals(cred.where()) || !"authorization".equals(cred.name()));
736
+ Object original = swap ? section.get("basic") : null;
737
+ if (swap) {
738
+ section.put("basic", retargetAuth(original, cred));
739
+ }
689
740
 
690
- authUtility.prepareAuth.apply(ctx);
741
+ try {
742
+ runsection("prepareAuth", (args) -> {
743
+ Context ctx = OmniResolver.omniCtx(args[0], authClient, authUtility);
691
744
 
692
- OmniResolver.omniSyncCtx(args[0], ctx);
745
+ authUtility.prepareAuth.apply(ctx);
693
746
 
694
- return null;
695
- });
747
+ OmniResolver.omniSyncCtx(args[0], ctx);
748
+
749
+ return null;
750
+ });
751
+ }
752
+ finally {
753
+ if (swap) {
754
+ section.put("basic", original);
755
+ }
756
+ }
696
757
  }
697
758
 
698
759
  @Test
@@ -9,6 +9,7 @@ import java.net.http.HttpResponse;
9
9
  import java.util.LinkedHashMap;
10
10
  import java.util.List;
11
11
  import java.util.Map;
12
+ import java.util.concurrent.ConcurrentHashMap;
12
13
  import java.util.function.BiFunction;
13
14
  import java.util.function.Supplier;
14
15
 
@@ -20,6 +21,40 @@ final class Fetcher {
20
21
 
21
22
  private Fetcher() {}
22
23
 
24
+ // Clients keyed by redirect policy and proxy. Each HttpClient owns a
25
+ // connection pool and a selector thread, so one per request reused
26
+ // nothing; one per key pools across calls.
27
+ private static final ConcurrentHashMap<String, HttpClient> CLIENTS =
28
+ new ConcurrentHashMap<>();
29
+
30
+ private static HttpClient clientFor(Map<String, Object> fetchdef) {
31
+ // "manual" (set by the station feature's middleware under a hosts
32
+ // policy) returns a 3xx like any other response: an automatic follow
33
+ // would carry injected credentials to a host no policy approved.
34
+ final HttpClient.Redirect redirectPolicy =
35
+ "manual".equals(fetchdef.get("redirect"))
36
+ ? HttpClient.Redirect.NEVER
37
+ : HttpClient.Redirect.NORMAL;
38
+
39
+ Object praw = fetchdef.get("proxy");
40
+ final String proxy = praw instanceof String ? (String) praw : "";
41
+
42
+ return CLIENTS.computeIfAbsent(redirectPolicy + "|" + proxy, key -> {
43
+ HttpClient.Builder clientb = HttpClient.newBuilder().followRedirects(redirectPolicy);
44
+ if (!"".equals(proxy)) {
45
+ try {
46
+ URI proxyUri = URI.create(proxy);
47
+ int port = proxyUri.getPort() < 0 ? 80 : proxyUri.getPort();
48
+ clientb.proxy(ProxySelector.of(new InetSocketAddress(proxyUri.getHost(), port)));
49
+ }
50
+ catch (RuntimeException e) {
51
+ // Unparseable proxy target: direct connection, as the go transport.
52
+ }
53
+ }
54
+ return clientb.build();
55
+ });
56
+ }
57
+
23
58
  static Map<String, Object> defaultHttpFetch(String fullurl, Map<String, Object> fetchdef) {
24
59
  String method = fetchdef.get("method") instanceof String
25
60
  ? (String) fetchdef.get("method") : "";
@@ -59,36 +94,9 @@ final class Fetcher {
59
94
  reqb.setHeader("User-Agent", "Mozilla/5.0 (compatible; ProjectNameSDK/1.0)");
60
95
  }
61
96
 
62
- // Honour a redirect annotation on the fetch definition (set by the
63
- // station feature's middleware under a hosts policy): "manual" returns
64
- // a 3xx like any other response instead of following it — the ts
65
- // donor's fetch honours the same key natively, and an automatic follow
66
- // would carry injected credentials to a host no policy approved.
67
- HttpClient.Redirect redirectPolicy =
68
- "manual".equals(fetchdef.get("redirect"))
69
- ? HttpClient.Redirect.NEVER
70
- : HttpClient.Redirect.NORMAL;
71
-
72
- // Honour a proxy annotation on the fetch definition (set by the proxy
73
- // feature): route the request through a proxied HttpClient.
74
- HttpClient.Builder clientb = HttpClient.newBuilder()
75
- .followRedirects(redirectPolicy);
76
- Object proxy = fetchdef.get("proxy");
77
- if (proxy instanceof String && !"".equals(proxy)) {
78
- try {
79
- URI proxyUri = URI.create((String) proxy);
80
- int port = proxyUri.getPort() < 0 ? 80 : proxyUri.getPort();
81
- clientb.proxy(ProxySelector.of(new InetSocketAddress(proxyUri.getHost(), port)));
82
- }
83
- catch (RuntimeException e) {
84
- // Unparseable proxy target: fall through to a direct connection,
85
- // mirroring the go transport (bad proxy URL is ignored).
86
- }
87
- }
88
-
89
97
  HttpResponse<String> resp;
90
98
  try {
91
- resp = clientb.build().send(reqb.build(), HttpResponse.BodyHandlers.ofString());
99
+ resp = clientFor(fetchdef).send(reqb.build(), HttpResponse.BodyHandlers.ofString());
92
100
  }
93
101
  catch (Exception e) {
94
102
  throw new RuntimeException("fetch: " + e.getMessage(), e);
@@ -35,7 +35,7 @@ final class PrepareQuery {
35
35
  for (List<Object> item : Struct.items(reqmatch)) {
36
36
  String key = item.get(0) instanceof String ? (String) item.get(0) : "";
37
37
  Object val = item.get(1);
38
- if (val != null && !containsStr(params, key)) {
38
+ if (val != null && !"$action".equals(key) && !containsStr(params, key)) {
39
39
  out.put(key, val);
40
40
  }
41
41
  }
@@ -19,17 +19,32 @@ final class TransformRequest {
19
19
  Map<String, Object> transform =
20
20
  Helpers.toMapAny(Struct.getprop(ctx.point, "transform"));
21
21
  if (transform == null) {
22
- return ctx.reqdata;
22
+ return stripAction(ctx.reqdata);
23
23
  }
24
24
 
25
25
  Object reqform = Struct.getprop(transform, "req", null);
26
26
  if (reqform == null) {
27
- return ctx.reqdata;
27
+ return stripAction(ctx.reqdata);
28
28
  }
29
29
 
30
30
  Map<String, Object> data = new LinkedHashMap<>();
31
31
  data.put("reqdata", ctx.reqdata);
32
32
 
33
- return Struct.transform(data, reqform);
33
+ return stripAction(Struct.transform(data, reqform));
34
+ }
35
+
36
+ // `$action` selects the point (see MakePoint); it is never an API field, so
37
+ // the body is a copy without it. The caller's map is left untouched.
38
+ private static Object stripAction(Object reqdata) {
39
+ if (!(reqdata instanceof Map) || !((Map<?, ?>) reqdata).containsKey("$action")) {
40
+ return reqdata;
41
+ }
42
+ Map<String, Object> body = new LinkedHashMap<>();
43
+ for (Map.Entry<?, ?> e : ((Map<?, ?>) reqdata).entrySet()) {
44
+ if (!"$action".equals(e.getKey())) {
45
+ body.put(String.valueOf(e.getKey()), e.getValue());
46
+ }
47
+ }
48
+ return body;
34
49
  }
35
50
  }
@@ -57,7 +57,13 @@ class Context {
57
57
  this.client = getprop(ctxmap, 'client', getprop(basectx, 'client'))
58
58
  this.utility = getprop(ctxmap, 'utility', getprop(basectx, 'utility'))
59
59
 
60
- this.ctrl = getprop(ctxmap, 'ctrl', getprop(basectx, 'ctrl', this.ctrl))
60
+ // An operation gets its OWN control unless the caller passed one: a
61
+ // paging cursor left on the client's control would otherwise be picked
62
+ // up by the next, unrelated call. A context with no opname is not an
63
+ // operation, so it still shares (an entity's, with the client's).
64
+ const opname = getprop(ctxmap, 'opname')
65
+ const basectrl = null == opname ? getprop(basectx, 'ctrl', this.ctrl) : this.ctrl
66
+ this.ctrl = getprop(ctxmap, 'ctrl', basectrl)
61
67
  this.meta = getprop(ctxmap, 'meta', getprop(basectx, 'meta', this.meta))
62
68
 
63
69
  this.config = getprop(ctxmap, 'config', getprop(basectx, 'config'))
@@ -78,7 +84,6 @@ class Context {
78
84
  this.result = getprop(ctxmap, 'result', getprop(basectx, 'result'))
79
85
  this.response = getprop(ctxmap, 'response', getprop(basectx, 'response'))
80
86
 
81
- const opname = getprop(ctxmap, 'opname')
82
87
  this.op = this.resolveOp(opname)
83
88
  }
84
89