@voxgig/sdkgen 4.2.8 → 4.4.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.
Files changed (63) hide show
  1. package/bin/voxgig-sdkgen +1 -1
  2. package/dist/helpers/naming.d.ts +2 -1
  3. package/dist/helpers/naming.js +50 -12
  4. package/dist/helpers/naming.js.map +1 -1
  5. package/dist/sdkgen.d.ts +2 -2
  6. package/dist/sdkgen.js +4 -3
  7. package/dist/sdkgen.js.map +1 -1
  8. package/dist/tsconfig.tsbuildinfo +1 -1
  9. package/package.json +1 -1
  10. package/project/.sdk/src/cmp/zig/Config_zig.ts +1 -1
  11. package/project/.sdk/tm/c/tests/primary_corpus_test.c +565 -0
  12. package/project/.sdk/tm/c/utility/prepare_method.c +3 -1
  13. package/project/.sdk/tm/c/utility/transform_request.c +4 -3
  14. package/project/.sdk/tm/clojure/src/sdk/core.clj +55 -5
  15. package/project/.sdk/tm/clojure/test/sdk/test/primary.clj +171 -1
  16. package/project/.sdk/tm/clojure/test/sdk/test/struct_corpus.clj +13 -2
  17. package/project/.sdk/tm/clojure/test/sdk/test_runner.clj +2 -0
  18. package/project/.sdk/tm/csharp/test/CustomUtilityTest.cs +104 -0
  19. package/project/.sdk/tm/csharp/utility/MakeOptions.cs +82 -2
  20. package/project/.sdk/tm/elixir/lib/projectname/utility.ex +50 -2
  21. package/project/.sdk/tm/elixir/test/primary_utility_test.exs +18 -200
  22. package/project/.sdk/tm/elixir/test/support/struct_corpus.ex +413 -2
  23. package/project/.sdk/tm/go/test/custom_utility_test.go +103 -0
  24. package/project/.sdk/tm/go/test/feature_corpus_test.go +550 -0
  25. package/project/.sdk/tm/go/utility/make_options.go +7 -1
  26. package/project/.sdk/tm/go/utility/register.go +194 -0
  27. package/project/.sdk/tm/java/test/CustomUtilityTest.java +55 -0
  28. package/project/.sdk/tm/java/test/FeatureCorpusTest.java +463 -0
  29. package/project/.sdk/tm/java/utility/MakeOptions.java +11 -2
  30. package/project/.sdk/tm/java/utility/Register.java +91 -0
  31. package/project/.sdk/tm/js/test/feature/Corpus.test.js +285 -0
  32. package/project/.sdk/tm/kotlin/utility/MakeOptions.kt +56 -2
  33. package/project/.sdk/tm/lua/utility/make_options.lua +22 -2
  34. package/project/.sdk/tm/ocaml/Makefile +15 -4
  35. package/project/.sdk/tm/ocaml/sdk_runtime.ml +8 -1
  36. package/project/.sdk/tm/ocaml/test/corpus_runner.ml +185 -0
  37. package/project/.sdk/tm/ocaml/test/primary_utility_test.ml +238 -0
  38. package/project/.sdk/tm/ocaml/test/struct_corpus.ml +5 -160
  39. package/project/.sdk/tm/perl/t/feature_corpus.t +345 -0
  40. package/project/.sdk/tm/perl/utility/make_options.pm +33 -1
  41. package/project/.sdk/tm/php/core/Context.php +3 -0
  42. package/project/.sdk/tm/php/core/Control.php +13 -0
  43. package/project/.sdk/tm/php/core/Error.php +12 -0
  44. package/project/.sdk/tm/php/test/FeatureCorpusTest.php +376 -0
  45. package/project/.sdk/tm/php/utility/MakeOptions.php +30 -1
  46. package/project/.sdk/tm/py/pkg/utility/make_options.py +42 -1
  47. package/project/.sdk/tm/py/test/test_feature_corpus.py +309 -0
  48. package/project/.sdk/tm/rb/test/feature_corpus_test.rb +281 -0
  49. package/project/.sdk/tm/rb/utility/make_options.rb +32 -1
  50. package/project/.sdk/tm/scala/Makefile +1 -0
  51. package/project/.sdk/tm/scala/sdktest/PrimaryCorpus.scala +294 -0
  52. package/project/.sdk/tm/scala/sdktest/StructCorpus.scala +0 -0
  53. package/project/.sdk/tm/scala/utility/Make.scala +49 -2
  54. package/project/.sdk/tm/scala/utility/Prepare.scala +3 -1
  55. package/project/.sdk/tm/swift/Sources/ProjectNameSDK/utility/MakeOptions.swift +21 -3
  56. package/project/.sdk/tm/ts/src/feature/test/TestFeature.ts +13 -2
  57. package/project/.sdk/tm/ts/test/feature/Corpus.test.ts +287 -0
  58. package/project/.sdk/tm/zig/core/utility.zig +8 -1
  59. package/project/.sdk/tm/zig/test/primary_utility_test.zig +445 -0
  60. package/project/.sdk/tm/zig/test/struct_runner.zig +238 -0
  61. package/project/sdkgen-package.json +1 -1
  62. package/src/helpers/naming.ts +54 -12
  63. package/src/sdkgen.ts +2 -1
