@voxgig/sdkgen 4.4.1 → 4.5.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 (104) hide show
  1. package/bin/voxgig-sdkgen +1 -1
  2. package/dist/cmp/FeatureDocs.d.ts +16 -0
  3. package/dist/cmp/FeatureDocs.js +72 -0
  4. package/dist/cmp/FeatureDocs.js.map +1 -0
  5. package/dist/cmp/Readme.js +2 -0
  6. package/dist/cmp/Readme.js.map +1 -1
  7. package/dist/cmp/ReadmeFeatures.d.ts +2 -0
  8. package/dist/cmp/ReadmeFeatures.js +95 -0
  9. package/dist/cmp/ReadmeFeatures.js.map +1 -0
  10. package/dist/cmp/ReadmeRef.js +4 -0
  11. package/dist/cmp/ReadmeRef.js.map +1 -1
  12. package/dist/cmp/ReadmeRefFeatures.d.ts +2 -0
  13. package/dist/cmp/ReadmeRefFeatures.js +130 -0
  14. package/dist/cmp/ReadmeRefFeatures.js.map +1 -0
  15. package/dist/cmp/ReadmeTop.js +46 -0
  16. package/dist/cmp/ReadmeTop.js.map +1 -1
  17. package/dist/helpers/opShape.js +5 -1
  18. package/dist/helpers/opShape.js.map +1 -1
  19. package/dist/sdkgen.d.ts +3 -2
  20. package/dist/sdkgen.js +30 -10
  21. package/dist/sdkgen.js.map +1 -1
  22. package/dist/tsconfig.tsbuildinfo +1 -1
  23. package/dist/utility.d.ts +2 -1
  24. package/dist/utility.js +17 -1
  25. package/dist/utility.js.map +1 -1
  26. package/package.json +1 -1
  27. package/project/.sdk/src/cmp/c/ReadmeRef_c.ts +5 -0
  28. package/project/.sdk/src/cmp/clojure/ReadmeRef_clojure.ts +5 -0
  29. package/project/.sdk/src/cmp/cpp/ReadmeRef_cpp.ts +5 -0
  30. package/project/.sdk/src/cmp/csharp/ReadmeRef_csharp.ts +5 -0
  31. package/project/.sdk/src/cmp/dart/ReadmeRef_dart.ts +5 -0
  32. package/project/.sdk/src/cmp/elixir/ReadmeRef_elixir.ts +5 -0
  33. package/project/.sdk/src/cmp/go/ReadmeRef_go.ts +5 -0
  34. package/project/.sdk/src/cmp/go/TestDirect_go.ts +123 -2
  35. package/project/.sdk/src/cmp/java/ReadmeRef_java.ts +5 -0
  36. package/project/.sdk/src/cmp/js/ReadmeRef_js.ts +5 -0
  37. package/project/.sdk/src/cmp/js/TestDirect_js.ts +62 -0
  38. package/project/.sdk/src/cmp/kotlin/ReadmeRef_kotlin.ts +5 -0
  39. package/project/.sdk/src/cmp/lean/ReadmeRef_lean.ts +27 -3
  40. package/project/.sdk/src/cmp/lua/ReadmeRef_lua.ts +5 -0
  41. package/project/.sdk/src/cmp/ocaml/ReadmeRef_ocaml.ts +5 -0
  42. package/project/.sdk/src/cmp/perl/ReadmeRef_perl.ts +5 -0
  43. package/project/.sdk/src/cmp/php/ReadmeRef_php.ts +5 -0
  44. package/project/.sdk/src/cmp/py/ReadmeRef_py.ts +5 -0
  45. package/project/.sdk/src/cmp/rb/ReadmeRef_rb.ts +5 -0
  46. package/project/.sdk/src/cmp/rust/ReadmeRef_rust.ts +5 -0
  47. package/project/.sdk/src/cmp/scala/ReadmeRef_scala.ts +5 -0
  48. package/project/.sdk/src/cmp/seneca-provider/Extras_seneca-provider.ts +19 -1
  49. package/project/.sdk/src/cmp/seneca-provider/Main_seneca-provider.ts +27 -2
  50. package/project/.sdk/src/cmp/swift/ReadmeRef_swift.ts +5 -0
  51. package/project/.sdk/src/cmp/ts/Config_ts.ts +4 -1
  52. package/project/.sdk/src/cmp/ts/ReadmeHowto_ts.ts +9 -4
  53. package/project/.sdk/src/cmp/ts/ReadmeModel_ts.ts +8 -3
  54. package/project/.sdk/src/cmp/ts/ReadmeQuick_ts.ts +3 -2
  55. package/project/.sdk/src/cmp/ts/ReadmeRef_ts.ts +7 -2
  56. package/project/.sdk/src/cmp/ts/ReadmeTopQuick_ts.ts +3 -2
  57. package/project/.sdk/src/cmp/ts/TestDirect_ts.ts +5 -2
  58. package/project/.sdk/src/cmp/ts/TestEntity_ts.ts +5 -2
  59. package/project/.sdk/src/cmp/zig/ReadmeRef_zig.ts +5 -0
  60. package/project/.sdk/tm/clojure/src/sdk/core.clj +6 -2
  61. package/project/.sdk/tm/clojure/src/sdk/features.clj +175 -0
  62. package/project/.sdk/tm/csharp/utility/MakeOptions.cs +5 -0
  63. package/project/.sdk/tm/dart/lib/utility/MakeOptionsUtility.dart +4 -0
  64. package/project/.sdk/tm/dart/test/feature_test.dart +54 -0
  65. package/project/.sdk/tm/elixir/lib/projectname/utility.ex +4 -1
  66. package/project/.sdk/tm/go/feature/test_feature.go +17 -6
  67. package/project/.sdk/tm/go/test/feature_corpus_test.go +10 -3
  68. package/project/.sdk/tm/java/test/FeatureCorpusTest.java +5 -4
  69. package/project/.sdk/tm/js/src/feature/test/TestFeature.js +8 -1
  70. package/project/.sdk/tm/js/src/utility/MakeOptionsUtility.js +3 -1
  71. package/project/.sdk/tm/js/test/feature/Corpus.test.js +13 -3
  72. package/project/.sdk/tm/js/test/feature.test.js +19 -9
  73. package/project/.sdk/tm/lean/src/SdkFeatures.lean +186 -1
  74. package/project/.sdk/tm/lua/feature/test_feature.lua +21 -7
  75. package/project/.sdk/tm/lua/test/feature_test.lua +18 -18
  76. package/project/.sdk/tm/lua/utility/make_options.lua +2 -0
  77. package/project/.sdk/tm/ocaml/sdk_features.ml +5 -0
  78. package/project/.sdk/tm/ocaml/sdk_runtime.ml +4 -1
  79. package/project/.sdk/tm/perl/t/feature_corpus.t +5 -4
  80. package/project/.sdk/tm/perl/utility/make_options.pm +4 -1
  81. package/project/.sdk/tm/php/feature/TestFeature.php +19 -8
  82. package/project/.sdk/tm/php/test/FeatureCorpusTest.php +7 -4
  83. package/project/.sdk/tm/php/utility/MakeOptions.php +2 -1
  84. package/project/.sdk/tm/py/pkg/feature/test_feature.py +15 -4
  85. package/project/.sdk/tm/py/pkg/utility/make_options.py +2 -0
  86. package/project/.sdk/tm/py/test/test_feature.py +40 -0
  87. package/project/.sdk/tm/py/test/test_feature_corpus.py +14 -3
  88. package/project/.sdk/tm/rb/test/feature_corpus_test.rb +12 -4
  89. package/project/.sdk/tm/rb/utility/make_options.rb +4 -1
  90. package/project/.sdk/tm/ts/src/feature/test/TestFeature.ts +15 -1
  91. package/project/.sdk/tm/ts/src/utility/MakeOptionsUtility.ts +3 -1
  92. package/project/.sdk/tm/ts/src/utility/PrepareAuthUtility.ts +20 -0
  93. package/project/.sdk/tm/ts/test/feature/Corpus.test.ts +13 -3
  94. package/project/.sdk/tm/ts/test/feature.test.ts +19 -9
  95. package/project/sdkgen-package.json +1 -1
  96. package/src/cmp/FeatureDocs.ts +100 -0
  97. package/src/cmp/Readme.ts +2 -0
  98. package/src/cmp/ReadmeFeatures.ts +113 -0
  99. package/src/cmp/ReadmeRef.ts +4 -0
  100. package/src/cmp/ReadmeRefFeatures.ts +149 -0
  101. package/src/cmp/ReadmeTop.ts +48 -0
  102. package/src/helpers/opShape.ts +5 -1
  103. package/src/sdkgen.ts +28 -7
  104. package/src/utility.ts +21 -1
