@voxgig/sdkgen 4.3.0 → 4.4.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.
- package/bin/voxgig-sdkgen +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/project/.sdk/src/cmp/ocaml/Config_ocaml.ts +13 -1
- package/project/.sdk/src/cmp/zig/Config_zig.ts +1 -1
- package/project/.sdk/tm/c/tests/primary_corpus_test.c +565 -0
- package/project/.sdk/tm/c/utility/prepare_method.c +3 -1
- package/project/.sdk/tm/c/utility/transform_request.c +4 -3
- package/project/.sdk/tm/clojure/src/sdk/core.clj +55 -5
- package/project/.sdk/tm/clojure/test/sdk/test/primary.clj +171 -1
- package/project/.sdk/tm/clojure/test/sdk/test/struct_corpus.clj +13 -2
- package/project/.sdk/tm/clojure/test/sdk/test_runner.clj +2 -0
- package/project/.sdk/tm/csharp/test/CustomUtilityTest.cs +104 -0
- package/project/.sdk/tm/csharp/utility/MakeOptions.cs +82 -2
- package/project/.sdk/tm/elixir/lib/projectname/utility.ex +50 -2
- package/project/.sdk/tm/elixir/test/primary_utility_test.exs +18 -200
- package/project/.sdk/tm/elixir/test/support/struct_corpus.ex +413 -2
- package/project/.sdk/tm/go/test/feature_corpus_test.go +18 -0
- package/project/.sdk/tm/java/test/FeatureCorpusTest.java +8 -0
- package/project/.sdk/tm/js/test/feature/Corpus.test.js +6 -1
- package/project/.sdk/tm/kotlin/utility/MakeOptions.kt +56 -2
- package/project/.sdk/tm/lua/utility/make_options.lua +22 -2
- package/project/.sdk/tm/ocaml/Makefile +15 -4
- package/project/.sdk/tm/ocaml/sdk_features.ml +224 -0
- package/project/.sdk/tm/ocaml/sdk_runtime.ml +8 -1
- package/project/.sdk/tm/ocaml/test/corpus_runner.ml +185 -0
- package/project/.sdk/tm/ocaml/test/primary_utility_test.ml +238 -0
- package/project/.sdk/tm/ocaml/test/struct_corpus.ml +5 -160
- package/project/.sdk/tm/perl/t/feature_corpus.t +9 -1
- package/project/.sdk/tm/php/test/FeatureCorpusTest.php +11 -0
- package/project/.sdk/tm/py/test/feature_harness.py +6 -0
- package/project/.sdk/tm/py/test/test_feature_corpus.py +6 -0
- package/project/.sdk/tm/rb/test/feature_corpus_test.rb +6 -1
- package/project/.sdk/tm/rust/feature/cost.rs +25 -1
- package/project/.sdk/tm/scala/Makefile +1 -0
- package/project/.sdk/tm/scala/sdktest/PrimaryCorpus.scala +294 -0
- package/project/.sdk/tm/scala/sdktest/StructCorpus.scala +0 -0
- package/project/.sdk/tm/scala/utility/Make.scala +49 -2
- package/project/.sdk/tm/scala/utility/Prepare.scala +3 -1
- package/project/.sdk/tm/swift/Sources/ProjectNameSDK/utility/MakeOptions.swift +21 -3
- package/project/.sdk/tm/ts/src/feature/test/TestFeature.ts +13 -2
- package/project/.sdk/tm/ts/test/feature/Corpus.test.ts +12 -1
- package/project/.sdk/tm/zig/core/utility.zig +8 -1
- package/project/.sdk/tm/zig/test/primary_utility_test.zig +445 -0
- package/project/.sdk/tm/zig/test/struct_runner.zig +238 -0
- package/project/sdkgen-package.json +1 -1
|
@@ -20,6 +20,7 @@ import (
|
|
|
20
20
|
"errors"
|
|
21
21
|
"fmt"
|
|
22
22
|
"reflect"
|
|
23
|
+
"sort"
|
|
23
24
|
"strings"
|
|
24
25
|
"testing"
|
|
25
26
|
|
|
@@ -184,6 +185,23 @@ func fcCandidates(client *sdk.ProjectNameSDK) []fcOp {
|
|
|
184
185
|
})
|
|
185
186
|
}
|
|
186
187
|
}
|
|
188
|
+
|
|
189
|
+
// SAFE OPS FIRST — see the ts harness for the reasoning: the cache stores
|
|
190
|
+
// only successful GETs, so an SDK whose first usable op is a `create`
|
|
191
|
+
// (POST) can never satisfy "a hit served from cache costs nothing".
|
|
192
|
+
safe := map[string]int{"list": 0, "load": 1}
|
|
193
|
+
rank := func(o fcOp) int {
|
|
194
|
+
if r, ok := safe[strings.ToLower(o.method)]; ok {
|
|
195
|
+
return r
|
|
196
|
+
}
|
|
197
|
+
return 2
|
|
198
|
+
}
|
|
199
|
+
sort.SliceStable(out, func(i, j int) bool {
|
|
200
|
+
if rank(out[i]) != rank(out[j]) {
|
|
201
|
+
return rank(out[i]) < rank(out[j])
|
|
202
|
+
}
|
|
203
|
+
return out[i].key < out[j].key
|
|
204
|
+
})
|
|
187
205
|
return out
|
|
188
206
|
}
|
|
189
207
|
|
|
@@ -182,6 +182,14 @@ public class FeatureCorpusTest {
|
|
|
182
182
|
out.add(new Op(e.getKey() + "." + opname, accessor, call));
|
|
183
183
|
}
|
|
184
184
|
}
|
|
185
|
+
|
|
186
|
+
// SAFE OPS FIRST — see the ts harness for the reasoning: the cache stores
|
|
187
|
+
// only successful GETs, so an SDK whose first usable op is a `create`
|
|
188
|
+
// (POST) can never satisfy "a hit served from cache costs nothing".
|
|
189
|
+
java.util.Map<String, Integer> safe = java.util.Map.of("list", 0, "load", 1);
|
|
190
|
+
out.sort(java.util.Comparator
|
|
191
|
+
.<Op>comparingInt(o -> safe.getOrDefault(o.key.substring(o.key.indexOf('.') + 1), 2))
|
|
192
|
+
.thenComparing(o -> o.key));
|
|
185
193
|
return out;
|
|
186
194
|
}
|
|
187
195
|
|
|
@@ -109,7 +109,12 @@ function candidates(client) {
|
|
|
109
109
|
out.push({ key: entity + '.' + op, accessor: accessor[entity], entity, op })
|
|
110
110
|
}
|
|
111
111
|
}
|
|
112
|
-
|
|
112
|
+
// SAFE OPS FIRST — see the ts harness for the reasoning: the cache stores
|
|
113
|
+
// only successful GETs, so an SDK whose first usable op is a `create`
|
|
114
|
+
// (POST) can never satisfy "a hit served from cache costs nothing".
|
|
115
|
+
const SAFE = { list: 0, load: 1 }
|
|
116
|
+
return out.sort((a, b) =>
|
|
117
|
+
((SAFE[a.op] ?? 2) - (SAFE[b.op] ?? 2)) || a.key.localeCompare(b.key))
|
|
113
118
|
}
|
|
114
119
|
|
|
115
120
|
|
|
@@ -2,6 +2,7 @@ package KOTLINPACKAGE.utility
|
|
|
2
2
|
|
|
3
3
|
import KOTLINPACKAGE.core.Context
|
|
4
4
|
import KOTLINPACKAGE.core.Helpers
|
|
5
|
+
import KOTLINPACKAGE.core.Utility
|
|
5
6
|
import KOTLINPACKAGE.utility.struct.Struct
|
|
6
7
|
|
|
7
8
|
@Suppress("UNCHECKED_CAST")
|
|
@@ -11,11 +12,20 @@ fun makeOptions(ctx: Context): MutableMap<String, Any?> {
|
|
|
11
12
|
options = linkedMapOf()
|
|
12
13
|
}
|
|
13
14
|
|
|
14
|
-
// Merge
|
|
15
|
+
// Merge utility overrides from options onto the utility object.
|
|
15
16
|
// Read from original options before clone for parity with the donors.
|
|
17
|
+
//
|
|
18
|
+
// A key naming a real utility member REPLACES it; anything else is attached
|
|
19
|
+
// as a custom extra. Shelving everything in `custom` - a map nothing reads -
|
|
20
|
+
// made `utility = mapOf("fetcher" to ...)`, the documented transport seam, a
|
|
21
|
+
// silent no-op here while ts honoured it.
|
|
16
22
|
val customUtils = Helpers.toMapAny(options["utility"])
|
|
17
23
|
if (customUtils != null && ctx.utility != null) {
|
|
18
|
-
|
|
24
|
+
for ((key, value) in customUtils) {
|
|
25
|
+
if (!overrideUtil(ctx.utility!!, key, value)) {
|
|
26
|
+
ctx.utility!!.custom[key] = value
|
|
27
|
+
}
|
|
28
|
+
}
|
|
19
29
|
}
|
|
20
30
|
|
|
21
31
|
var opts = Struct.clone(options) as MutableMap<String, Any?>
|
|
@@ -165,3 +175,47 @@ fun makeOptions(ctx: Context): MutableMap<String, Any?> {
|
|
|
165
175
|
|
|
166
176
|
return opts
|
|
167
177
|
}
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Replaces one utility member from `options.utility`, matching the ts
|
|
182
|
+
* reference: a key naming a real member REPLACES it, and any other key is
|
|
183
|
+
* attached as a custom extra. Returns false when the key names no member or
|
|
184
|
+
* the value is not that member's type, so the caller keeps it in `custom`.
|
|
185
|
+
*
|
|
186
|
+
* REFLECTION, NOT A KEYED SWITCH. The go and java ports list every member by
|
|
187
|
+
* hand and carry a "keep this in step with registerAll" warning, because a
|
|
188
|
+
* utility added to one list and not the other is overridable there and not
|
|
189
|
+
* here. The field set is readable off the class, so the list cannot drift.
|
|
190
|
+
*
|
|
191
|
+
* Kotlin function types erase to FunctionN, so `isInstance` checks arity and
|
|
192
|
+
* not the full signature - the same limit java's port documents. A wrongly
|
|
193
|
+
* shaped value of the right arity is accepted, exactly as the dynamic donors
|
|
194
|
+
* accept whatever they are given.
|
|
195
|
+
*
|
|
196
|
+
* Only a PUBLIC name may replace a member: public utility names are camelCase
|
|
197
|
+
* and carry no underscore, so an underscore means the caller named something
|
|
198
|
+
* of their own rather than a member.
|
|
199
|
+
*/
|
|
200
|
+
internal fun overrideUtil(utility: Utility, key: String, value: Any?): Boolean {
|
|
201
|
+
if (key.isEmpty() || key.contains('_') || "custom" == key) {
|
|
202
|
+
return false
|
|
203
|
+
}
|
|
204
|
+
if (null == value) {
|
|
205
|
+
return false
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
val field = try {
|
|
209
|
+
Utility::class.java.getDeclaredField(key)
|
|
210
|
+
} catch (e: NoSuchFieldException) {
|
|
211
|
+
return false
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (!field.type.isInstance(value)) {
|
|
215
|
+
return false
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
field.isAccessible = true
|
|
219
|
+
field.set(utility, value)
|
|
220
|
+
return true
|
|
221
|
+
}
|
|
@@ -5,13 +5,33 @@ local vs = require("utility.struct.struct")
|
|
|
5
5
|
local function make_options_util(ctx)
|
|
6
6
|
local options = ctx.options or {}
|
|
7
7
|
|
|
8
|
-
-- Merge
|
|
8
|
+
-- Merge utility overrides from options onto the utility object.
|
|
9
|
+
--
|
|
10
|
+
-- A key naming a real utility member REPLACES it; anything else is attached
|
|
11
|
+
-- as a custom extra. Shelving everything in `custom` - a table nothing reads
|
|
12
|
+
-- - made `utility = { fetcher = ... }`, the documented transport seam, a
|
|
13
|
+
-- silent no-op here while ts honoured it.
|
|
14
|
+
--
|
|
15
|
+
-- Option keys are camelCase, as ts spells them; members here are
|
|
16
|
+
-- snake_case. Only a PUBLIC name may replace: public utility names carry no
|
|
17
|
+
-- underscore, so an underscore means the caller named something of their
|
|
18
|
+
-- own - possibly the internal spelling of a real member. `make_error` must
|
|
19
|
+
-- stay an extension, or a non-callable would break the error path on the
|
|
20
|
+
-- next request.
|
|
9
21
|
local custom_utils = vs.getprop(options, "utility")
|
|
10
22
|
if type(custom_utils) == "table" then
|
|
11
23
|
local utility = ctx.utility
|
|
12
24
|
if utility ~= nil then
|
|
13
25
|
for key, val in pairs(custom_utils) do
|
|
14
|
-
|
|
26
|
+
local member = nil
|
|
27
|
+
if type(key) == "string" and not key:find("_") then
|
|
28
|
+
member = key:gsub("(%u)", function(c) return "_" .. c:lower() end)
|
|
29
|
+
end
|
|
30
|
+
if member ~= nil and member ~= "custom" and utility[member] ~= nil then
|
|
31
|
+
utility[member] = val
|
|
32
|
+
else
|
|
33
|
+
utility.custom[key] = val
|
|
34
|
+
end
|
|
15
35
|
end
|
|
16
36
|
end
|
|
17
37
|
end
|
|
@@ -34,11 +34,16 @@ SDK_TEST = $(RUNTIME) sdk_config.ml $(ENTITIES) sdk_client.ml sdk_error.ml \
|
|
|
34
34
|
$(ENTITY_TESTS) $(DOC_TESTS) test/t_main.ml
|
|
35
35
|
|
|
36
36
|
CORPUS_SRC = utility/vregex.ml utility/voxgig_struct.ml sdk_json.ml \
|
|
37
|
-
test/struct_corpus.ml
|
|
37
|
+
test/corpus_runner.ml test/struct_corpus.ml
|
|
38
38
|
|
|
39
|
-
|
|
39
|
+
# The primary-utility corpus needs the whole SDK, because it drives the SDK's
|
|
40
|
+
# own request-shaping utilities — not just the struct library.
|
|
41
|
+
PRIMARY_SRC = $(RUNTIME) sdk_config.ml $(ENTITIES) sdk_client.ml sdk_error.ml \
|
|
42
|
+
test/corpus_runner.ml test/primary_utility_test.ml
|
|
40
43
|
|
|
41
|
-
|
|
44
|
+
.PHONY: test test-sdk test-corpus test-primary build lint clean
|
|
45
|
+
|
|
46
|
+
test: test-sdk test-corpus test-primary
|
|
42
47
|
|
|
43
48
|
test-sdk: run_sdk_test
|
|
44
49
|
./run_sdk_test
|
|
@@ -50,7 +55,13 @@ run_sdk_test: $(SDK_TEST)
|
|
|
50
55
|
$(OCAMLC) $(INC) $(SDK_TEST) -o run_sdk_test
|
|
51
56
|
|
|
52
57
|
run_struct_corpus: $(CORPUS_SRC)
|
|
53
|
-
$(OCAMLC) -I utility $(CORPUS_SRC) -o run_struct_corpus
|
|
58
|
+
$(OCAMLC) -I utility -I test $(CORPUS_SRC) -o run_struct_corpus
|
|
59
|
+
|
|
60
|
+
test-primary: run_primary_corpus
|
|
61
|
+
./run_primary_corpus $(CORPUS)
|
|
62
|
+
|
|
63
|
+
run_primary_corpus: $(PRIMARY_SRC)
|
|
64
|
+
$(OCAMLC) $(INC) $(PRIMARY_SRC) -o run_primary_corpus
|
|
54
65
|
|
|
55
66
|
# Type-check the SDK library (a clean compile means it is sound).
|
|
56
67
|
build: lint
|
|
@@ -592,6 +592,230 @@ 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
|
+
| _ -> ());
|
|
817
|
+
f
|
|
818
|
+
|
|
595
819
|
(* ------------------------------------------------------------------ *)
|
|
596
820
|
(* clienttrack *)
|
|
597
821
|
(* ------------------------------------------------------------------ *)
|
|
@@ -345,7 +345,14 @@ let prepare_method_util (ctx : ctx) : string =
|
|
|
345
345
|
| _ ->
|
|
346
346
|
match ctx.c_op.op_name with
|
|
347
347
|
| "create" -> "POST" | "update" -> "PUT" | "load" -> "GET"
|
|
348
|
-
| "list" -> "GET" | "remove" -> "DELETE" | "patch" -> "PATCH"
|
|
348
|
+
| "list" -> "GET" | "remove" -> "DELETE" | "patch" -> "PATCH"
|
|
349
|
+
(* NO CATCH-ALL GET. The ts reference returns methodMap[key], which is
|
|
350
|
+
* undefined for an op the map does not name — the request is then rejected
|
|
351
|
+
* rather than silently issued. A `| _ -> "GET"` here turned every
|
|
352
|
+
* unrecognised op into a GET, which is both a divergence from the corpus
|
|
353
|
+
* (which expects no method for opname "bad") and the more dangerous of the
|
|
354
|
+
* two behaviours: a mistyped or unsupported op quietly fetched. *)
|
|
355
|
+
| _ -> ""
|
|
349
356
|
|
|
350
357
|
let prepare_headers_util (ctx : ctx) : value =
|
|
351
358
|
let options = client_options_map (cc ctx) in
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
(* Shared corpus harness for the ProjectName SDK.
|
|
2
|
+
*
|
|
3
|
+
* Extracted from struct_corpus.ml so the primary-utility suite can drive the
|
|
4
|
+
* SAME runner over test.json's "primary" section. OCaml runs a module's
|
|
5
|
+
* top-level effects on link, so a harness sitting beside a `let () = ...` main
|
|
6
|
+
* cannot be reused — linking it would run the struct corpus as a side effect.
|
|
7
|
+
* This module deliberately has no main.
|
|
8
|
+
*)
|
|
9
|
+
|
|
10
|
+
(* Test runner for the shared JSON corpus (build/test/test.json).
|
|
11
|
+
* Self-contained: an in-tree JSON reader builds the library's `value` type
|
|
12
|
+
* directly, so the OCaml port is exercised exactly as in production. *)
|
|
13
|
+
|
|
14
|
+
open Voxgig_struct
|
|
15
|
+
|
|
16
|
+
let nullmark = "__NULL__"
|
|
17
|
+
let undefmark = "__UNDEF__"
|
|
18
|
+
let existsmark = "__EXISTS__"
|
|
19
|
+
|
|
20
|
+
(* The JSON reader now lives in the runtime (sdk_json.ml) so the SDK's
|
|
21
|
+
* generated config can use it; the corpus runs against that same function
|
|
22
|
+
* rather than a second copy that could drift from it. *)
|
|
23
|
+
let json_read = Sdk_json.json_read
|
|
24
|
+
|
|
25
|
+
(* ---------------- fixJSON / equality ---------------- *)
|
|
26
|
+
|
|
27
|
+
let rec fix_json v flag_null =
|
|
28
|
+
match v with
|
|
29
|
+
| Noval | Null -> if flag_null then Str nullmark else v
|
|
30
|
+
| Map m -> let o = empty_map () in
|
|
31
|
+
List.iter (fun (k, x) -> ignore (setprop o (Str k) (fix_json x flag_null))) m.entries; o
|
|
32
|
+
| List r -> lst (List.map (fun x -> fix_json x flag_null) !r)
|
|
33
|
+
| _ -> v
|
|
34
|
+
|
|
35
|
+
(* Order-independent deep equality for maps; sequence equality for lists. *)
|
|
36
|
+
let rec eqv a b =
|
|
37
|
+
match a, b with
|
|
38
|
+
| (Noval | Null), (Noval | Null) -> true
|
|
39
|
+
| Bool x, Bool y -> x = y
|
|
40
|
+
| Num x, Num y -> x = y
|
|
41
|
+
| Str x, Str y -> x = y
|
|
42
|
+
| List x, List y -> List.length !x = List.length !y && List.for_all2 eqv !x !y
|
|
43
|
+
| Map x, Map y ->
|
|
44
|
+
omap_len x = omap_len y &&
|
|
45
|
+
List.for_all (fun (k, v) -> match omap_get y k with Some w -> eqv v w | None -> false) x.entries
|
|
46
|
+
| _ -> a == b
|
|
47
|
+
|
|
48
|
+
(* ---------------- match support ---------------- *)
|
|
49
|
+
|
|
50
|
+
let matchval check base =
|
|
51
|
+
let check = if check = Str undefmark || check = Str nullmark then Noval else check in
|
|
52
|
+
if eqv check base then true
|
|
53
|
+
else match check with
|
|
54
|
+
| Str cs ->
|
|
55
|
+
let basestr = stringify base in
|
|
56
|
+
if String.length cs >= 2 && cs.[0] = '/' && cs.[String.length cs - 1] = '/' then
|
|
57
|
+
Vregex.test_str (String.sub cs 1 (String.length cs - 2)) basestr
|
|
58
|
+
else
|
|
59
|
+
let low s = String.lowercase_ascii s in
|
|
60
|
+
let contains hay needle =
|
|
61
|
+
let hl = String.length hay and nl = String.length needle in
|
|
62
|
+
let rec go i = if i + nl > hl then false
|
|
63
|
+
else if String.sub hay i nl = needle then true else go (i + 1) in
|
|
64
|
+
nl = 0 || go 0 in
|
|
65
|
+
contains (low basestr) (low (stringify check))
|
|
66
|
+
| Func _ -> true
|
|
67
|
+
| _ -> false
|
|
68
|
+
|
|
69
|
+
let do_match check base =
|
|
70
|
+
let base = clone base in
|
|
71
|
+
ignore (walk ~before:(fun _k v _p path ->
|
|
72
|
+
(if not (isnode v) then begin
|
|
73
|
+
let baseval = getpath base path in
|
|
74
|
+
if eqv baseval v then ()
|
|
75
|
+
else if v = Str undefmark && is_nullish baseval then ()
|
|
76
|
+
else if v = Str existsmark && not (is_nullish baseval) then ()
|
|
77
|
+
else if not (matchval v baseval) then
|
|
78
|
+
raise (Struct_error (Printf.sprintf "MATCH: %s: [%s] <=> [%s]"
|
|
79
|
+
(String.concat "." (List.map js_string (match path with List r -> !r | _ -> [])))
|
|
80
|
+
(stringify v) (stringify baseval)))
|
|
81
|
+
end);
|
|
82
|
+
v) check)
|
|
83
|
+
|
|
84
|
+
(* ---------------- result tracking ---------------- *)
|
|
85
|
+
|
|
86
|
+
let npass = ref 0
|
|
87
|
+
let nfail = ref 0
|
|
88
|
+
let failures = ref []
|
|
89
|
+
|
|
90
|
+
let record group name ok msg =
|
|
91
|
+
if ok then incr npass
|
|
92
|
+
else (incr nfail; failures := Printf.sprintf "FAIL %s %s - %s" group name msg :: !failures)
|
|
93
|
+
|
|
94
|
+
(* ---------------- per-entry runner ---------------- *)
|
|
95
|
+
|
|
96
|
+
let omap_v kvs =
|
|
97
|
+
let m = empty_map () in
|
|
98
|
+
List.iter (fun (k, v) -> ignore (setprop m (Str k) v)) kvs; m
|
|
99
|
+
|
|
100
|
+
let getprop_raw_pub e k = (match e with Map m -> (match omap_get m k with Some x -> x | None -> Noval) | _ -> Noval)
|
|
101
|
+
let entry_get e k = getprop_raw_pub e k
|
|
102
|
+
let entry_has e k = match e with Map m -> omap_has m k | _ -> false
|
|
103
|
+
let default_injdef_pub () =
|
|
104
|
+
{ d_meta = Noval; d_extra = Noval; d_errs = Noval; d_modify = None; d_handler = None;
|
|
105
|
+
d_base = Noval; d_dparent = Noval; d_dpath = Noval; d_key = Noval }
|
|
106
|
+
|
|
107
|
+
let resolve_args entry =
|
|
108
|
+
if entry_has entry "ctx" then [entry_get entry "ctx"]
|
|
109
|
+
else if entry_has entry "args" then (
|
|
110
|
+
match entry_get entry "args" with
|
|
111
|
+
| List r ->
|
|
112
|
+
(* ts's resolveArgs writes the live first arg back as entry.ctx, so a
|
|
113
|
+
`match: {ctx: ...}` resolves for args-style entries too. Without it
|
|
114
|
+
every such assertion reads null and asserts nothing. *)
|
|
115
|
+
(match !r with
|
|
116
|
+
| (Map _ as first) :: _ -> ignore (setprop entry (Str "ctx") first)
|
|
117
|
+
| _ -> ());
|
|
118
|
+
!r
|
|
119
|
+
| _ -> [])
|
|
120
|
+
else if entry_has entry "in" then [clone (entry_get entry "in")]
|
|
121
|
+
else [Noval]
|
|
122
|
+
|
|
123
|
+
let check_result entry args res =
|
|
124
|
+
let matched = ref false in
|
|
125
|
+
(if entry_has entry "match" then begin
|
|
126
|
+
do_match (entry_get entry "match")
|
|
127
|
+
(omap_v ["in", entry_get entry "in"; "args", lst args;
|
|
128
|
+
"out", entry_get entry "res"; "ctx", entry_get entry "ctx"]);
|
|
129
|
+
matched := true
|
|
130
|
+
end);
|
|
131
|
+
let out = entry_get entry "out" in
|
|
132
|
+
if eqv out res then ()
|
|
133
|
+
else if !matched && (out = Str nullmark || is_nullish out) then ()
|
|
134
|
+
else raise (Struct_error (Printf.sprintf "Expected: %s, got: %s" (stringify out) (stringify res)))
|
|
135
|
+
|
|
136
|
+
let handle_error entry err =
|
|
137
|
+
let msg = (match err with Struct_error m -> m | e -> Printexc.to_string e) in
|
|
138
|
+
if entry_has entry "err" then begin
|
|
139
|
+
let entry_err = entry_get entry "err" in
|
|
140
|
+
if entry_err = Bool true || matchval entry_err (Str msg) then begin
|
|
141
|
+
if entry_has entry "match" then
|
|
142
|
+
do_match (entry_get entry "match")
|
|
143
|
+
(omap_v ["in", entry_get entry "in"; "out", entry_get entry "res";
|
|
144
|
+
(* ts hands do_match the ERROR OBJECT, so a corpus
|
|
145
|
+
`match: {err: {message: ...}}` resolves; a bare string
|
|
146
|
+
leaves err.message reading null. *)
|
|
147
|
+
"ctx", entry_get entry "ctx";
|
|
148
|
+
"err", omap_v ["message", Str msg]])
|
|
149
|
+
end else
|
|
150
|
+
raise (Struct_error (Printf.sprintf "ERROR MATCH: [%s] <=> [%s]" (stringify entry_err) msg))
|
|
151
|
+
end else raise err
|
|
152
|
+
|
|
153
|
+
let run_set ?(flags = []) group node subject =
|
|
154
|
+
let flag_null = (match List.assoc_opt "null" flags with Some b -> b | None -> true) in
|
|
155
|
+
let fixed = fix_json node flag_null in
|
|
156
|
+
let testset = (match getprop fixed (Str "set") with List r -> !r | _ -> []) in
|
|
157
|
+
List.iter (fun entry ->
|
|
158
|
+
let name = js_string (entry_get entry "name") in
|
|
159
|
+
try
|
|
160
|
+
(if not (entry_has entry "out") && flag_null then ignore (setprop entry (Str "out") (Str nullmark)));
|
|
161
|
+
let args = resolve_args entry in
|
|
162
|
+
let res = fix_json (subject args) flag_null in
|
|
163
|
+
ignore (setprop entry (Str "res") res);
|
|
164
|
+
check_result entry args res;
|
|
165
|
+
record group name true ""
|
|
166
|
+
with
|
|
167
|
+
| e ->
|
|
168
|
+
(try handle_error entry e; record group name true ""
|
|
169
|
+
with e2 -> record group name false
|
|
170
|
+
(match e2 with Struct_error m -> m | _ -> Printexc.to_string e2)))
|
|
171
|
+
testset
|
|
172
|
+
|
|
173
|
+
let run_single group node actual_fn =
|
|
174
|
+
try
|
|
175
|
+
let expected = getprop_raw_pub node "out" in
|
|
176
|
+
let actual = actual_fn (getprop_raw_pub node "in") in
|
|
177
|
+
if eqv expected actual then record group "single" true ""
|
|
178
|
+
else record group "single" false (Printf.sprintf "Expected: %s, got: %s" (stringify expected) (stringify actual))
|
|
179
|
+
with e -> record group "single" false (match e with Struct_error m -> m | _ -> Printexc.to_string e)
|
|
180
|
+
|
|
181
|
+
(* ---------------- arg helpers ---------------- *)
|
|
182
|
+
|
|
183
|
+
let arg1 f = fun args -> f (match args with x :: _ -> x | [] -> Noval)
|
|
184
|
+
let vget vin k = match vin with Map m -> (match omap_get m k with Some x -> x | None -> Noval) | _ -> Noval
|
|
185
|
+
let vhas vin k = match vin with Map m -> omap_has m k | _ -> false
|