@@ -0,0 +1,294 @@
1
+ // Primary-utility corpus driver.
2
+ //
3
+ // Drives every `primary` section of the shared corpus (../.sdk/test/test.json)
4
+ // through this SDK's utilities, the way the ts reference harness does. The
5
+ // match/args/err machinery is reused from Runner (StructCorpus.scala); what
6
+ // this file adds is the bridge between the corpus Value ADT and the SDK's
7
+ // Java-collection world, plus a live Context built from each corpus entry.
8
+
9
+ import voxgig.struct.{clone as sclone, *}
10
+ import SCALAPACKAGE.core.*
11
+
12
+ import java.util.{ArrayList, LinkedHashMap, List as JList, Map as JMap}
13
+ import scala.collection.mutable.{ArrayBuffer, LinkedHashMap as MLinkedHashMap}
14
+ import scala.jdk.CollectionConverters.*
15
+
16
+ object PrimaryCorpus {
17
+
18
+ // ---- Value <-> Java bridge ---------------------------------------------
19
+
20
+ def toJava(v: Value): Object = v match {
21
+ case VMap(m) =>
22
+ val out = new LinkedHashMap[String, Object]()
23
+ m.foreach { case (k, x) => out.put(k, toJava(x)) }
24
+ out
25
+ case VList(b) =>
26
+ val out = new ArrayList[Object]()
27
+ b.foreach { x => out.add(toJava(x)) }
28
+ out
29
+ case VStr(s) => s
30
+ case VBool(b) => java.lang.Boolean.valueOf(b)
31
+ case VNum(n) =>
32
+ // The corpus writes whole numbers as JSON integers; handing the SDK a
33
+ // Double turned "1" into "1.0" everywhere it reached a URL or header.
34
+ if (n == Math.floor(n) && !n.isInfinite) java.lang.Long.valueOf(n.toLong)
35
+ else java.lang.Double.valueOf(n)
36
+ case _ => null
37
+ }
38
+
39
+ def toValue(o: Object): Value = o match {
40
+ case null => VNull
41
+ // Struct.UNDEF is the library's "no value" sentinel, not a value: an
42
+ // unresolved transform path returns it, and without this it reached the
43
+ // corpus as the string "java.lang.Object@..." instead of null.
44
+ case x if x eq SCALAPACKAGE.utility.struct.Struct.UNDEF => VNull
45
+ case m: JMap[_, _] =>
46
+ val out = MLinkedHashMap.empty[String, Value]
47
+ m.asInstanceOf[JMap[String, Object]].asScala.foreach { case (k, x) => out.put(k, toValue(x)) }
48
+ VMap(out)
49
+ case l: JList[_] =>
50
+ val out = ArrayBuffer.empty[Value]
51
+ l.asInstanceOf[JList[Object]].asScala.foreach { x => out += toValue(x) }
52
+ VList(out)
53
+ case s: String => VStr(s)
54
+ case b: java.lang.Boolean => VBool(b.booleanValue())
55
+ case n: java.lang.Number => VNum(n.doubleValue())
56
+ case e: RuntimeException =>
57
+ val m = MLinkedHashMap.empty[String, Value]
58
+ m.put("message", VStr(String.valueOf(e.getMessage)))
59
+ VMap(m)
60
+ case x => VStr(String.valueOf(x))
61
+ }
62
+
63
+ private def jmap(v: Value): JMap[String, Object] = toJava(v) match {
64
+ case m: JMap[_, _] => m.asInstanceOf[JMap[String, Object]]
65
+ case _ => new LinkedHashMap[String, Object]()
66
+ }
67
+
68
+ private def vget(v: Value, k: String): Value = v match {
69
+ case VMap(m) => m.getOrElse(k, Noval)
70
+ case _ => Noval
71
+ }
72
+
73
+ // ---- corpus access -----------------------------------------------------
74
+
75
+ private var CORPUS: Value = Noval
76
+
77
+ private def sectionBasic(name: String): Value = vget(vget(vget(CORPUS, "primary"), name), "basic")
78
+
79
+ // makeSpec and prepareAuth read defaults off the CLIENT, as the ts reference
80
+ // does via client.options(), so a section's DEF.setup cannot reach them
81
+ // through ctx.options — those sections get their own client.
82
+ private def sectionSetup(name: String): Value =
83
+ vget(vget(vget(vget(vget(CORPUS, "primary"), name), "DEF"), "setup"), "a")
84
+
85
+ // ---- live context from a corpus map ------------------------------------
86
+
87
+ private def corpusCtx(client: ProjectNameSDK, ctxmap: Value): Context = {
88
+ val utility = client.getUtility()
89
+ val cm = new LinkedHashMap[String, Object]()
90
+ // Only when the corpus names one: defaulting to "load" made the SDK report
91
+ // the wrong operation in the error messages the corpus matches on.
92
+ vget(ctxmap, "opname") match {
93
+ case VStr(s) => cm.put("opname", s)
94
+ case _ =>
95
+ }
96
+ val ctx = utility.makeContext(cm, client.getRootCtx())
97
+
98
+ vget(ctxmap, "spec") match {
99
+ case m @ VMap(_) => ctx.spec = new Spec(jmap(m))
100
+ case _ =>
101
+ }
102
+
103
+ vget(ctxmap, "result") match {
104
+ case m @ VMap(_) =>
105
+ val r = new Result(jmap(m))
106
+ // Result does not carry an err in from the map, so a corpus result
107
+ // holding one arrived empty and resultBasic had no previous message
108
+ // to prepend.
109
+ vget(vget(m, "err"), "message") match {
110
+ case VStr(msg) if msg.nonEmpty => r.err = new SdkError("", msg, null)
111
+ case _ =>
112
+ }
113
+ ctx.result = r
114
+ case _ =>
115
+ }
116
+
117
+ vget(ctxmap, "response") match {
118
+ case m @ VMap(_) =>
119
+ val resp = new Response(jmap(m))
120
+ // resultBody reads response.json and requires it to be CALLABLE; the
121
+ // corpus supplies a plain `body`.
122
+ vget(m, "body") match {
123
+ case Noval => ()
124
+ case b =>
125
+ val bj = toJava(b)
126
+ resp.body = bj
127
+ resp.jsonFunc = new java.util.function.Supplier[Object] { def get(): Object = bj }
128
+ }
129
+ // Header names arrive from the wire in any case and the contract is
130
+ // lowercase; resultHeaders copies them verbatim.
131
+ vget(m, "headers") match {
132
+ case VMap(hm) =>
133
+ val low = new LinkedHashMap[String, Object]()
134
+ hm.foreach { case (k, x) => low.put(k.toLowerCase, toJava(x)) }
135
+ resp.headers = low
136
+ case _ =>
137
+ }
138
+ ctx.response = resp
139
+ case _ =>
140
+ }
141
+
142
+ vget(ctxmap, "point") match { case m @ VMap(_) => ctx.point = jmap(m); case _ => }
143
+ vget(ctxmap, "reqdata") match { case Noval => (); case v => ctx.reqdata = jmap(v) }
144
+ vget(ctxmap, "reqmatch") match { case Noval => (); case v => ctx.reqmatch = jmap(v) }
145
+ vget(ctxmap, "data") match { case Noval => (); case v => ctx.data = jmap(v) }
146
+ vget(ctxmap, "match") match { case Noval => (); case v => ctx.matchData = jmap(v) }
147
+ vget(ctxmap, "options") match { case m @ VMap(_) => ctx.options = jmap(m); case _ => }
148
+ vget(ctxmap, "config") match { case m @ VMap(_) => ctx.config = jmap(m); case _ => }
149
+ ctx
150
+ }
151
+
152
+ // The match reads the corpus map while the utilities mutate the live objects
153
+ // hanging off the context, so without this every ctx.* assertion reads null.
154
+ private def publishCtx(ctxmap: Value, ctx: Context): Unit = ctxmap match {
155
+ case VMap(m) =>
156
+ if (ctx.spec != null) m.put("spec", specValue(ctx.spec))
157
+ if (ctx.result != null) m.put("result", resultValue(ctx.result))
158
+ if (ctx.response != null) m.put("response", VStr(Runner.EXISTSMARK))
159
+ case _ =>
160
+ }
161
+
162
+ private def specValue(s: Spec): Value = {
163
+ val m = MLinkedHashMap.empty[String, Value]
164
+ m.put("base", VStr(s.base)); m.put("prefix", VStr(s.prefix)); m.put("suffix", VStr(s.suffix))
165
+ m.put("path", VStr(s.path)); m.put("url", VStr(s.url)); m.put("step", VStr(s.step))
166
+ m.put("method", VStr(s.method))
167
+ m.put("headers", toValue(s.headers)); m.put("params", toValue(s.params))
168
+ m.put("query", toValue(s.query)); m.put("alias", toValue(s.alias))
169
+ if (s.parts != null) m.put("parts", toValue(s.parts))
170
+ if (s.body != null) m.put("body", toValue(s.body))
171
+ VMap(m)
172
+ }
173
+
174
+ private def resultValue(r: Result): Value = {
175
+ val m = MLinkedHashMap.empty[String, Value]
176
+ m.put("ok", VBool(r.ok)); m.put("status", VNum(r.status.toDouble))
177
+ m.put("statusText", VStr(r.statusText))
178
+ m.put("headers", toValue(r.headers))
179
+ if (r.body != null) m.put("body", toValue(r.body))
180
+ if (r.resdata != null) m.put("resdata", toValue(r.resdata))
181
+ if (r.resmatch != null) m.put("resmatch", toValue(r.resmatch))
182
+ if (r.err != null) {
183
+ val e = MLinkedHashMap.empty[String, Value]
184
+ e.put("message", VStr(String.valueOf(r.err.getMessage)))
185
+ m.put("err", VMap(e))
186
+ }
187
+ VMap(m)
188
+ }
189
+
190
+ // ---- section runners ---------------------------------------------------
191
+
192
+ private def argAt(args: Seq[Value], i: Int): Value = if (i < args.length) args(i) else Noval
193
+
194
+ // A section driven with a ctx built from the corpus entry.
195
+ private def runset(name: String, client: ProjectNameSDK)(f: (Context, Seq[Value]) => Value): Unit =
196
+ Runner.runSet(name, sectionBasic(name), (args: Seq[Value]) => {
197
+ val ctxmap = argAt(args, 0)
198
+ val ctx = corpusCtx(client, ctxmap)
199
+ val out = f(ctx, args)
200
+ publishCtx(ctxmap, ctx)
201
+ out
202
+ })
203
+
204
+ // A section that takes a bare map or explicit args rather than a ctx.
205
+ private def runsetArgs(name: String)(f: Seq[Value] => Value): Unit =
206
+ Runner.runSet(name, sectionBasic(name), f)
207
+
208
+ private def clientFor(name: String, shared: ProjectNameSDK): ProjectNameSDK =
209
+ sectionSetup(name) match {
210
+ case m @ VMap(_) => ProjectNameSDK.testSDK(null, jmap(m))
211
+ case _ => shared
212
+ }
213
+
214
+ def run(testfile: String): Int = {
215
+ val src = new String(java.nio.file.Files.readAllBytes(java.nio.file.Paths.get(testfile)), "UTF-8")
216
+ CORPUS = Runner.jsonRead(src)
217
+
218
+ val sdk = ProjectNameSDK.testSDK()
219
+ val u = sdk.getUtility()
220
+
221
+ runset("done", sdk) { (c, _) => toValue(u.done(c)) }
222
+ runset("makeUrl", sdk) { (c, _) => toValue(u.makeUrl(c)) }
223
+ runset("makeRequest", sdk) { (c, _) => u.makeRequest(c); resultValue(c.result) }
224
+ runset("makeResponse", sdk) { (c, _) => u.makeResponse(c); resultValue(c.result) }
225
+ runset("makeSpec", clientFor("makeSpec", sdk)) { (c, _) =>
226
+ val s = u.makeSpec(c); if (s != null) c.spec = s; specValue(c.spec)
227
+ }
228
+ runset("prepareAuth", clientFor("prepareAuth", sdk)) { (c, _) =>
229
+ u.prepareAuth(c); specValue(c.spec)
230
+ }
231
+ runset("prepareBody", sdk) { (c, _) => toValue(u.prepareBody(c)) }
232
+ runset("prepareHeaders", sdk) { (c, _) => toValue(u.prepareHeaders(c)) }
233
+ runset("prepareMethod", sdk) { (c, _) =>
234
+ val m = u.prepareMethod(c); if (m == null || m.isEmpty) VNull else VStr(m)
235
+ }
236
+ runset("prepareParams", sdk) { (c, _) => toValue(u.prepareParams(c)) }
237
+ runset("preparePath", sdk) { (c, _) => toValue(u.preparePath(c)) }
238
+ runset("prepareQuery", sdk) { (c, _) => toValue(u.prepareQuery(c)) }
239
+ runset("resultBasic", sdk) { (c, _) =>
240
+ val r = u.resultBasic(c); if (r != null) c.result = r; resultValue(c.result)
241
+ }
242
+ runset("resultBody", sdk) { (c, _) =>
243
+ val r = u.resultBody(c); if (r != null) c.result = r; resultValue(c.result)
244
+ }
245
+ runset("resultHeaders", sdk) { (c, _) =>
246
+ val r = u.resultHeaders(c); if (r != null) c.result = r; resultValue(c.result)
247
+ }
248
+ runset("transformRequest", sdk) { (c, _) => toValue(u.transformRequest(c)) }
249
+ runset("transformResponse", sdk) { (c, _) => toValue(u.transformResponse(c)) }
250
+ runset("param", sdk) { (c, args) => toValue(u.param(c, toJava(argAt(args, 1)))) }
251
+ runset("makeError", sdk) { (c, args) =>
252
+ val msg = vget(argAt(args, 1), "message") match { case VStr(s) => s; case _ => "" }
253
+ val in = if (msg.isEmpty) null else new SdkError("", msg, null)
254
+ toValue(u.makeError(c, in))
255
+ }
256
+
257
+ runsetArgs("makeContext") { args =>
258
+ val c = corpusCtx(sdk, argAt(args, 0))
259
+ val op = MLinkedHashMap.empty[String, Value]
260
+ if (c.op != null) {
261
+ op.put("entity", VStr(c.op.entity)); op.put("name", VStr(c.op.name))
262
+ op.put("input", VStr(c.op.input)); op.put("points", toValue(c.op.points))
263
+ }
264
+ val out = MLinkedHashMap.empty[String, Value]
265
+ out.put("op", VMap(op))
266
+ VMap(out)
267
+ }
268
+ runsetArgs("makeOptions") { args =>
269
+ val in = argAt(args, 0)
270
+ val c = corpusCtx(sdk, VMap(MLinkedHashMap.empty[String, Value]))
271
+ vget(in, "config") match { case Noval => (); case v => c.config = jmap(v) }
272
+ vget(in, "options") match { case Noval => (); case v => c.options = jmap(v) }
273
+ toValue(u.makeOptions(c))
274
+ }
275
+ runsetArgs("operator") { args =>
276
+ val in = argAt(args, 0)
277
+ val m = MLinkedHashMap.empty[String, Value]
278
+ m.put("entity", vget(in, "entity") match { case Noval => VStr("_"); case v => v })
279
+ m.put("input", vget(in, "input") match { case Noval => VStr("_"); case v => v })
280
+ m.put("name", vget(in, "name") match { case Noval => VStr("_"); case v => v })
281
+ m.put("points", vget(in, "points") match { case l @ VList(_) => l; case _ => VList(ArrayBuffer.empty[Value]) })
282
+ VMap(m)
283
+ }
284
+
285
+ Runner.reportPrimary()
286
+ }
287
+ }
288
+
289
+ object PrimaryCorpusMain {
290
+ def main(args: Array[String]): Unit = {
291
+ val testfile = if (args.length > 0) args(0) else "../.sdk/test/test.json"
292
+ System.exit(PrimaryCorpus.run(testfile))
293
+ }
294
+ }
@@ -199,13 +199,60 @@ object MakePoint {
199
199
  }
