@voxgig/sdkgen 4.4.0 → 4.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/voxgig-sdkgen +1 -1
- package/dist/cmp/FeatureDocs.d.ts +16 -0
- package/dist/cmp/FeatureDocs.js +72 -0
- package/dist/cmp/FeatureDocs.js.map +1 -0
- package/dist/cmp/Readme.js +2 -0
- package/dist/cmp/Readme.js.map +1 -1
- package/dist/cmp/ReadmeFeatures.d.ts +2 -0
- package/dist/cmp/ReadmeFeatures.js +95 -0
- package/dist/cmp/ReadmeFeatures.js.map +1 -0
- package/dist/cmp/ReadmeRef.js +4 -0
- package/dist/cmp/ReadmeRef.js.map +1 -1
- package/dist/cmp/ReadmeRefFeatures.d.ts +2 -0
- package/dist/cmp/ReadmeRefFeatures.js +130 -0
- package/dist/cmp/ReadmeRefFeatures.js.map +1 -0
- package/dist/cmp/ReadmeTop.js +46 -0
- package/dist/cmp/ReadmeTop.js.map +1 -1
- package/dist/helpers/opShape.js +5 -1
- package/dist/helpers/opShape.js.map +1 -1
- package/dist/sdkgen.d.ts +3 -2
- package/dist/sdkgen.js +30 -10
- package/dist/sdkgen.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/utility.d.ts +2 -1
- package/dist/utility.js +17 -1
- package/dist/utility.js.map +1 -1
- package/package.json +1 -1
- package/project/.sdk/src/cmp/c/ReadmeRef_c.ts +5 -0
- package/project/.sdk/src/cmp/clojure/ReadmeRef_clojure.ts +5 -0
- package/project/.sdk/src/cmp/cpp/ReadmeRef_cpp.ts +5 -0
- package/project/.sdk/src/cmp/csharp/ReadmeRef_csharp.ts +5 -0
- package/project/.sdk/src/cmp/dart/ReadmeRef_dart.ts +5 -0
- package/project/.sdk/src/cmp/elixir/ReadmeRef_elixir.ts +5 -0
- package/project/.sdk/src/cmp/go/ReadmeRef_go.ts +5 -0
- package/project/.sdk/src/cmp/go/TestDirect_go.ts +123 -2
- package/project/.sdk/src/cmp/java/ReadmeRef_java.ts +5 -0
- package/project/.sdk/src/cmp/js/ReadmeRef_js.ts +5 -0
- package/project/.sdk/src/cmp/js/TestDirect_js.ts +62 -0
- package/project/.sdk/src/cmp/kotlin/ReadmeRef_kotlin.ts +5 -0
- package/project/.sdk/src/cmp/lean/ReadmeRef_lean.ts +27 -3
- package/project/.sdk/src/cmp/lua/ReadmeRef_lua.ts +5 -0
- package/project/.sdk/src/cmp/ocaml/Config_ocaml.ts +13 -1
- package/project/.sdk/src/cmp/ocaml/ReadmeRef_ocaml.ts +5 -0
- package/project/.sdk/src/cmp/perl/ReadmeRef_perl.ts +5 -0
- package/project/.sdk/src/cmp/php/ReadmeRef_php.ts +5 -0
- package/project/.sdk/src/cmp/py/ReadmeRef_py.ts +5 -0
- package/project/.sdk/src/cmp/rb/ReadmeRef_rb.ts +5 -0
- package/project/.sdk/src/cmp/rust/ReadmeRef_rust.ts +5 -0
- package/project/.sdk/src/cmp/scala/ReadmeRef_scala.ts +5 -0
- package/project/.sdk/src/cmp/seneca-provider/Extras_seneca-provider.ts +19 -1
- package/project/.sdk/src/cmp/seneca-provider/Main_seneca-provider.ts +27 -2
- package/project/.sdk/src/cmp/swift/ReadmeRef_swift.ts +5 -0
- package/project/.sdk/src/cmp/ts/Config_ts.ts +4 -1
- package/project/.sdk/src/cmp/ts/ReadmeHowto_ts.ts +9 -4
- package/project/.sdk/src/cmp/ts/ReadmeModel_ts.ts +8 -3
- package/project/.sdk/src/cmp/ts/ReadmeQuick_ts.ts +3 -2
- package/project/.sdk/src/cmp/ts/ReadmeRef_ts.ts +7 -2
- package/project/.sdk/src/cmp/ts/ReadmeTopQuick_ts.ts +3 -2
- package/project/.sdk/src/cmp/ts/TestDirect_ts.ts +5 -2
- package/project/.sdk/src/cmp/ts/TestEntity_ts.ts +5 -2
- package/project/.sdk/src/cmp/zig/ReadmeRef_zig.ts +5 -0
- package/project/.sdk/tm/clojure/src/sdk/features.clj +175 -0
- package/project/.sdk/tm/dart/test/feature_test.dart +54 -0
- package/project/.sdk/tm/go/feature/test_feature.go +17 -6
- package/project/.sdk/tm/go/test/feature_corpus_test.go +28 -3
- package/project/.sdk/tm/java/test/FeatureCorpusTest.java +13 -4
- package/project/.sdk/tm/js/src/feature/test/TestFeature.js +8 -1
- package/project/.sdk/tm/js/src/utility/MakeOptionsUtility.js +3 -1
- package/project/.sdk/tm/js/test/feature/Corpus.test.js +19 -4
- package/project/.sdk/tm/js/test/feature.test.js +19 -9
- package/project/.sdk/tm/lean/src/SdkFeatures.lean +186 -1
- package/project/.sdk/tm/lua/feature/test_feature.lua +21 -7
- package/project/.sdk/tm/lua/test/feature_test.lua +18 -18
- package/project/.sdk/tm/lua/utility/make_options.lua +2 -0
- package/project/.sdk/tm/ocaml/sdk_features.ml +229 -0
- package/project/.sdk/tm/perl/t/feature_corpus.t +14 -5
- package/project/.sdk/tm/php/feature/TestFeature.php +19 -8
- package/project/.sdk/tm/php/test/FeatureCorpusTest.php +18 -4
- package/project/.sdk/tm/php/utility/MakeOptions.php +2 -1
- package/project/.sdk/tm/py/pkg/feature/test_feature.py +15 -4
- package/project/.sdk/tm/py/pkg/utility/make_options.py +2 -0
- package/project/.sdk/tm/py/test/feature_harness.py +6 -0
- package/project/.sdk/tm/py/test/test_feature.py +40 -0
- package/project/.sdk/tm/py/test/test_feature_corpus.py +20 -3
- package/project/.sdk/tm/rb/test/feature_corpus_test.rb +18 -5
- package/project/.sdk/tm/rust/feature/cost.rs +25 -1
- package/project/.sdk/tm/ts/src/feature/test/TestFeature.ts +15 -1
- package/project/.sdk/tm/ts/src/utility/MakeOptionsUtility.ts +3 -1
- package/project/.sdk/tm/ts/src/utility/PrepareAuthUtility.ts +20 -0
- package/project/.sdk/tm/ts/test/feature/Corpus.test.ts +25 -4
- package/project/.sdk/tm/ts/test/feature.test.ts +19 -9
- package/project/sdkgen-package.json +1 -1
- package/src/cmp/FeatureDocs.ts +100 -0
- package/src/cmp/Readme.ts +2 -0
- package/src/cmp/ReadmeFeatures.ts +113 -0
- package/src/cmp/ReadmeRef.ts +4 -0
- package/src/cmp/ReadmeRefFeatures.ts +149 -0
- package/src/cmp/ReadmeTop.ts +48 -0
- package/src/helpers/opShape.ts +5 -1
- package/src/sdkgen.ts +28 -7
- package/src/utility.ts +21 -1
|
@@ -61,11 +61,21 @@ function TestFeature:init(ctx, options)
|
|
|
61
61
|
if type(restf) ~= "string" then
|
|
62
62
|
return data
|
|
63
63
|
end
|
|
64
|
-
local
|
|
65
|
-
if
|
|
64
|
+
local path = string.match(restf, "^`body%.(.+)`$")
|
|
65
|
+
if path == nil then
|
|
66
66
|
return data
|
|
67
67
|
end
|
|
68
|
-
|
|
68
|
+
-- Multi-segment on purpose: GraphQL ops unwrap body.data.<field> (and
|
|
69
|
+
-- body.data.<field>.<entity> for mutations), not just one level.
|
|
70
|
+
local segs = {}
|
|
71
|
+
for seg in string.gmatch(path, "[^.]+") do
|
|
72
|
+
table.insert(segs, seg)
|
|
73
|
+
end
|
|
74
|
+
local out = data
|
|
75
|
+
for i = #segs, 1, -1 do
|
|
76
|
+
out = { [segs[i]] = out }
|
|
77
|
+
end
|
|
78
|
+
return out
|
|
69
79
|
end
|
|
70
80
|
|
|
71
81
|
local function respond(status, data, extra)
|
|
@@ -309,10 +319,14 @@ function TestFeature:build_args(ctx, op, args)
|
|
|
309
319
|
local points = vs.getpath(ctx.config, "entity." .. ctx.entity:get_name() .. ".op." .. opname .. ".points")
|
|
310
320
|
local point = vs.getelem(points, -1)
|
|
311
321
|
|
|
312
|
-
--
|
|
322
|
+
-- Path AND query: a path-only read misses a query-addressed record
|
|
323
|
+
-- (e.g. GET /result?trace_id=), which has no path param at all.
|
|
313
324
|
local params_path = vs.getpath(point, "args.params")
|
|
314
325
|
local reqd_params = vs.select(params_path, { reqd = true })
|
|
315
|
-
local
|
|
326
|
+
local reqd_from_params = vs.transform(reqd_params, { "`$EACH`", "", "`$KEY.name`" })
|
|
327
|
+
local query_path = vs.getpath(point, "args.query")
|
|
328
|
+
local reqd_query = vs.select(query_path, { reqd = true })
|
|
329
|
+
local reqd_from_query = vs.transform(reqd_query, { "`$EACH`", "", "`$KEY.name`" })
|
|
316
330
|
|
|
317
331
|
local qand = {}
|
|
318
332
|
local q = { ["`$AND`"] = qand }
|
|
@@ -322,8 +336,8 @@ function TestFeature:build_args(ctx, op, args)
|
|
|
322
336
|
if keys ~= nil then
|
|
323
337
|
for _, key in ipairs(keys) do
|
|
324
338
|
local is_id = (key == "id")
|
|
325
|
-
local
|
|
326
|
-
|
|
339
|
+
local is_reqd = not vs.isempty(vs.select(reqd_from_params, key))
|
|
340
|
+
or not vs.isempty(vs.select(reqd_from_query, key))
|
|
327
341
|
|
|
328
342
|
if is_id or is_reqd then
|
|
329
343
|
local v = ctx.utility.param(ctx, key)
|
|
@@ -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`",
|
|
@@ -592,6 +592,235 @@ let audit_feature () : feature =
|
|
|
592
592
|
| _ -> ());
|
|
593
593
|
f
|
|
594
594
|
|
|
595
|
+
(* ------------------------------------------------------------------ *)
|
|
596
|
+
(* cost *)
|
|
597
|
+
(* ------------------------------------------------------------------ *)
|
|
598
|
+
(* Prices every transport ATTEMPT and commits the spend once per
|
|
599
|
+
OPERATION. Mirrors tm/ts/src/feature/cost/CostFeature.ts; the corpus
|
|
600
|
+
cases are .sdk/test/feature/cost.aon.
|
|
601
|
+
|
|
602
|
+
ORDER MATTERS. Cost must sit INSIDE the cache, or a response served from
|
|
603
|
+
cache is charged for money that was never spent. Activate in array form
|
|
604
|
+
with cost first: [{ name: 'cost' }, { name: 'cache' }]. *)
|
|
605
|
+
|
|
606
|
+
let cost_feature () : feature =
|
|
607
|
+
let options = ref (empty_map ()) in
|
|
608
|
+
(* Per-context accumulation: attempts are priced as they happen and the
|
|
609
|
+
total is committed once, at PreDone. *)
|
|
610
|
+
let pending : (string, value) Hashtbl.t = Hashtbl.create 8 in
|
|
611
|
+
let seq = ref 0 in
|
|
612
|
+
let f = { f_name = "cost"; f_version = "0.0.1"; f_active = true; f_options = Noval;
|
|
613
|
+
f_init = (fun _ _ -> ()); f_hook = (fun _ _ -> ()) } in
|
|
614
|
+
|
|
615
|
+
let per_unit () = opt_num !options "perUnit" ~default:0. in
|
|
616
|
+
let limit () = opt_num !options "budget" ~default:0. in
|
|
617
|
+
|
|
618
|
+
let record ctx =
|
|
619
|
+
let cl = cc ctx in
|
|
620
|
+
track_bucket cl "cost" (fun () ->
|
|
621
|
+
jo [("currency", Str (opt_str !options "currency" ~default:"USD"));
|
|
622
|
+
("total", jo [("calls", Num 0.); ("attempts", Num 0.); ("amount", Num 0.);
|
|
623
|
+
("reported", Num 0.); ("estimated", Num 0.)]);
|
|
624
|
+
("ops", empty_map ()); ("actors", empty_map ());
|
|
625
|
+
("budget", jo [("limit", Num (limit ())); ("spent", Num 0.);
|
|
626
|
+
("remaining", Num (limit ())); ("exceeded", Bool false)]);
|
|
627
|
+
("last", Noval)]) in
|
|
628
|
+
|
|
629
|
+
let new_pending () =
|
|
630
|
+
jo [("attempts", Num 0.); ("amount", Num 0.); ("reported", Num 0.);
|
|
631
|
+
("estimated", Num 0.); ("source", Str "none"); ("piped", Bool false)] in
|
|
632
|
+
|
|
633
|
+
let pending_for ctx =
|
|
634
|
+
match Hashtbl.find_opt pending ctx.c_id with
|
|
635
|
+
| Some p -> p
|
|
636
|
+
| None -> let p = new_pending () in Hashtbl.replace pending ctx.c_id p; p in
|
|
637
|
+
|
|
638
|
+
(* A header figure is server-stated, so it beats the table and the unit. *)
|
|
639
|
+
let price_header res =
|
|
640
|
+
let name = opt_str !options "header" ~default:"" in
|
|
641
|
+
if name = "" then None
|
|
642
|
+
else match header_ci (getp res "headers") name with
|
|
643
|
+
| Noval | Null -> None
|
|
644
|
+
| v -> (match float_of_string_opt (vstr_of v) with
|
|
645
|
+
| Some n -> Some (n *. per_unit (), "header")
|
|
646
|
+
| None -> None) in
|
|
647
|
+
|
|
648
|
+
(* Same lookup grammar as rbac's rules: '<entity>.<op>', then '<op>',
|
|
649
|
+
then '*'. *)
|
|
650
|
+
let price_rate ctx =
|
|
651
|
+
let rates = match getp !options "rates" with Map _ as m -> m | _ -> empty_map () in
|
|
652
|
+
let entity = match ctx.c_entity with
|
|
653
|
+
| Some e -> e.e_name
|
|
654
|
+
| None -> ctx.c_op.op_entity in
|
|
655
|
+
let opname = ctx.c_op.op_name in
|
|
656
|
+
let pick k = match getp rates k with Num n -> Some n | _ -> None in
|
|
657
|
+
match pick (entity ^ "." ^ opname) with
|
|
658
|
+
| Some n -> Some (n, "table")
|
|
659
|
+
| None -> (match pick opname with
|
|
660
|
+
| Some n -> Some (n, "table")
|
|
661
|
+
| None -> (match pick "*" with Some n -> Some (n, "table") | None -> None)) in
|
|
662
|
+
|
|
663
|
+
let price ctx res =
|
|
664
|
+
match price_header res with
|
|
665
|
+
| Some p -> p
|
|
666
|
+
| None ->
|
|
667
|
+
match price_rate ctx with
|
|
668
|
+
| Some p -> p
|
|
669
|
+
| None ->
|
|
670
|
+
let unit = opt_num !options "unit" ~default:0. in
|
|
671
|
+
if unit <> 0. then (unit, "unit") else (0., "none") in
|
|
672
|
+
|
|
673
|
+
(* A usage figure from the parsed result body, priced by perUnit. Read at
|
|
674
|
+
commit, not at the transport seam, because the body is one-shot. *)
|
|
675
|
+
let price_body ctx =
|
|
676
|
+
let path = opt_str !options "path" ~default:"" in
|
|
677
|
+
if path = "" then None
|
|
678
|
+
else match ctx.c_result with
|
|
679
|
+
| None -> None
|
|
680
|
+
| Some rt ->
|
|
681
|
+
(match rt.rt_body with
|
|
682
|
+
| Map _ ->
|
|
683
|
+
(match getpath_s rt.rt_body path with
|
|
684
|
+
| Num n -> Some (n *. per_unit ())
|
|
685
|
+
| Str s -> (match float_of_string_opt s with
|
|
686
|
+
| Some n -> Some (n *. per_unit ()) | None -> None)
|
|
687
|
+
| _ -> None)
|
|
688
|
+
| _ -> None) in
|
|
689
|
+
|
|
690
|
+
let spend rec_ amount reported estimated =
|
|
691
|
+
let total = getp rec_ "total" in
|
|
692
|
+
bump_num total "amount" amount;
|
|
693
|
+
bump_num total "reported" reported;
|
|
694
|
+
bump_num total "estimated" estimated;
|
|
695
|
+
let budget = getp rec_ "budget" in
|
|
696
|
+
let lim = match getp budget "limit" with Num n -> n | _ -> 0. in
|
|
697
|
+
let spent = match getp total "amount" with Num n -> n | _ -> 0. in
|
|
698
|
+
setp budget "spent" (Num spent);
|
|
699
|
+
setp budget "remaining" (Num (if lim > 0. then max 0. (lim -. spent) else 0.));
|
|
700
|
+
if lim > 0. && spent >= lim then setp budget "exceeded" (Bool true) in
|
|
701
|
+
|
|
702
|
+
let bump bucket key amount =
|
|
703
|
+
let b = match getp bucket key with
|
|
704
|
+
| Map _ as m -> m
|
|
705
|
+
| _ -> let m = jo [("calls", Num 0.); ("amount", Num 0.)] in setp bucket key m; m in
|
|
706
|
+
bump_num b "calls" 1.;
|
|
707
|
+
bump_num b "amount" amount in
|
|
708
|
+
|
|
709
|
+
let commit ctx p entity opname =
|
|
710
|
+
let rec_ = record ctx in
|
|
711
|
+
let amount = ref (match getp p "amount" with Num n -> n | _ -> 0.) in
|
|
712
|
+
let reported = ref (match getp p "reported" with Num n -> n | _ -> 0.) in
|
|
713
|
+
let estimated = ref (match getp p "estimated" with Num n -> n | _ -> 0.) in
|
|
714
|
+
let source = ref (vstr_of (getp p "source")) in
|
|
715
|
+
|
|
716
|
+
(* A body figure prices the whole call, so it REPLACES the per-attempt
|
|
717
|
+
estimate rather than adding to it - and, being server-stated, the
|
|
718
|
+
whole amount counts as reported. *)
|
|
719
|
+
(match price_body ctx with
|
|
720
|
+
| Some b -> amount := b; reported := b; estimated := 0.; source := "body"
|
|
721
|
+
| None -> ());
|
|
722
|
+
|
|
723
|
+
spend rec_ !amount !reported !estimated;
|
|
724
|
+
|
|
725
|
+
let actor = match ctx.c_ctrl.ctrl_actor with
|
|
726
|
+
| Str a when a <> "" -> a
|
|
727
|
+
| _ -> opt_str !options "actor" ~default:"anonymous" in
|
|
728
|
+
|
|
729
|
+
bump_num (getp rec_ "total") "calls" 1.;
|
|
730
|
+
bump (getp rec_ "ops") (entity ^ "." ^ opname) !amount;
|
|
731
|
+
bump (getp rec_ "actors") actor !amount;
|
|
732
|
+
|
|
733
|
+
incr seq;
|
|
734
|
+
setp rec_ "last"
|
|
735
|
+
(jo [("seq", Num (float_of_int !seq)); ("entity", Str entity); ("op", Str opname);
|
|
736
|
+
("actor", Str actor); ("amount", Num !amount);
|
|
737
|
+
("currency", getp rec_ "currency"); ("source", Str !source);
|
|
738
|
+
("attempts", getp p "attempts")]) in
|
|
739
|
+
|
|
740
|
+
let finish ctx done_ =
|
|
741
|
+
if f.f_active then
|
|
742
|
+
match Hashtbl.find_opt pending ctx.c_id with
|
|
743
|
+
| None -> ()
|
|
744
|
+
| Some p ->
|
|
745
|
+
Hashtbl.remove pending ctx.c_id;
|
|
746
|
+
let attempts = match getp p "attempts" with Num n -> n | _ -> 0. in
|
|
747
|
+
(* A FAILED operation that made no attempt never reached the network:
|
|
748
|
+
the budget gate (or rbac, or an unresolvable endpoint) refused it.
|
|
749
|
+
Committing would count a call that never happened.
|
|
750
|
+
|
|
751
|
+
A SUCCEEDED operation that made no attempt is the opposite case: it
|
|
752
|
+
was served from the cache. That is a real call, and the fact that it
|
|
753
|
+
cost nothing is the whole point of ordering cost inside the cache. *)
|
|
754
|
+
if done_ || attempts > 0. then begin
|
|
755
|
+
let entity = if ctx.c_op.op_entity <> "" then ctx.c_op.op_entity else "_" in
|
|
756
|
+
let opname = if ctx.c_op.op_name <> "" then ctx.c_op.op_name else "_" in
|
|
757
|
+
commit ctx p entity opname
|
|
758
|
+
end in
|
|
759
|
+
|
|
760
|
+
let charge ctx url fetchdef inner =
|
|
761
|
+
(* A rejecting transport still costs an attempt: without this a run of
|
|
762
|
+
connection-level failures under `retry` would be charged nothing, and
|
|
763
|
+
an onBudget: 'deny' ceiling could never stop it. *)
|
|
764
|
+
let (res, err) = inner ctx url fetchdef in
|
|
765
|
+
let (amount, source) = price ctx res in
|
|
766
|
+
let p = pending_for ctx in
|
|
767
|
+
bump_num p "attempts" 1.;
|
|
768
|
+
bump_num p "amount" amount;
|
|
769
|
+
bump_num p (if source = "header" || source = "body" then "reported" else "estimated") amount;
|
|
770
|
+
setp p "source" (Str source);
|
|
771
|
+
bump_num (getp (record ctx) "total") "attempts" 1.;
|
|
772
|
+
|
|
773
|
+
(* direct() and graphql() dispatch no pipeline hooks at all - no PrePoint
|
|
774
|
+
to gate on and no PreDone to commit - so their spend is committed here
|
|
775
|
+
or never. `piped` is set by PrePoint, so its absence is the signal. *)
|
|
776
|
+
if getp p "piped" <> Bool true then begin
|
|
777
|
+
Hashtbl.remove pending ctx.c_id;
|
|
778
|
+
commit ctx p "_" "direct"
|
|
779
|
+
end;
|
|
780
|
+
(res, err) in
|
|
781
|
+
|
|
782
|
+
f.f_init <- (fun ctx opts ->
|
|
783
|
+
options := (match to_map opts with Map _ -> opts | _ -> empty_map ());
|
|
784
|
+
f.f_active <- opt_active opts;
|
|
785
|
+
if f.f_active then begin
|
|
786
|
+
Hashtbl.reset pending;
|
|
787
|
+
seq := 0;
|
|
788
|
+
ignore (record ctx);
|
|
789
|
+
let u = cu ctx in
|
|
790
|
+
let inner = u.u_fetcher in
|
|
791
|
+
u.u_fetcher <- (fun fctx url fd -> charge fctx url fd inner)
|
|
792
|
+
end);
|
|
793
|
+
|
|
794
|
+
f.f_hook <- (fun name ctx ->
|
|
795
|
+
if f.f_active then
|
|
796
|
+
match name with
|
|
797
|
+
| "PrePoint" ->
|
|
798
|
+
(* Mark the context as piped, so charge knows a PreDone is coming. *)
|
|
799
|
+
let p = pending_for ctx in
|
|
800
|
+
setp p "piped" (Bool true);
|
|
801
|
+
let lim = limit () in
|
|
802
|
+
if lim > 0. then begin
|
|
803
|
+
let rec_ = record ctx in
|
|
804
|
+
let spent = match getp (getp rec_ "total") "amount" with Num n -> n | _ -> 0. in
|
|
805
|
+
if spent >= lim then begin
|
|
806
|
+
setp (getp rec_ "budget") "exceeded" (Bool true);
|
|
807
|
+
if opt_str !options "onBudget" ~default:"warn" = "deny" then begin
|
|
808
|
+
let err = ctx_make_error ctx "cost_budget"
|
|
809
|
+
("Cost budget of " ^ js_string (Num lim) ^ " " ^ vstr_of (getp rec_ "currency")
|
|
810
|
+
^ " is spent (" ^ js_string (Num spent) ^ " used)") in
|
|
811
|
+
Hashtbl.replace ctx.c_out "point" (OErr err)
|
|
812
|
+
end
|
|
813
|
+
end
|
|
814
|
+
end
|
|
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
|
|
821
|
+
| _ -> ());
|
|
822
|
+
f
|
|
823
|
+
|
|
595
824
|
(* ------------------------------------------------------------------ *)
|
|
596
825
|
(* clienttrack *)
|
|
597
826
|
(* ------------------------------------------------------------------ *)
|
|
@@ -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
|
|
33
|
-
#
|
|
34
|
-
|
|
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);
|
|
@@ -144,7 +145,15 @@ sub candidates {
|
|
|
144
145
|
};
|
|
145
146
|
}
|
|
146
147
|
}
|
|
147
|
-
|
|
148
|
+
|
|
149
|
+
# SAFE OPS FIRST - see the ts harness for the reasoning: the cache stores
|
|
150
|
+
# only successful GETs, so an SDK whose first usable op is a `create`
|
|
151
|
+
# (POST) can never satisfy "a hit served from cache costs nothing".
|
|
152
|
+
my %safe = (list => 0, load => 1);
|
|
153
|
+
return sort {
|
|
154
|
+
($safe{ $a->{op} } // 2) <=> ($safe{ $b->{op} } // 2)
|
|
155
|
+
|| $a->{key} cmp $b->{key}
|
|
156
|
+
} @out;
|
|
148
157
|
}
|
|
149
158
|
|
|
150
159
|
|
|
@@ -274,7 +283,7 @@ ok(scalar(@ops) > 0,
|
|
|
274
283
|
exit 0;
|
|
275
284
|
};
|
|
276
285
|
|
|
277
|
-
for my $name (
|
|
286
|
+
for my $name (sort keys %{ $CORPUS->{feature} || {} }) {
|
|
278
287
|
my $section = ($CORPUS->{feature} || {})->{$name};
|
|
279
288
|
next unless defined $section;
|
|
280
289
|
|
|
@@ -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\.(
|
|
72
|
+
if (!preg_match('/^`body\.(.+)`$/', $restf, $m)) {
|
|
73
73
|
return $data;
|
|
74
74
|
}
|
|
75
|
-
|
|
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
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
if ($
|
|
442
|
-
$
|
|
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
|
|
27
|
-
//
|
|
28
|
-
|
|
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'];
|
|
@@ -145,6 +146,17 @@ class FeatureCorpusTest extends TestCase
|
|
|
145
146
|
}
|
|
146
147
|
}
|
|
147
148
|
}
|
|
149
|
+
|
|
150
|
+
// SAFE OPS FIRST — see the ts harness for the reasoning: the cache
|
|
151
|
+
// stores only successful GETs, so an SDK whose first usable op is a
|
|
152
|
+
// `create` (POST) can never satisfy "a hit served from cache costs
|
|
153
|
+
// nothing".
|
|
154
|
+
$safe = ['list' => 0, 'load' => 1];
|
|
155
|
+
usort($out, function ($a, $b) use ($safe) {
|
|
156
|
+
$ra = $safe[$a['op']] ?? 2;
|
|
157
|
+
$rb = $safe[$b['op']] ?? 2;
|
|
158
|
+
return $ra === $rb ? strcmp($a['key'], $b['key']) : $ra - $rb;
|
|
159
|
+
});
|
|
148
160
|
return $out;
|
|
149
161
|
}
|
|
150
162
|
|
|
@@ -289,7 +301,9 @@ class FeatureCorpusTest extends TestCase
|
|
|
289
301
|
. 'the corpus (create-sdkgen .sdk/test/feature/) to run these cases');
|
|
290
302
|
}
|
|
291
303
|
|
|
292
|
-
|
|
304
|
+
$names = array_keys(self::corpus()['feature'] ?? []);
|
|
305
|
+
sort($names);
|
|
306
|
+
foreach ($names as $name) {
|
|
293
307
|
$section = self::corpus()['feature'][$name] ?? null;
|
|
294
308
|
if (null === $section) {
|
|
295
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
|
-
|
|
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
|
-
#
|
|
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
|
-
|
|
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`",
|
|
@@ -165,6 +165,12 @@ class _Ctx:
|
|
|
165
165
|
self.meta = {}
|
|
166
166
|
self.op = op
|
|
167
167
|
self.entity = entity
|
|
168
|
+
# The pipeline always resolves a point before a feature sees the ctx,
|
|
169
|
+
# so features read ctx.point freely — paging checks `point.kind` for
|
|
170
|
+
# graphql. This stub had no such attribute, so every paging test died
|
|
171
|
+
# with AttributeError before reaching the transport. Those tests were
|
|
172
|
+
# SKIPPED until an SDK activated paging, which is why it went unseen.
|
|
173
|
+
self.point = {}
|
|
168
174
|
self.spec = None
|
|
169
175
|
self.response = None
|
|
170
176
|
self.result = None
|