@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.
Files changed (46) hide show
  1. package/bin/voxgig-sdkgen +1 -1
  2. package/dist/tsconfig.tsbuildinfo +1 -1
  3. package/package.json +1 -1
  4. package/project/.sdk/src/cmp/ocaml/Config_ocaml.ts +13 -1
  5. package/project/.sdk/src/cmp/zig/Config_zig.ts +1 -1
  6. package/project/.sdk/tm/c/tests/primary_corpus_test.c +565 -0
  7. package/project/.sdk/tm/c/utility/prepare_method.c +3 -1
  8. package/project/.sdk/tm/c/utility/transform_request.c +4 -3
  9. package/project/.sdk/tm/clojure/src/sdk/core.clj +55 -5
  10. package/project/.sdk/tm/clojure/test/sdk/test/primary.clj +171 -1
  11. package/project/.sdk/tm/clojure/test/sdk/test/struct_corpus.clj +13 -2
  12. package/project/.sdk/tm/clojure/test/sdk/test_runner.clj +2 -0
  13. package/project/.sdk/tm/csharp/test/CustomUtilityTest.cs +104 -0
  14. package/project/.sdk/tm/csharp/utility/MakeOptions.cs +82 -2
  15. package/project/.sdk/tm/elixir/lib/projectname/utility.ex +50 -2
  16. package/project/.sdk/tm/elixir/test/primary_utility_test.exs +18 -200
  17. package/project/.sdk/tm/elixir/test/support/struct_corpus.ex +413 -2
  18. package/project/.sdk/tm/go/test/feature_corpus_test.go +18 -0
  19. package/project/.sdk/tm/java/test/FeatureCorpusTest.java +8 -0
  20. package/project/.sdk/tm/js/test/feature/Corpus.test.js +6 -1
  21. package/project/.sdk/tm/kotlin/utility/MakeOptions.kt +56 -2
  22. package/project/.sdk/tm/lua/utility/make_options.lua +22 -2
  23. package/project/.sdk/tm/ocaml/Makefile +15 -4
  24. package/project/.sdk/tm/ocaml/sdk_features.ml +224 -0
  25. package/project/.sdk/tm/ocaml/sdk_runtime.ml +8 -1
  26. package/project/.sdk/tm/ocaml/test/corpus_runner.ml +185 -0
  27. package/project/.sdk/tm/ocaml/test/primary_utility_test.ml +238 -0
  28. package/project/.sdk/tm/ocaml/test/struct_corpus.ml +5 -160
  29. package/project/.sdk/tm/perl/t/feature_corpus.t +9 -1
  30. package/project/.sdk/tm/php/test/FeatureCorpusTest.php +11 -0
  31. package/project/.sdk/tm/py/test/feature_harness.py +6 -0
  32. package/project/.sdk/tm/py/test/test_feature_corpus.py +6 -0
  33. package/project/.sdk/tm/rb/test/feature_corpus_test.rb +6 -1
  34. package/project/.sdk/tm/rust/feature/cost.rs +25 -1
  35. package/project/.sdk/tm/scala/Makefile +1 -0
  36. package/project/.sdk/tm/scala/sdktest/PrimaryCorpus.scala +294 -0
  37. package/project/.sdk/tm/scala/sdktest/StructCorpus.scala +0 -0
  38. package/project/.sdk/tm/scala/utility/Make.scala +49 -2
  39. package/project/.sdk/tm/scala/utility/Prepare.scala +3 -1
  40. package/project/.sdk/tm/swift/Sources/ProjectNameSDK/utility/MakeOptions.swift +21 -3
  41. package/project/.sdk/tm/ts/src/feature/test/TestFeature.ts +13 -2
  42. package/project/.sdk/tm/ts/test/feature/Corpus.test.ts +12 -1
  43. package/project/.sdk/tm/zig/core/utility.zig +8 -1
  44. package/project/.sdk/tm/zig/test/primary_utility_test.zig +445 -0
  45. package/project/.sdk/tm/zig/test/struct_runner.zig +238 -0
  46. 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") (vec (.get 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
- "ctx" (.get entry "ctx") "err" msg)))
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 custom utility overrides onto the utility object.
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.Custom[kv.Key] = kv.Value;
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
- Enum.each(S.keysof(custom_utils), fn k -> S.setprop(custom, k, S.getprop(custom_utils, k)) end)
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
- _ -> Map.get(@method_map, opname, "GET")
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