@@ -441,7 +441,7 @@ describe("feature", function()
441
441
  -- retry ---------------------------------------------------------------
442
442
  if has_feature("retry") then describe("retry", function()
443
443
 
444
- it("retries transient failures then succeeds", function()
444
+ if has_feature("netsim") then it("retries transient failures then succeeds", function()
445
445
  local clock = make_clock()
446
446
  local h = make_client({ features = {
447
447
  { name = "netsim", options = { failTimes = 2, failStatus = 503 } },
@@ -450,9 +450,9 @@ describe("feature", function()
450
450
  } })
451
451
  assert.is_true(h.op({ op = "load" }).ok)
452
452
  assert.are.equal(2, h.client._retry.attempts)
453
- end)
453
+ end) end
454
454
 
455
- it("gives up after the budget", function()
455
+ if has_feature("netsim") then it("gives up after the budget", function()
456
456
  local clock = make_clock()
457
457
  local h = make_client({ features = {
458
458
  { name = "netsim", options = { failTimes = 9, failStatus = 500 } },
@@ -460,7 +460,7 @@ describe("feature", function()
460
460
  retries = 2, minDelay = 1, jitter = false, sleep = clock.sleep } },
461
461
  } })
462
462
  assert.are.equal(500, h.op({ op = "load" }).result.status)
463
- end)
463
+ end) end
464
464
 
465
465
  it("does not retry a non-retryable status", function()
466
466
  local rec = recording_server(function() return make_response(404) end)
@@ -487,7 +487,7 @@ describe("feature", function()
487
487
  assert.are.equal(3, n)
488
488
  end)
489
489
 
490
- it("honours a server Retry-After", function()
490
+ if has_feature("netsim") then it("honours a server Retry-After", function()
491
491
  local clock = make_clock()
492
492
  local h = make_client({ features = {
493
493
  { name = "netsim", options = { rateLimitTimes = 1, retryAfter = 2 } },
@@ -497,15 +497,15 @@ describe("feature", function()
497
497
  } })
498
498
  assert.is_true(h.op({ op = "load" }).ok)
499
499
  assert.are.equal(2000, clock.time())
500
- end)
500
+ end) end
501
501
 
502
- it("default jitter path still succeeds", function()
502
+ if has_feature("netsim") then it("default jitter path still succeeds", function()
503
503
  local h = make_client({ features = {
504
504
  { name = "netsim", options = { failTimes = 1 } },
505
505
  { name = "retry", options = { retries = 2, minDelay = 0 } },
506
506
  } })
507
507
  assert.is_true(h.op({ op = "load" }).ok)
508
- end)
508
+ end) end
509
509
 
510
510
  it("inactive retry does not wrap", function()
511
511
  local rec = recording_server(function() return make_response(503) end)
@@ -551,7 +551,7 @@ describe("feature", function()
551
551
  -- shared virtual clock stands in for elapsed transport time.
552
552
  if has_feature("timeout") then describe("timeout", function()
553
553
 
554
- it("a slow request times out", function()
554
+ if has_feature("netsim") then it("a slow request times out", function()
555
555
  local clock = make_clock()
556
556
  local h = make_client({ features = {
557
557
  { name = "netsim", options = { latency = 80, sleep = clock.sleep } },
@@ -560,7 +560,7 @@ describe("feature", function()
560
560
  local res = h.op({ op = "load" })
561
561
  assert.are.equal("timeout", res.error.code)
562
562
  assert.are.equal(1, h.client._timeout.count)
563
- end)
563
+ end) end
564
564
 
565
565
  it("a fast request passes through", function()
566
566
  local clock = make_clock()
@@ -819,7 +819,7 @@ describe("feature", function()
819
819
  -- metrics ---------------------------------------------------------------
820
820
  if has_feature("metrics") then describe("metrics", function()
821
821
 
822
- it("counts ok and err per op", function()
822
+ if has_feature("netsim") then it("counts ok and err per op", function()
823
823
  local h = make_client({ features = {
824
824
  { name = "netsim", options = { failTimes = 1, failStatus = 500 } },
825
825
  { name = "metrics", options = {} },
@@ -832,7 +832,7 @@ describe("feature", function()
832
832
  assert.are.equal(2, m.total.ok)
833
833
  assert.are.equal(1, m.total.err)
834
834
  assert.are.equal(2, m.ops["widget.load"].count)
835
- end)
835
+ end) end
836
836
 
837
837
  it("injected clock accumulates durations", function()
838
838
  local t = 0
@@ -872,14 +872,14 @@ describe("feature", function()
872
872
  assert.is_truthy(string.match(sent["traceparent"], "^00%-.+%-.+%-01$"))
873
873
  end)
874
874
 
875
- it("records a failed span on error", function()
875
+ if has_feature("netsim") then it("records a failed span on error", function()
876
876
  local h = make_client({ features = {
877
877
  { name = "netsim", options = { failTimes = 1, failStatus = 500 } },
878
878
  { name = "telemetry", options = {} },
879
879
  } })
880
880
  h.op({ op = "load" })
881
881
  assert.is_false(h.client._telemetry.spans[1].ok)
882
- end)
882
+ end) end
883
883
 
884
884
  it("default id generation and no exporter", function()
885
885
  local h = make_client({ features = { { name = "telemetry" } } })
@@ -925,14 +925,14 @@ describe("feature", function()
925
925
  assert.are.equal("<redacted>", seen[1].headers.authorization)
926
926
  end)
927
927
 
928
- it("captures failures", function()
928
+ if has_feature("netsim") then it("captures failures", function()
929
929
  local h = make_client({ features = {
930
930
  { name = "netsim", options = { failTimes = 1, failStatus = 500 } },
931
931
  { name = "debug", options = {} },
932
932
  } })
933
933
  h.op({ op = "load" })
934
934
  assert.is_false(h.client._debug.entries[1].ok)
935
- end)
935
+ end) end
936
936
 
937
937
  it("injected clock + custom redact", function()
938
938
  local h = make_client({ features = {
@@ -957,7 +957,7 @@ describe("feature", function()
957
957
  -- audit -----------------------------------------------------------------
958
958
  if has_feature("audit") then describe("audit", function()
959
959
 
960
- it("one record per op with sink + actor", function()
960
+ if has_feature("netsim") then it("one record per op with sink + actor", function()
961
961
  local sink = {}
962
962
  local h = make_client({ features = {
963
963
  { name = "netsim", options = { failTimes = 1, failStatus = 500 } },
@@ -972,7 +972,7 @@ describe("feature", function()
972
972
  assert.are.equal("svc", recs[1].actor)
973
973
  assert.are.equal("per-call", recs[2].actor)
974
974
  assert.are.equal(2, #sink)
975
- end)
975
+ end) end
976
976
 
977
977
  it("default actor is anonymous", function()
978
978
  local h = make_client({ features = { { name = "audit" } } })
@@ -75,11 +75,13 @@ local function make_options_util(ctx)
75
75
 
76
76
  local optspec = {
77
77
  apikey = "",
78
+ secret = "",
78
79
  base = "http://localhost:8000",
79
80
  prefix = "",
80
81
  suffix = "",
81
82
  auth = {
82
83
  prefix = "",
84
+ basic = false,
83
85
  },
84
86
  headers = {
85
87
  ["`$CHILD`"] = "`$STRING`",
@@ -813,6 +813,11 @@ let cost_feature () : feature =
813
813
  end
814
814
  end
815
815
  | "PreDone" -> finish ctx true
816
+ (* A failed operation never reaches PreDone - make_error dispatches
817
+ PreUnexpected - so its attempts would be priced and discarded, and
818
+ repeated failures could slip past an onBudget "deny" ceiling.
819
+ `false` keeps a call that made NO attempt from being counted. *)
820
+ | "PreUnexpected" -> finish ctx false
816
821
  | _ -> ());
817
822
  f
818
823
 
@@ -937,10 +937,13 @@ let fetcher_util (ctx : ctx) (fullurl : string) (fetchdef : value) : (value * sd
937
937
  let opt_spec_value () : value =
938
938
  jo [
939
939
  ("apikey", Str "");
940
+ ("secret", Str "");
940
941
  ("base", Str "http://localhost:8000");
941
942
  ("prefix", Str "");
942
943
  ("suffix", Str "");
943
- ("auth", jo [("prefix", Str "")]);
944
+ (* `basic` and `secret`: HTTP Basic Auth needs a second credential and a
945
+ flag to say the pair is Basic rather than a single bearer token. *)
946
+ ("auth", jo [("prefix", Str ""); ("basic", Bool false)]);
944
947
  ("headers", jo [("`$CHILD`", Str "`$STRING`")]);
945
948
  ("allow", jo [("method", Str "GET,PUT,POST,PATCH,DELETE,OPTIONS");
946
949
  ("op", Str "create,update,load,list,remove,command,direct,graphql")]);
@@ -29,9 +29,10 @@ unless (defined $TEST_JSON && -e $TEST_JSON) {
29
29
  plan skip_all => 'test.json corpus not found';
30
30
  }
31
31
 
32
- # Features with a corpus section. A name here with no section is a skip, not
33
- # a failure: an SDK generated without the feature has nothing to run.
34
- my @FEATURE_CORPUS_NAMES = ('cost');
32
+ # Features with a corpus section are read from the corpus itself (see the
33
+ # loop below), so a project-authored section - a custom feature added under
34
+ # .sdk/test/feature/ - runs without editing this file. An SDK generated
35
+ # without a listed feature still skips, not fails.
35
36
 
36
37
  # The standard operation names, in the order the runner prefers them.
37
38
  my @FEATURE_CORPUS_OPS = qw(load list create update remove);
@@ -282,7 +283,7 @@ ok(scalar(@ops) > 0,
282
283
  exit 0;
283
284
  };
284
285
 
285
- for my $name (@FEATURE_CORPUS_NAMES) {
286
+ for my $name (sort keys %{ $CORPUS->{feature} || {} }) {
286
287
  my $section = ($CORPUS->{feature} || {})->{$name};
287
288
  next unless defined $section;
288
289
 
@@ -108,9 +108,12 @@ $REGISTRY{make_options} = sub {
108
108
  my $optspec = {
109
109
  'apikey' => '',
110
110
  'base' => 'http://localhost:8000',
111
+ 'secret' => '',
111
112
  'prefix' => '',
112
113
  'suffix' => '',
113
- 'auth' => { 'prefix' => '' },
114
+ # `basic` and `secret`: HTTP Basic Auth needs a second credential and a
115
+ # flag to say the pair is Basic rather than a single bearer token.
116
+ 'auth' => { 'prefix' => '', 'basic' => $JF },
114
117
  'headers' => { '`$CHILD`' => '`$STRING`' },
115
118
  'allow' => {
116
119
  'method' => 'GET,PUT,POST,PATCH,DELETE,OPTIONS',
@@ -69,10 +69,16 @@ class ProjectNameTestFeature extends ProjectNameBaseFeature
69
69
  if (!is_string($restf)) {
70
70
  return $data;
71
71
  }
72
- if (!preg_match('/^`body\.([^`.]+)`$/', $restf, $m)) {
72
+ if (!preg_match('/^`body\.(.+)`$/', $restf, $m)) {
73
73
  return $data;
74
74
  }
75
- return [$m[1] => $data];
75
+ // Multi-segment on purpose: GraphQL ops unwrap body.data.<field>
76
+ // (and body.data.<field>.<entity> for mutations), not just one level.
77
+ $out = $data;
78
+ foreach (array_reverse(explode('.', $m[1])) as $seg) {
79
+ $out = [$seg => $out];
80
+ }
81
+ return $out;
76
82
  };
77
83
 
78
84
  $respond = function (int $status, mixed $data, ?array $extra = null) use ($envelope): array {
@@ -433,13 +439,18 @@ class ProjectNameTestFeature extends ProjectNameBaseFeature
433
439
  }
434
440
  }
435
441
  }
442
+ // Path AND query: a path-only read misses a query-addressed
443
+ // record (e.g. GET /result?trace_id=), which has no path param.
436
444
  $params = is_array($point) ? ($point['args']['params'] ?? null) : null;
437
- if (is_array($params)) {
438
- foreach ($params as $p) {
439
- if (is_array($p) && (($p['reqd'] ?? false) === true)) {
440
- $n = $p['name'] ?? null;
441
- if ($n !== null) {
442
- $reqd_names[] = $n;
445
+ $query = is_array($point) ? ($point['args']['query'] ?? null) : null;
446
+ foreach ([$params, $query] as $arglist) {
447
+ if (is_array($arglist)) {
448
+ foreach ($arglist as $p) {
449
+ if (is_array($p) && (($p['reqd'] ?? false) === true)) {
450
+ $n = $p['name'] ?? null;
451
+ if ($n !== null) {
452
+ $reqd_names[] = $n;
453
+ }
443
454
  }
444
455
  }
445
456
  }
@@ -23,9 +23,10 @@ use PHPUnit\Framework\TestCase;
23
23
 
24
24
  class FeatureCorpusTest extends TestCase
25
25
  {
26
- // Features with a corpus section. A name here with no section is a skip,
27
- // not a failure: an SDK generated without the feature has nothing to run.
28
- private const FEATURE_CORPUS_NAMES = ['cost'];
26
+ // Features with a corpus section are read from the corpus itself (see
27
+ // testFeatureCorpus), so a project-authored section - a custom feature
28
+ // added under .sdk/test/feature/ - runs without editing this file. An
29
+ // SDK generated without a listed feature still skips, not fails.
29
30
 
30
31
  // The standard operation names, in the order the runner prefers them.
31
32
  private const FEATURE_CORPUS_OPS = ['load', 'list', 'create', 'update', 'remove'];
@@ -300,7 +301,9 @@ class FeatureCorpusTest extends TestCase
300
301
  . 'the corpus (create-sdkgen .sdk/test/feature/) to run these cases');
301
302
  }
302
303
 
303
- foreach (self::FEATURE_CORPUS_NAMES as $name) {
304
+ $names = array_keys(self::corpus()['feature'] ?? []);
305
+ sort($names);
306
+ foreach ($names as $name) {
304
307
  $section = self::corpus()['feature'][$name] ?? null;
305
308
  if (null === $section) {
306
309
  continue;
@@ -130,10 +130,11 @@ class ProjectNameMakeOptions
130
130
 
131
131
  $optspec = [
132
132
  'apikey' => '',
133
+ 'secret' => '',
133
134
  'base' => 'http://localhost:8000',
134
135
  'prefix' => '',
135
136
  'suffix' => '',
136
- 'auth' => ['prefix' => ''],
137
+ 'auth' => ['prefix' => '', 'basic' => false],
137
138
  'headers' => ['`$CHILD`' => '`$STRING`'],
138
139
  'allow' => [
139
140
  'method' => 'GET,PUT,POST,PATCH,DELETE,OPTIONS',
@@ -11,7 +11,7 @@ from projectname_sdk.feature.base_feature import ProjectNameBaseFeature
11
11
 
12
12
  # The `body.<key>` form of an op's response transform: the mock wraps its
13
13
  # payload in <key> so the transform can unwrap it again.
14
- ENVELOPE_RES_RE = re.compile(r"^`body\.([^`.]+)`$")
14
+ ENVELOPE_RES_RE = re.compile(r"^`body\.(.+)`$")
15
15
 
16
16
 
17
17
  class ProjectNameTestFeature(ProjectNameBaseFeature):
@@ -60,7 +60,14 @@ class ProjectNameTestFeature(ProjectNameBaseFeature):
60
60
  if not isinstance(restf, str):
61
61
  return data
62
62
  m = ENVELOPE_RES_RE.match(restf)
63
- return data if m is None else {m.group(1): data}
63
+ if m is None:
64
+ return data
65
+ # Multi-segment on purpose: GraphQL ops unwrap body.data.<field>
66
+ # (and body.data.<field>.<entity> for mutations), not just one level.
67
+ out = data
68
+ for seg in reversed(m.group(1).split(".")):
69
+ out = {seg: out}
70
+ return out
64
71
 
65
72
  def respond(status, data, extra=None):
66
73
  payload = envelope(data)
@@ -260,10 +267,14 @@ class ProjectNameTestFeature(ProjectNameBaseFeature):
260
267
  points = vs.getpath(ctx.config, "entity." + ctx.entity.get_name() + ".op." + opname + ".points")
261
268
  point = vs.getelem(points, -1)
262
269
 
263
- # Get required params.
270
+ # Path AND query: a path-only read misses a query-addressed record
271
+ # (e.g. GET /result?trace_id=), which has no path param at all.
264
272
  params_path = vs.getpath(point, "args.params")
265
273
  reqd_params = vs.select(params_path, {"reqd": True})
266
- reqd = vs.transform(reqd_params, ["`$EACH`", "", "`$KEY.name`"])
274
+ query_path = vs.getpath(point, "args.query")
275
+ reqd_query = vs.select(query_path, {"reqd": True})
276
+ reqd = (vs.transform(reqd_params, ["`$EACH`", "", "`$KEY.name`"]) or []) + \
277
+ (vs.transform(reqd_query, ["`$EACH`", "", "`$KEY.name`"]) or [])
267
278
 
268
279
  qand = []
269
280
  q = {"`$AND`": qand}
@@ -97,11 +97,13 @@ def make_options_util(ctx):
97
97
 
98
98
  optspec = {
99
99
  "apikey": "",
100
+ "secret": "",
100
101
  "base": "http://localhost:8000",
101
102
  "prefix": "",
102
103
  "suffix": "",
103
104
  "auth": {
104
105
  "prefix": "",
106
+ "basic": False,
105
107
  },
106
108
  "headers": {
107
109
  "`$CHILD`": "`$STRING`",
@@ -105,6 +105,10 @@ class TestNetsimFeature:
105
105
  @pytest.mark.skipif(not has_feature("retry"), reason="retry not in this SDK")
106
106
  class TestRetryFeature:
107
107
 
108
+ # Drives netsim as the simulated network: runnable only when this
109
+ # SDK was generated with it (the harness skips absent features).
110
+ @pytest.mark.skipif(not has_feature("netsim"),
111
+ reason="netsim not in this SDK")
108
112
  def test_retries_transient_failures_then_succeeds(self):
109
113
  clock = make_clock()
110
114
  h = make_client([
@@ -115,6 +119,10 @@ class TestRetryFeature:
115
119
  assert h.op(op="load")["ok"] is True
116
120
  assert h.client._retry["attempts"] == 2
117
121
 
122
+ # Drives netsim as the simulated network: runnable only when this
123
+ # SDK was generated with it (the harness skips absent features).
124
+ @pytest.mark.skipif(not has_feature("netsim"),
125
+ reason="netsim not in this SDK")
118
126
  def test_gives_up_after_the_budget(self):
119
127
  clock = make_clock()
120
128
  h = make_client([
@@ -163,6 +171,10 @@ class TestRetryFeature:
163
171
  assert h.op(op="load")["ok"] is True
164
172
  assert state["n"] == 2
165
173
 
174
+ # Drives netsim as the simulated network: runnable only when this
175
+ # SDK was generated with it (the harness skips absent features).
176
+ @pytest.mark.skipif(not has_feature("netsim"),
177
+ reason="netsim not in this SDK")
166
178
  def test_honours_a_server_retry_after(self):
167
179
  clock = make_clock()
168
180
  h = make_client([
@@ -174,6 +186,10 @@ class TestRetryFeature:
174
186
  assert h.op(op="load")["ok"] is True
175
187
  assert clock.time == 2000
176
188
 
189
+ # Drives netsim as the simulated network: runnable only when this
190
+ # SDK was generated with it (the harness skips absent features).
191
+ @pytest.mark.skipif(not has_feature("netsim"),
192
+ reason="netsim not in this SDK")
177
193
  def test_default_jitter_path_still_succeeds(self):
178
194
  h = make_client([
179
195
  {"name": "netsim", "options": {"failTimes": 1}},
@@ -202,6 +218,10 @@ class TestRetryFeature:
202
218
  @pytest.mark.skipif(not has_feature("timeout"), reason="timeout not in this SDK")
203
219
  class TestTimeoutFeature:
204
220
 
221
+ # Drives netsim as the simulated network: runnable only when this
222
+ # SDK was generated with it (the harness skips absent features).
223
+ @pytest.mark.skipif(not has_feature("netsim"),
224
+ reason="netsim not in this SDK")
205
225
  def test_a_slow_request_times_out(self):
206
226
  clock = make_clock()
207
227
  h = make_client([
@@ -438,6 +458,10 @@ class TestRbacFeature:
438
458
  @pytest.mark.skipif(not has_feature("metrics"), reason="metrics not in this SDK")
439
459
  class TestMetricsFeature:
440
460
 
461
+ # Drives netsim as the simulated network: runnable only when this
462
+ # SDK was generated with it (the harness skips absent features).
463
+ @pytest.mark.skipif(not has_feature("netsim"),
464
+ reason="netsim not in this SDK")
441
465
  def test_counts_ok_and_err_per_op(self):
442
466
  h = make_client([
443
467
  {"name": "netsim", "options": {"failTimes": 1, "failStatus": 500}},
@@ -490,6 +514,10 @@ class TestTelemetryFeature:
490
514
  assert sent["X-Trace-Id"] == h.client._telemetry["spans"][0]["traceId"]
491
515
  assert re.match(r"^00-.+-.+-01$", sent["traceparent"])
492
516
 
517
+ # Drives netsim as the simulated network: runnable only when this
518
+ # SDK was generated with it (the harness skips absent features).
519
+ @pytest.mark.skipif(not has_feature("netsim"),
520
+ reason="netsim not in this SDK")
493
521
  def test_records_a_failed_span_on_error(self):
494
522
  h = make_client([
495
523
  {"name": "netsim", "options": {"failTimes": 1, "failStatus": 500}},
@@ -498,6 +526,10 @@ class TestTelemetryFeature:
498
526
  h.op(op="load")
499
527
  assert h.client._telemetry["spans"][0]["ok"] is False
500
528
 
529
+ # Drives netsim as the simulated network: runnable only when this
530
+ # SDK was generated with it (the harness skips absent features).
531
+ @pytest.mark.skipif(not has_feature("netsim"),
532
+ reason="netsim not in this SDK")
501
533
  def test_closes_each_span_exactly_once(self):
502
534
  h = make_client([
503
535
  {"name": "netsim", "options": {"failTimes": 1, "failStatus": 500}},
@@ -542,6 +574,10 @@ class TestDebugFeature:
542
574
  assert len(seen) == 2
543
575
  assert seen[0]["headers"]["authorization"] == "<redacted>"
544
576
 
577
+ # Drives netsim as the simulated network: runnable only when this
578
+ # SDK was generated with it (the harness skips absent features).
579
+ @pytest.mark.skipif(not has_feature("netsim"),
580
+ reason="netsim not in this SDK")
545
581
  def test_captures_failures(self):
546
582
  h = make_client([
547
583
  {"name": "netsim", "options": {"failTimes": 1, "failStatus": 500}},
@@ -571,6 +607,10 @@ class TestDebugFeature:
571
607
  @pytest.mark.skipif(not has_feature("audit"), reason="audit not in this SDK")
572
608
  class TestAuditFeature:
573
609
 
610
+ # Drives netsim as the simulated network: runnable only when this
611
+ # SDK was generated with it (the harness skips absent features).
612
+ @pytest.mark.skipif(not has_feature("netsim"),
613
+ reason="netsim not in this SDK")
574
614
  def test_one_record_per_op_with_sink_and_actor(self):
575
615
  sink = []
576
616
  h = make_client([
@@ -23,9 +23,20 @@ from projectname_sdk import ProjectNameSDK
23
23
 
24
24
  _TEST_DIR = os.path.dirname(os.path.abspath(__file__))
25
25
 
26
- # Features with a corpus section. A name here with no section is a skip, not
27
- # a failure: an SDK generated without the feature has nothing to run.
28
- FEATURE_CORPUS_NAMES = ["cost"]
26
+ # Features with a corpus section read from the corpus itself, so a
27
+ # project-authored section (a custom feature added under
28
+ # .sdk/test/feature/) runs without editing this file. Read eagerly:
29
+ # parametrize needs the names at collection time. An SDK generated
30
+ # without a listed feature still skips, not fails.
31
+ def _corpus_feature_names():
32
+ try:
33
+ with open(os.path.join(_TEST_DIR, "../../.sdk/test/test.json"), "r") as f:
34
+ return sorted((json.loads(f.read()).get("feature") or {}).keys())
35
+ except OSError:
36
+ return []
37
+
38
+
39
+ FEATURE_CORPUS_NAMES = _corpus_feature_names()
29
40
 
30
41
  # The standard operation names, in the order the runner prefers them.
31
42
  FEATURE_CORPUS_OPS = ["load", "list", "create", "update", "remove"]
@@ -19,9 +19,10 @@ require_relative "../ProjectName_sdk"
19
19
 
20
20
  class FeatureCorpusTest < Minitest::Test
21
21
 
22
- # Features with a corpus section. A name here with no section is a skip,
23
- # not a failure: an SDK generated without the feature has nothing to run.
24
- FEATURE_CORPUS_NAMES = ["cost"].freeze
22
+ # Features with a corpus section are read from the corpus itself (see
23
+ # test_feature_corpus), so a project-authored section - a custom feature
24
+ # added under .sdk/test/feature/ - runs without editing this file. An SDK
25
+ # generated without a listed feature still skips, not fails.
25
26
 
26
27
  # The standard operation names, in the order the runner prefers them.
27
28
  FEATURE_CORPUS_OPS = %w[load list create update remove].freeze
@@ -192,6 +193,13 @@ class FeatureCorpusTest < Minitest::Test
192
193
  end
193
194
 
194
195
  def record(client, name)
196
+ # A corpus key is any manifest item name, so it may carry characters
197
+ # (foo-bar, foo.2) that cannot form a Ruby instance-variable name. The
198
+ # probe uses this lookup to SKIP such a section, so answer nil rather
199
+ # than letting instance_variable_get raise NameError for a feature this
200
+ # SDK could never have stored under that name anyway.
201
+ return nil unless name.match?(/\A[A-Za-z0-9_]+\z/)
202
+
195
203
  client.instance_variable_get(:"@_#{name}")
196
204
  end
197
205
 
@@ -218,7 +226,7 @@ class FeatureCorpusTest < Minitest::Test
218
226
  end
219
227
 
220
228
  def test_feature_corpus
221
- FEATURE_CORPUS_NAMES.each do |name|
229
+ (corpus["feature"] || {}).keys.sort.each do |name|
222
230
  section = (corpus["feature"] || {})[name]
223
231
  next if section.nil?
224
232
 
@@ -69,9 +69,12 @@ module ProjectNameUtilities
69
69
  optspec = {
70
70
  "apikey" => "",
71
71
  "base" => "http://localhost:8000",
72
+ "secret" => "",
72
73
  "prefix" => "",
73
74
  "suffix" => "",
74
- "auth" => { "prefix" => "" },
75
+ # `basic` and `secret`: HTTP Basic Auth needs a second credential and
76
+ # a flag to say the pair is Basic rather than a single bearer token.
77
+ "auth" => { "prefix" => "", "basic" => false },
75
78
  "headers" => { "`$CHILD`" => "`$STRING`" },
76
79
  "allow" => {
77
80
  "method" => "GET,PUT,POST,PATCH,DELETE,OPTIONS",
@@ -43,6 +43,13 @@ function ownIdField(config: any, getpath: any, entityName: string): string {
43
43
  // key, so move on to the next op rather than guess from it.
44
44
  const lastPart = 0 < parts.length ? String(parts[parts.length - 1]) : ''
45
45
  if (lastPart.startsWith('{')) return lastPart.slice(1, -1)
46
+
47
+ // No path param at all (or the route ends in a literal, e.g.
48
+ // /orgs/{org}/private-registries/public-key): a single required QUERY
49
+ // param can still be the record's own key (e.g. GET /result?trace_id=).
50
+ const query = (best && best.args && best.args.query) || []
51
+ const reqdQuery = query.filter((q: any) => false !== q.reqd)
52
+ if (1 === reqdQuery.length) return String(reqdQuery[0].name)
46
53
  }
47
54
  return 'id'
48
55
  }
@@ -332,10 +339,17 @@ class TestFeature extends BaseFeature {
332
339
  }
333
340
  }
334
341
 
335
- const reqd = transform(
342
+ // Path AND query: a path-only read misses a query-addressed record
343
+ // (e.g. GET /result?trace_id=), which has no path param at all.
344
+ const reqdParams = transform(
336
345
  select(getpath(point, ['args', 'params']), { reqd: true }),
337
346
  ['`$EACH`', '', '`$KEY.name`']
338
347
  )
348
+ const reqdQuery = transform(
349
+ select(getpath(point, ['args', 'query']), { reqd: true }),
350
+ ['`$EACH`', '', '`$KEY.name`']
351
+ )
352
+ const reqd = [...(reqdParams || []), ...(reqdQuery || [])]
339
353
 
340
354
  const qand: any[] = []
341
355
  const q = { '`$AND`': qand }
@@ -44,11 +44,13 @@ function makeOptions(ctx: Context) {
44
44
  // Standard SDK option values.
45
45
  const optspec = {
46
46
  apikey: '',
47
+ secret: '',
47
48
  base: 'http://localhost:8000',
48
49
  prefix: '',
49
50
  suffix: '',
50
51
  auth: {
51
- prefix: ''
52
+ prefix: '',
53
+ basic: false
52
54
  },
53
55
  headers: {
54
56
  '`$CHILD`': '`$STRING`'
@@ -5,6 +5,7 @@ import { Context, Spec } from '../types'
5
5
  const HEADER_auth = 'authorization'
6
6
 
7
7
  const OPTION_apikey = 'apikey'
8
+ const OPTION_secret = 'secret'
8
9
 
9
10
  const NOTFOUND = '__NOTFOUND__'
10
11
 
@@ -40,6 +41,25 @@ function prepareAuth(ctx: Context): Spec | Error {
40
41
 
41
42
  const apikey = getprop(options, OPTION_apikey, NOTFOUND)
42
43
 
44
+ // True HTTP Basic Auth needs TWO credentials, base64-joined - a single
45
+ // token in the header (the branch below) can never authenticate against
46
+ // an API that actually checks `Authorization: Basic base64(user:pass)`.
47
+ if (true === options.auth.basic) {
48
+ const secret = getprop(options, OPTION_secret, NOTFOUND)
49
+ const noApikey = NOTFOUND === apikey || null == apikey || '' === apikey
50
+ const noSecret = NOTFOUND === secret || null == secret || '' === secret
51
+
52
+ if (noApikey || noSecret) {
53
+ delprop(headers, HEADER_auth)
54
+ }
55
+ else {
56
+ const b64 = Buffer.from(apikey + ':' + secret).toString('base64')
57
+ setprop(headers, HEADER_auth, prefix ? prefix + ' ' + b64 : b64)
58
+ }
59
+
60
+ return spec
61
+ }
62
+
43
63
  if (NOTFOUND === apikey || null == apikey || '' === apikey) {
44
64
  delprop(headers, HEADER_auth)
45
65
  }