@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
|
@@ -4,8 +4,100 @@
|
|
|
4
4
|
(:require [sdk.core :as core]
|
|
5
5
|
[sdk.client :as client]
|
|
6
6
|
[sdk.testutil :as t]
|
|
7
|
+
[sdk.test.struct-corpus :as corpus]
|
|
7
8
|
[voxgig.struct :as vs]
|
|
8
|
-
[clojure.string :as str])
|
|
9
|
+
[clojure.string :as str])
|
|
10
|
+
(:import [java.util Map]))
|
|
11
|
+
|
|
12
|
+
;; THE SHARED CORPUS.
|
|
13
|
+
;;
|
|
14
|
+
;; The checks below assert against inputs written here; this drives
|
|
15
|
+
;; .sdk/test/test.json -> "primary" through the same utilities, so the cases
|
|
16
|
+
;; cannot drift from the reference implementation. That is what moves clojure
|
|
17
|
+
;; from the MIRRORED parity tier to FULL — a mirrored suite cannot notice a
|
|
18
|
+
;; corpus case that changed, or one that was added.
|
|
19
|
+
|
|
20
|
+
(def ^:private CORPUS "../.sdk/test/test.json")
|
|
21
|
+
|
|
22
|
+
(defn- getspec [root & path]
|
|
23
|
+
(reduce (fn [acc k] (when acc (.get ^Map acc k))) root path))
|
|
24
|
+
|
|
25
|
+
(defn- lower-headers [m]
|
|
26
|
+
;; Header names arrive in any case and the contract is lowercase; the SDK
|
|
27
|
+
;; copies them verbatim, so the driver normalises, as the lua reference does.
|
|
28
|
+
(when (vs/ismap m)
|
|
29
|
+
(let [out (vs/jm)]
|
|
30
|
+
(doseq [k (vs/keysof m)] (vs/setprop out (str/lower-case k) (vs/getprop m k)))
|
|
31
|
+
out)))
|
|
32
|
+
|
|
33
|
+
(defn- neutral
|
|
34
|
+
"A struct map with STRING keys, from this port's keyword-keyed atom.
|
|
35
|
+
|
|
36
|
+
spec/result/response are atoms wrapping {:base ... :status-text ...}; the
|
|
37
|
+
corpus is camelCase and string-keyed, so a raw deref matches nothing.
|
|
38
|
+
status-text is spelled statusText here for the same reason."
|
|
39
|
+
[v]
|
|
40
|
+
(let [m (if (instance? clojure.lang.Atom v) (deref v) v)]
|
|
41
|
+
(if-not (map? m)
|
|
42
|
+
m
|
|
43
|
+
(let [out (vs/jm)]
|
|
44
|
+
(doseq [[k val] m]
|
|
45
|
+
(let [ks (if (keyword? k) (name k) (str k))
|
|
46
|
+
ks (case ks "status-text" "statusText" ks)
|
|
47
|
+
v0 (if (instance? clojure.lang.Atom val) (deref val) val)
|
|
48
|
+
;; An SDK error object is keyword-keyed with :msg; the contract
|
|
49
|
+
;; spells it err.message, so a raw copy reads null.
|
|
50
|
+
v1 (if (and (map? v0) (core/sdk-error? v0))
|
|
51
|
+
(vs/jm "message" (str (:msg v0)))
|
|
52
|
+
v0)]
|
|
53
|
+
(vs/setprop out ks v1)))
|
|
54
|
+
out))))
|
|
55
|
+
|
|
56
|
+
(defn- publish-ctx
|
|
57
|
+
"Write the mutated spec/result/response back onto the corpus map.
|
|
58
|
+
|
|
59
|
+
The match reads entry.ctx — the raw corpus map — while the utilities mutate
|
|
60
|
+
the live objects hanging off the context, so without this every ctx.*
|
|
61
|
+
assertion reads null."
|
|
62
|
+
[ctxmap ctx]
|
|
63
|
+
(doseq [[k kw] [["spec" :spec] ["result" :result] ["response" :response]
|
|
64
|
+
["err" :err]]]
|
|
65
|
+
(when-let [v (core/oget ctx kw)]
|
|
66
|
+
(vs/setprop ctxmap k (neutral v))))
|
|
67
|
+
ctxmap)
|
|
68
|
+
|
|
69
|
+
(defn- corpus-ctx
|
|
70
|
+
"A LIVE context from a corpus map: the utilities read and MUTATE spec, result
|
|
71
|
+
and response, so a bare map leaves them nothing to work on.
|
|
72
|
+
|
|
73
|
+
The context is an ATOM here, not a struct map — core/oget and core/oset!
|
|
74
|
+
with KEYWORD keys are its accessors, and vs/setprop on it silently does
|
|
75
|
+
nothing."
|
|
76
|
+
[sdk utility ctxmap]
|
|
77
|
+
(let [ctxmap (if (vs/ismap ctxmap) ctxmap (vs/jm))
|
|
78
|
+
opname (vs/getprop ctxmap "opname")
|
|
79
|
+
ctx (core/make-context
|
|
80
|
+
(vs/jm "opname" (or opname "") "client" sdk "utility" utility) nil)]
|
|
81
|
+
(when (nil? (core/oget ctx :options))
|
|
82
|
+
(core/oset! ctx :options (core/client-options-map sdk)))
|
|
83
|
+
(when-let [sp (vs/getprop ctxmap "spec")]
|
|
84
|
+
(when (vs/ismap sp) (core/oset! ctx :spec (core/make-spec sp))))
|
|
85
|
+
(when-let [rs (vs/getprop ctxmap "result")]
|
|
86
|
+
(when (vs/ismap rs) (core/oset! ctx :result (core/make-result rs))))
|
|
87
|
+
(when-let [rp (vs/getprop ctxmap "response")]
|
|
88
|
+
(when (vs/ismap rp)
|
|
89
|
+
(let [resp (core/make-response rp)]
|
|
90
|
+
(when-let [lh (lower-headers (vs/getprop rp "headers"))]
|
|
91
|
+
(core/oset! resp :headers lh))
|
|
92
|
+
;; result-body reads the response's json thunk, not its body; the
|
|
93
|
+
;; corpus supplies a plain body, so wrap it as the other drivers do.
|
|
94
|
+
(when-let [b (vs/getprop rp "body")]
|
|
95
|
+
(core/oset! resp :json (fn [] b)))
|
|
96
|
+
(core/oset! ctx :response resp))))
|
|
97
|
+
(doseq [[k kw] [["point" :point] ["reqdata" :reqdata]
|
|
98
|
+
["reqmatch" :reqmatch] ["data" :data]]]
|
|
99
|
+
(when-let [v (vs/getprop ctxmap k)] (core/oset! ctx kw v)))
|
|
100
|
+
ctx))
|
|
9
101
|
|
|
10
102
|
(defn run [rec]
|
|
11
103
|
(let [sdk (client/test-sdk nil nil)
|
|
@@ -50,3 +142,81 @@
|
|
|
50
142
|
(t/is-eq (vs/getprop fetchdef "method") "GET" "GET")
|
|
51
143
|
(t/is-true (str/includes? (str (vs/getprop fetchdef "url")) "/api/items/item01") "url includes")))))
|
|
52
144
|
nil))
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
(defn run-corpus
|
|
148
|
+
"Drive every primary section of the shared corpus through this SDK."
|
|
149
|
+
[rec]
|
|
150
|
+
(let [sdk (client/test-sdk nil nil)
|
|
151
|
+
utility (core/get-utility sdk)
|
|
152
|
+
u (fn [k] (get (deref utility) k))
|
|
153
|
+
alltests (core/json-parse (slurp CORPUS))
|
|
154
|
+
primary (.get ^Map alltests "primary")
|
|
155
|
+
setup-of (fn [nm] (getspec primary nm "DEF" "setup" "a"))
|
|
156
|
+
client-for (fn [nm] (let [su (setup-of nm)]
|
|
157
|
+
(if (vs/ismap su) (client/test-sdk nil su) sdk)))
|
|
158
|
+
;; make-spec and prepare-auth read defaults off the CLIENT, as the ts
|
|
159
|
+
;; reference does via client.options(), so a section's DEF.setup cannot
|
|
160
|
+
;; reach them through ctx options.
|
|
161
|
+
spec-sdk (client-for "makeSpec")
|
|
162
|
+
auth-sdk (client-for "prepareAuth")
|
|
163
|
+
ran (atom 0)
|
|
164
|
+
;; One section, driven with a ctx built from the corpus entry.
|
|
165
|
+
runset (fn [nm cl f]
|
|
166
|
+
(let [node (getspec primary nm "basic")]
|
|
167
|
+
(when (nil? node)
|
|
168
|
+
(throw (ex-info (str "corpus section missing: " nm) {})))
|
|
169
|
+
(swap! ran inc)
|
|
170
|
+
;; run-set APPLIES the resolved args, so an args-style entry
|
|
171
|
+
;; calls the subject with more than one — variadic, and the
|
|
172
|
+
;; extra arg reaches the section through `more`.
|
|
173
|
+
(corpus/run-set nm node
|
|
174
|
+
(fn [& as]
|
|
175
|
+
(let [ctxmap (first as)
|
|
176
|
+
more (second as)
|
|
177
|
+
ctx (corpus-ctx cl (core/get-utility cl) ctxmap)
|
|
178
|
+
out (f ctx ctxmap more)]
|
|
179
|
+
(publish-ctx ctxmap ctx)
|
|
180
|
+
out)))))
|
|
181
|
+
argof (fn [ctxmap k] (vs/getprop ctxmap k))]
|
|
182
|
+
|
|
183
|
+
(t/run-check rec "primary-corpus"
|
|
184
|
+
(fn []
|
|
185
|
+
(binding [corpus/*results* (atom {:pass [] :fail []})]
|
|
186
|
+
(runset "done" sdk (fn [c _ _2] ((u :done) c)))
|
|
187
|
+
(runset "makeUrl" sdk (fn [c _ _2] (first ((u :make-url) c))))
|
|
188
|
+
(runset "makeRequest" sdk (fn [c _ _2] (first ((u :make-request) c))))
|
|
189
|
+
(runset "makeResponse" sdk (fn [c _ _2] (first ((u :make-response) c))))
|
|
190
|
+
(runset "makeSpec" spec-sdk (fn [c _ _2] (first ((u :make-spec) c))))
|
|
191
|
+
(runset "prepareAuth" auth-sdk (fn [c _ _2] (first ((u :prepare-auth) c))))
|
|
192
|
+
(runset "prepareBody" sdk (fn [c _ _2] ((u :prepare-body) c)))
|
|
193
|
+
(runset "prepareHeaders" sdk (fn [c _ _2] ((u :prepare-headers) c)))
|
|
194
|
+
(runset "prepareMethod" sdk (fn [c _ _2] ((u :prepare-method) c)))
|
|
195
|
+
(runset "prepareParams" sdk (fn [c _ _2] ((u :prepare-params) c)))
|
|
196
|
+
(runset "preparePath" sdk (fn [c _ _2] ((u :prepare-path) c)))
|
|
197
|
+
(runset "prepareQuery" sdk (fn [c _ _2] ((u :prepare-query) c)))
|
|
198
|
+
(runset "resultBasic" sdk (fn [c _ _2] (neutral ((u :result-basic) c))))
|
|
199
|
+
(runset "resultBody" sdk (fn [c _ _2] (neutral ((u :result-body) c))))
|
|
200
|
+
(runset "resultHeaders" sdk (fn [c _ _2] (neutral ((u :result-headers) c))))
|
|
201
|
+
(runset "transformRequest" sdk (fn [c _ _2] ((u :transform-request) c)))
|
|
202
|
+
(runset "transformResponse" sdk (fn [c _ _2] ((u :transform-response) c)))
|
|
203
|
+
(runset "makeOptions" sdk (fn [c m _2]
|
|
204
|
+
(core/oset! c :config (argof m "config"))
|
|
205
|
+
(core/oset! c :options (argof m "options"))
|
|
206
|
+
((u :make-options) c)))
|
|
207
|
+
(runset "makeContext" sdk (fn [c _ _2]
|
|
208
|
+
(let [op (core/oget c :op)]
|
|
209
|
+
(vs/jm "op" (neutral op)))))
|
|
210
|
+
(runset "makeError" sdk (fn [c _ err] (first ((u :make-error) c err))))
|
|
211
|
+
(runset "operator" sdk (fn [_ m _2] (neutral (core/make-operation m))))
|
|
212
|
+
(runset "param" sdk (fn [c _ pd] ((u :param) c pd)))
|
|
213
|
+
|
|
214
|
+
(let [r @corpus/*results*
|
|
215
|
+
np (count (:pass r))
|
|
216
|
+
nf (count (:fail r))]
|
|
217
|
+
(doseq [f (:fail r)]
|
|
218
|
+
(println "PRIMARY-FAIL" (:group f) (:name f) "-" (:msg f)))
|
|
219
|
+
(println "PRIMARY CORPUS: PASS" np " FAIL" nf)
|
|
220
|
+
(t/is-true (< 0 @ran) "the primary corpus executed no sections")
|
|
221
|
+
(t/is-true (< 0 np) "the primary corpus executed no cases")
|
|
222
|
+
(t/is-true (= 0 nf) (str nf " primary corpus failures"))))))))
|
|
@@ -68,7 +68,14 @@
|
|
|
68
68
|
(defn- resolve-args [^Map entry subject]
|
|
69
69
|
(cond
|
|
70
70
|
(.containsKey entry "ctx") [(.get entry "ctx")]
|
|
71
|
-
(.containsKey entry "args")
|
|
71
|
+
(.containsKey entry "args")
|
|
72
|
+
(let [args (vec (.get entry "args"))]
|
|
73
|
+
;; ts's resolveArgs writes the live first arg back as entry.ctx, so a
|
|
74
|
+
;; `match: {ctx: ...}` resolves for args-style entries too. Without it
|
|
75
|
+
;; every such assertion reads null and silently passes nothing.
|
|
76
|
+
(when (and (seq args) (s/ismap (first args)))
|
|
77
|
+
(.put entry "ctx" (first args)))
|
|
78
|
+
args)
|
|
72
79
|
(.containsKey entry "in") [(s/clone (.get entry "in"))]
|
|
73
80
|
:else []))
|
|
74
81
|
|
|
@@ -98,7 +105,11 @@
|
|
|
98
105
|
(when (.containsKey entry "match")
|
|
99
106
|
(do-match (.get entry "match")
|
|
100
107
|
(omap "in" (.get entry "in") "out" (.get entry "res")
|
|
101
|
-
|
|
108
|
+
;; The ts runner hands do-match the ERROR OBJECT, so a
|
|
109
|
+
;; corpus `match: {err: {message: ...}}` resolves. This
|
|
110
|
+
;; port passed the bare message string, where err.message
|
|
111
|
+
;; walks into a string and reads null.
|
|
112
|
+
"ctx" (.get entry "ctx") "err" (omap "message" msg))))
|
|
102
113
|
(throw (AssertionError. (str "ERROR MATCH: [" (s/stringify entry-err) "] <=> [" msg "]"))))
|
|
103
114
|
(throw (if (instance? AssertionError err) err (AssertionError. (str err)))))))
|
|
104
115
|
|
|
@@ -18,6 +18,8 @@
|
|
|
18
18
|
(let [results (atom [])
|
|
19
19
|
rec (fn [name ok? msg] (swap! results conj {:name name :ok ok? :msg msg}))]
|
|
20
20
|
(primary/run rec)
|
|
21
|
+
;; The shared corpus, driven through this SDK's utilities.
|
|
22
|
+
(primary/run-corpus rec)
|
|
21
23
|
(pipeline/run rec)
|
|
22
24
|
(feature/run rec)
|
|
23
25
|
(netsim/run rec)
|
|
@@ -48,4 +48,108 @@ public class CustomUtilityTest
|
|
|
48
48
|
Assert.Equal(name.ToUpperInvariant(), result["util"]);
|
|
49
49
|
}
|
|
50
50
|
}
|
|
51
|
+
|
|
52
|
+
// The half the test above cannot see. Those keys are ALIASES - `auth`,
|
|
53
|
+
// `body`, `spec` - and no utility member has those names, so landing in
|
|
54
|
+
// Custom is the right outcome for them and the assertion passes whether or
|
|
55
|
+
// not overriding works at all.
|
|
56
|
+
//
|
|
57
|
+
// A key that DOES name a member must replace it. That is the documented
|
|
58
|
+
// contract, it is what ts does, and it was silently absent here: every
|
|
59
|
+
// entry went to Custom, which nothing reads, so `utility: { fetcher = ... }`
|
|
60
|
+
// did nothing while ts honoured it.
|
|
61
|
+
[Fact]
|
|
62
|
+
public void ARealUtilityMemberIsReplacedNotShelved()
|
|
63
|
+
{
|
|
64
|
+
var reached = 0;
|
|
65
|
+
|
|
66
|
+
// A NATURAL lambda, as a caller writes it - NOT declared as
|
|
67
|
+
// FetcherFunc. C# delegate types are nominal, so this is a `Func`4`
|
|
68
|
+
// and not an instance of FetcherFunc at all; declaring it as the named
|
|
69
|
+
// type here would test only the one form that already worked and hide
|
|
70
|
+
// every ordinary caller. It captures `reached`, so the conversion has
|
|
71
|
+
// to carry the closure target too.
|
|
72
|
+
var scripted = (Context ctx, string fullurl, Dictionary<string, object?> fetchdef) =>
|
|
73
|
+
{
|
|
74
|
+
reached++;
|
|
75
|
+
return (object?)new Dictionary<string, object?>
|
|
76
|
+
{
|
|
77
|
+
["status"] = 200,
|
|
78
|
+
["statusText"] = "OK",
|
|
79
|
+
["headers"] = new Dictionary<string, object?>(),
|
|
80
|
+
["body"] = "{}",
|
|
81
|
+
};
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
// The plain constructor, not TestSDK. The `test` feature is
|
|
85
|
+
// transport: 'base' - it REPLACES the transport by design - so a client
|
|
86
|
+
// in test mode would shadow the scripted fetcher and this would assert
|
|
87
|
+
// nothing.
|
|
88
|
+
var client = new ProjectNameSDK(new Dictionary<string, object?>
|
|
89
|
+
{
|
|
90
|
+
["utility"] = new Dictionary<string, object?> { ["fetcher"] = scripted },
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
var u = client.GetUtility();
|
|
94
|
+
|
|
95
|
+
Assert.NotNull(u.Fetcher);
|
|
96
|
+
Assert.False(u.Custom.ContainsKey("fetcher"),
|
|
97
|
+
"fetcher was shelved in Custom instead of replacing the member");
|
|
98
|
+
|
|
99
|
+
// Behaviour, not identity: drive it.
|
|
100
|
+
var ctx = u.MakeContext(new Dictionary<string, object?>(), client.GetRootCtx());
|
|
101
|
+
u.Fetcher(ctx, "http://example.test/probe", new Dictionary<string, object?>());
|
|
102
|
+
Assert.Equal(1, reached);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// The other spelling of the same value. A caller who names the exported
|
|
106
|
+
// delegate gets a FetcherFunc, which IS an instance of the field type and
|
|
107
|
+
// needs no conversion - the mirror image of the test above, and broken by
|
|
108
|
+
// any fix that swaps one path for the other.
|
|
109
|
+
[Fact]
|
|
110
|
+
public void ANamedDelegateIsAcceptedToo()
|
|
111
|
+
{
|
|
112
|
+
var reached = 0;
|
|
113
|
+
FetcherFunc scripted = (ctx, fullurl, fetchdef) =>
|
|
114
|
+
{
|
|
115
|
+
reached++;
|
|
116
|
+
return new Dictionary<string, object?>
|
|
117
|
+
{
|
|
118
|
+
["status"] = 200,
|
|
119
|
+
["statusText"] = "OK",
|
|
120
|
+
["headers"] = new Dictionary<string, object?>(),
|
|
121
|
+
["body"] = "{}",
|
|
122
|
+
};
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
var client = new ProjectNameSDK(new Dictionary<string, object?>
|
|
126
|
+
{
|
|
127
|
+
["utility"] = new Dictionary<string, object?> { ["fetcher"] = scripted },
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
var u = client.GetUtility();
|
|
131
|
+
Assert.False(u.Custom.ContainsKey("fetcher"),
|
|
132
|
+
"a named-delegate fetcher was shelved in Custom");
|
|
133
|
+
|
|
134
|
+
var ctx = u.MakeContext(new Dictionary<string, object?>(), client.GetRootCtx());
|
|
135
|
+
u.Fetcher(ctx, "http://example.test/probe", new Dictionary<string, object?>());
|
|
136
|
+
Assert.Equal(1, reached);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// An unknown key must still be attached rather than dropped, so the two
|
|
140
|
+
// halves cannot be satisfied by a rule that also swallows extras.
|
|
141
|
+
[Fact]
|
|
142
|
+
public void AnUnknownKeyIsStillAttached()
|
|
143
|
+
{
|
|
144
|
+
var client = new ProjectNameSDK(new Dictionary<string, object?>
|
|
145
|
+
{
|
|
146
|
+
["utility"] = new Dictionary<string, object?>
|
|
147
|
+
{
|
|
148
|
+
["notAUtilityMember"] = Util("EXTRA"),
|
|
149
|
+
},
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
Assert.True(client.GetUtility().Custom.ContainsKey("notAUtilityMember"),
|
|
153
|
+
"an unknown utility key was dropped instead of kept in Custom");
|
|
154
|
+
}
|
|
51
155
|
}
|
|
@@ -15,12 +15,89 @@ public static partial class SdkUtility
|
|
|
15
15
|
new(@"\{([A-Za-z0-9_]+)\}", RegexOptions.Compiled);
|
|
16
16
|
|
|
17
17
|
|
|
18
|
+
/// <summary>
|
|
19
|
+
/// Replaces one utility member from <c>options.utility</c>, matching the
|
|
20
|
+
/// ts reference: a key naming a real member REPLACES it, and any other
|
|
21
|
+
/// key is attached as a custom extra. Returns false when the key names no
|
|
22
|
+
/// member or the value is not that member's delegate type, so the caller
|
|
23
|
+
/// keeps it in Custom.
|
|
24
|
+
/// </summary>
|
|
25
|
+
/// <remarks>
|
|
26
|
+
/// REFLECTION, NOT A KEYED SWITCH. The go and java ports list every member
|
|
27
|
+
/// by hand and carry a "keep this in step with registerAll" warning,
|
|
28
|
+
/// because a utility added to one list and not the other is overridable
|
|
29
|
+
/// there and not here. C# can read the field set off the type itself, so
|
|
30
|
+
/// the list cannot drift, and FieldType.IsInstanceOfType is a real check
|
|
31
|
+
/// rather than an unchecked cast - a wrongly-shaped value falls through to
|
|
32
|
+
/// Custom instead of throwing later in the pipeline.
|
|
33
|
+
///
|
|
34
|
+
/// Only a PUBLIC name may replace a member. Option keys are camelCase, as
|
|
35
|
+
/// ts spells them, and fields here are PascalCase; public names carry no
|
|
36
|
+
/// underscore, so an underscore means the caller named something of their
|
|
37
|
+
/// own rather than a member.
|
|
38
|
+
/// </remarks>
|
|
39
|
+
internal static bool OverrideUtil(Utility utility, string key, object? val)
|
|
40
|
+
{
|
|
41
|
+
if (string.IsNullOrEmpty(key) || key.Contains('_'))
|
|
42
|
+
{
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
var name = char.ToUpperInvariant(key[0]) + key.Substring(1);
|
|
47
|
+
if ("Custom" == name)
|
|
48
|
+
{
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
var field = typeof(Utility).GetField(name);
|
|
53
|
+
if (null == field || null == val)
|
|
54
|
+
{
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (field.FieldType.IsInstanceOfType(val))
|
|
59
|
+
{
|
|
60
|
+
field.SetValue(utility, val);
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// SAME SHAPE, DIFFERENT NAMED TYPE. Every member is a NAMED delegate
|
|
65
|
+
// (FetcherFunc) or a constructed Func/Action, and C# delegate types are
|
|
66
|
+
// nominal: a naturally-typed lambda gets Func`4, and
|
|
67
|
+
// `Func<Context, string, Dictionary<string, object?>, object?>` written
|
|
68
|
+
// out longhand is a third type again. None is an instance of
|
|
69
|
+
// FetcherFunc despite an identical signature, so IsInstanceOfType alone
|
|
70
|
+
// honoured only a caller who knew to declare the named type - which is
|
|
71
|
+
// to say, almost nobody.
|
|
72
|
+
//
|
|
73
|
+
// CreateDelegate rebinds the same target and method onto the field's
|
|
74
|
+
// type, and with throwOnBindFailure: false it returns null rather than
|
|
75
|
+
// throwing when the signature genuinely does not match - so a wrongly
|
|
76
|
+
// shaped delegate still falls through to `custom`.
|
|
77
|
+
if (val is Delegate d && typeof(Delegate).IsAssignableFrom(field.FieldType))
|
|
78
|
+
{
|
|
79
|
+
var converted = Delegate.CreateDelegate(field.FieldType, d.Target, d.Method, false);
|
|
80
|
+
if (null != converted)
|
|
81
|
+
{
|
|
82
|
+
field.SetValue(utility, converted);
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
|
|
18
90
|
internal static Dictionary<string, object?> MakeOptionsUtil(Context ctx)
|
|
19
91
|
{
|
|
20
92
|
var options = ctx.Options ?? new Dictionary<string, object?>();
|
|
21
93
|
|
|
22
|
-
// Merge
|
|
94
|
+
// Merge utility overrides from options onto the utility object.
|
|
23
95
|
// Read from original options before clone for safety.
|
|
96
|
+
//
|
|
97
|
+
// A key naming a real utility member REPLACES it; anything else is
|
|
98
|
+
// attached as a custom extra. Shelving everything in Custom - a map
|
|
99
|
+
// nothing reads - made `utility: { fetcher = ... }`, the documented
|
|
100
|
+
// transport seam, a silent no-op here while ts honoured it.
|
|
24
101
|
if (Helpers.ToMapAny(options.TryGetValue("utility", out var cu) ? cu : null)
|
|
25
102
|
is Dictionary<string, object?> customUtils)
|
|
26
103
|
{
|
|
@@ -29,7 +106,10 @@ public static partial class SdkUtility
|
|
|
29
106
|
{
|
|
30
107
|
foreach (var kv in customUtils)
|
|
31
108
|
{
|
|
32
|
-
utility
|
|
109
|
+
if (!OverrideUtil(utility, kv.Key, kv.Value))
|
|
110
|
+
{
|
|
111
|
+
utility.Custom[kv.Key] = kv.Value;
|
|
112
|
+
}
|
|
33
113
|
}
|
|
34
114
|
}
|
|
35
115
|
}
|
|
@@ -193,17 +193,53 @@ defmodule ProjectName.Utility do
|
|
|
193
193
|
|
|
194
194
|
# ---- make_options --------------------------------------------------------
|
|
195
195
|
|
|
196
|
+
# Public camelCase option key -> snake_case utility member name, or nil
|
|
197
|
+
# when the key is not a public name (see make_options_impl).
|
|
198
|
+
defp util_member(key) when is_binary(key) do
|
|
199
|
+
if String.contains?(key, "_") do
|
|
200
|
+
nil
|
|
201
|
+
else
|
|
202
|
+
String.replace(key, ~r/([A-Z])/, "_\\1") |> String.downcase()
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
defp util_member(_key), do: nil
|
|
207
|
+
|
|
196
208
|
def make_options_impl(ctx) do
|
|
197
209
|
options = H.or_(S.getprop(ctx, "options"), S.jm([]))
|
|
198
210
|
|
|
199
211
|
custom_utils = S.getprop(options, "utility")
|
|
200
212
|
|
|
213
|
+
# Utility overrides from options.
|
|
214
|
+
#
|
|
215
|
+
# A key naming a real utility member REPLACES it; anything else is
|
|
216
|
+
# attached as a custom extra. Shelving everything in `custom` - a map
|
|
217
|
+
# nothing reads - made `utility: %{"fetcher" => ...}`, the documented
|
|
218
|
+
# transport seam, a silent no-op here while ts honoured it.
|
|
219
|
+
#
|
|
220
|
+
# Only a PUBLIC name may replace. Option keys are camelCase, as ts spells
|
|
221
|
+
# them, and members here are snake_case; public names carry no underscore,
|
|
222
|
+
# so an underscore means the caller named something of their own -
|
|
223
|
+
# possibly the internal spelling of a real member. `make_error` must stay
|
|
224
|
+
# an extension, or a non-callable would break the error path on the next
|
|
225
|
+
# request.
|
|
201
226
|
if S.ismap(custom_utils) do
|
|
202
227
|
utility = S.getprop(ctx, "utility")
|
|
203
228
|
|
|
204
229
|
if utility != nil do
|
|
205
230
|
custom = S.getprop(utility, "custom")
|
|
206
|
-
|
|
231
|
+
|
|
232
|
+
Enum.each(S.keysof(custom_utils), fn k ->
|
|
233
|
+
val = S.getprop(custom_utils, k)
|
|
234
|
+
member = util_member(k)
|
|
235
|
+
|
|
236
|
+
if member != nil and member != "custom" and
|
|
237
|
+
S.getprop(utility, member) != nil do
|
|
238
|
+
S.setprop(utility, member, val)
|
|
239
|
+
else
|
|
240
|
+
S.setprop(custom, k, val)
|
|
241
|
+
end
|
|
242
|
+
end)
|
|
207
243
|
end
|
|
208
244
|
end
|
|
209
245
|
|
|
@@ -863,6 +899,16 @@ defmodule ProjectName.Utility do
|
|
|
863
899
|
cond do
|
|
864
900
|
match?(%ProjectName.Error{}, err) -> err.msg
|
|
865
901
|
is_exception(err) -> Exception.message(err)
|
|
902
|
+
# S.ismap, not is_map: a struct value is a {:vmap, id} TUPLE, so
|
|
903
|
+
# is_map/1 is false for every heap map and this branch could never
|
|
904
|
+
# fire for one. And the neutral contract spells it "message" — "msg"
|
|
905
|
+
# is this port's own internal name. Between them, an error map reached
|
|
906
|
+
# the stringify fallback and the SDK reported
|
|
907
|
+
# "foo: {message:zed}" where every other target reports "foo: zed".
|
|
908
|
+
S.ismap(err) ->
|
|
909
|
+
m = S.getprop(err, "message") || S.getprop(err, "msg")
|
|
910
|
+
if is_binary(m) and m != "", do: m, else: "unknown error"
|
|
911
|
+
|
|
866
912
|
is_map(err) and not is_struct(err) and S.getprop(err, "msg") != nil -> S.getprop(err, "msg")
|
|
867
913
|
is_binary(err) -> err
|
|
868
914
|
true -> S.stringify(err)
|
|
@@ -1066,7 +1112,9 @@ defmodule ProjectName.Utility do
|
|
|
1066
1112
|
# Only fall back to the op-name convention when the point has no method.
|
|
1067
1113
|
case S.getprop(S.getprop(ctx, "point"), "method") do
|
|
1068
1114
|
m when is_binary(m) and m != "" -> String.upcase(m)
|
|
1069
|
-
|
|
1115
|
+
# No GET catch-all: an unrecognised op must fall through to the
|
|
1116
|
+
# allow.method gate, not be silently issued as a GET.
|
|
1117
|
+
_ -> Map.get(@method_map, opname)
|
|
1070
1118
|
end
|
|
1071
1119
|
end
|
|
1072
1120
|
|