200
200
 
201
201
  object MakeOptions {
202
+
203
+ /**
204
+ * Replaces one utility member from `options.utility`, matching the ts
205
+ * reference: a key naming a real member REPLACES it, and any other key is
206
+ * attached as a custom extra. Returns false when the key names no member or
207
+ * the value is not that member's type, so the caller keeps it in `custom`.
208
+ *
209
+ * REFLECTION, NOT A KEYED SWITCH. The go and java ports list every member by
210
+ * hand and carry a "keep this in step with registerAll" warning, because a
211
+ * utility added to one list and not the other is overridable there and not
212
+ * here. The field set is readable off the class, so the list cannot drift.
213
+ *
214
+ * Scala `var` fields compile to a private field plus accessors, so the
215
+ * lookup is on the declared field and needs setAccessible. Function types
216
+ * erase on the JVM, so isInstance checks the FunctionN arity rather than the
217
+ * full signature - the same limit java's port documents.
218
+ *
219
+ * Only a PUBLIC name may replace a member: public utility names are
220
+ * camelCase and carry no underscore, so an underscore means the caller named
221
+ * something of their own rather than a member.
222
+ */
223
+ def overrideUtil(utility: Utility, key: String, value: Object): Boolean = {
224
+ if (null == key || key.isEmpty || key.contains("_") || "custom" == key) return false
225
+ if (null == value) return false
226
+
227
+ try {
228
+ val field = classOf[Utility].getDeclaredField(key)
229
+ if (!field.getType.isInstance(value)) return false
230
+ field.setAccessible(true)
231
+ field.set(utility, value)
232
+ true
233
+ }
234
+ catch {
235
+ case _: NoSuchFieldException => false
236
+ }
237
+ }
238
+
202
239
  def makeOptions(ctx: Context): JMap[String, Object] = {
203
240
  var options = ctx.options
204
241
  if (options == null) options = new LinkedHashMap[String, Object]()
205
242
 
206
- // Merge custom utility overrides onto the utility object.
243
+ // Merge utility overrides from options onto the utility object.
244
+ //
245
+ // A key naming a real utility member REPLACES it; anything else is
246
+ // attached as a custom extra. Shelving everything in `custom` - a map
247
+ // nothing reads - made `utility -> Map("fetcher" -> ...)`, the documented
248
+ // transport seam, a silent no-op here while ts honoured it.
207
249
  val customUtils = Helpers.toMapAny(options.get("utility"))
208
- if (customUtils != null && ctx.utility != null) ctx.utility.custom.putAll(customUtils)
250
+ if (customUtils != null && ctx.utility != null) {
251
+ customUtils.entrySet().forEach { e =>
252
+ if (!overrideUtil(ctx.utility, e.getKey, e.getValue))
253
+ ctx.utility.custom.put(e.getKey, e.getValue)
254
+ }
255
+ }
209
256
 
210
257
  var opts = Struct.clone(options).asInstanceOf[JMap[String, Object]]
211
258
 
@@ -26,7 +26,9 @@ object PrepareMethod {
26
26
  case "list" => "GET"
27
27
  case "remove" => "DELETE"
28
28
  case "patch" => "PATCH"
29
- case _ => "GET"
29
+ // No GET catch-all: an unrecognised op must fall through to the
30
+ // allow.method gate, not be silently issued as a GET.
31
+ case _ => ""
30
32
  }
31
33
  }
32
34
  }
@@ -57,12 +57,30 @@ private func buildOptSpec() -> Value {
57
57
  func makeOptionsUtil(_ ctx: Context) -> VMap {
58
58
  let options = ctx.options ?? VMap()
59
59
 
60
- // Merge custom utility overrides onto the utility object. Read from the
61
- // original options before clone for safety.
60
+ // Merge utility overrides from options onto the utility object. Read from
61
+ // the original options before clone for safety.
62
+ //
63
+ // `fetcher` REPLACES the member; every other key is attached as a custom
64
+ // extra. Shelving the fetcher too made `utility: ["fetcher": ...]`, the
65
+ // documented transport seam, a silent no-op here while ts honoured it - and
66
+ // it is the seam the shared feature corpus scripts.
67
+ //
68
+ // PARTIAL, DELIBERATELY. The other members are replaceable in principle -
69
+ // Value.native carries any Swift value - but each has a distinct closure
70
+ // type, so honouring them means one `as?` arm per member with the signature
71
+ // written exactly. `fetcher` is the documented seam and the one the corpus
72
+ // needs; the rest stay in `custom` until a swift toolchain can compile the
73
+ // arms rather than have them written blind. See AGENTS.md.
62
74
  if let customUtils = gp(options, "utility").asMap {
63
75
  if let utility = ctx.utility {
64
76
  for (k, v) in customUtils.entries {
65
- utility.custom[k] = v.asNative ?? v
77
+ let native = v.asNative ?? v
78
+ if "fetcher" == k, let fn = native as? FetcherFunc {
79
+ utility.fetcher = fn
80
+ }
81
+ else {
82
+ utility.custom[k] = native
83
+ }
66
84
  }
67
85
  }
68
86
  }
@@ -30,8 +30,19 @@ function ownIdField(config: any, getpath: any, entityName: string): string {
30
30
  if (ptterm !== bestterm ? ptterm : pt.parts.length < best.parts.length) best = pt
31
31
  }
32
32
  const parts: string[] = (best && best.parts) || []
33
- const last = [...parts].reverse().find((p: string) => p.startsWith('{'))
34
- if (null != last) return last.slice(1, -1)
33
+ // THE LAST PART, not the last param anywhere in the path. A record route
34
+ // ENDS in its key: /orgs/{org}/private-registries/{secret_name} does,
35
+ // /orgs/{org}/private-registries/public-key does not. Reading the last
36
+ // param wherever it fell returned `{org}` for that second path — a PARENT
37
+ // reference — and the seeding walk then stamped the record's own key over
38
+ // org_id, destroying the ORG01 the fixture set and the test looks up from
39
+ // idmap. github's private_registry failed its update with a 404 that named
40
+ // nothing to do with orgs.
41
+ //
42
+ // A point that does not end in a param says nothing about this entity's
43
+ // key, so move on to the next op rather than guess from it.
44
+ const lastPart = 0 < parts.length ? String(parts[parts.length - 1]) : ''
45
+ if (lastPart.startsWith('{')) return lastPart.slice(1, -1)
35
46
  }
36
47
  return 'id'
37
48
  }