@voxgig/sdkgen 3.5.1 → 3.6.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 (171) hide show
  1. package/bin/voxgig-sdkgen +1 -1
  2. package/dist/cmp/ExternalTarget.js +1 -1
  3. package/dist/cmp/ExternalTarget.js.map +1 -1
  4. package/dist/helpers/naming.d.ts +4 -1
  5. package/dist/helpers/naming.js +119 -5
  6. package/dist/helpers/naming.js.map +1 -1
  7. package/dist/helpers/opShape.js +72 -0
  8. package/dist/helpers/opShape.js.map +1 -1
  9. package/dist/helpers/packageMeta.d.ts +2 -1
  10. package/dist/helpers/packageMeta.js +57 -20
  11. package/dist/helpers/packageMeta.js.map +1 -1
  12. package/dist/sdkgen.d.ts +3 -3
  13. package/dist/sdkgen.js +61 -18
  14. package/dist/sdkgen.js.map +1 -1
  15. package/dist/testkit.d.ts +39 -0
  16. package/dist/testkit.js +663 -0
  17. package/dist/testkit.js.map +1 -0
  18. package/dist/tsconfig.tsbuildinfo +1 -1
  19. package/package.json +32 -8
  20. package/project/.sdk/src/cmp/c/Test_c.ts +5 -7
  21. package/project/.sdk/src/cmp/clojure/Test_clojure.ts +3 -7
  22. package/project/.sdk/src/cmp/cpp/Test_cpp.ts +4 -7
  23. package/project/.sdk/src/cmp/csharp/Package_csharp.ts +1 -1
  24. package/project/.sdk/src/cmp/csharp/Test_csharp.ts +5 -7
  25. package/project/.sdk/src/cmp/dart/Test_dart.ts +3 -4
  26. package/project/.sdk/src/cmp/elixir/Package_elixir.ts +1 -1
  27. package/project/.sdk/src/cmp/elixir/Test_elixir.ts +5 -6
  28. package/project/.sdk/src/cmp/go/EntityTypes_go.ts +1 -1
  29. package/project/.sdk/src/cmp/go/Entity_go.ts +1 -1
  30. package/project/.sdk/src/cmp/go/Gitignore_go.ts +6 -0
  31. package/project/.sdk/src/cmp/go/Main_go.ts +2 -2
  32. package/project/.sdk/src/cmp/go/Package_go.ts +1 -1
  33. package/project/.sdk/src/cmp/go/ReadmeExamplesTest_go.ts +59 -6
  34. package/project/.sdk/src/cmp/go/ReadmeExplanation_go.ts +1 -1
  35. package/project/.sdk/src/cmp/go/ReadmeHowto_go.ts +1 -1
  36. package/project/.sdk/src/cmp/go/ReadmeInstall_go.ts +1 -1
  37. package/project/.sdk/src/cmp/go/ReadmeModel_go.ts +1 -1
  38. package/project/.sdk/src/cmp/go/ReadmeQuick_go.ts +1 -1
  39. package/project/.sdk/src/cmp/go/ReadmeTopQuick_go.ts +1 -1
  40. package/project/.sdk/src/cmp/go/ReadmeTopTest_go.ts +1 -1
  41. package/project/.sdk/src/cmp/go/Test_go.ts +6 -8
  42. package/project/.sdk/src/cmp/go-cli/Main_go-cli.ts +6 -2
  43. package/project/.sdk/src/cmp/go-mcp/Main_go-mcp.ts +6 -2
  44. package/project/.sdk/src/cmp/java/Test_java.ts +4 -7
  45. package/project/.sdk/src/cmp/js/Package_js.ts +1 -1
  46. package/project/.sdk/src/cmp/js/Test_js.ts +4 -7
  47. package/project/.sdk/src/cmp/kotlin/Test_kotlin.ts +4 -7
  48. package/project/.sdk/src/cmp/lean/Test_lean.ts +3 -3
  49. package/project/.sdk/src/cmp/lua/Package_lua.ts +2 -2
  50. package/project/.sdk/src/cmp/lua/Test_lua.ts +5 -7
  51. package/project/.sdk/src/cmp/ocaml/Test_ocaml.ts +3 -7
  52. package/project/.sdk/src/cmp/perl/Package_perl.ts +1 -1
  53. package/project/.sdk/src/cmp/perl/Test_perl.ts +5 -7
  54. package/project/.sdk/src/cmp/php/EntityTypes_php.ts +13 -2
  55. package/project/.sdk/src/cmp/php/Package_php.ts +2 -2
  56. package/project/.sdk/src/cmp/php/Test_php.ts +5 -7
  57. package/project/.sdk/src/cmp/py/Package_py.ts +2 -2
  58. package/project/.sdk/src/cmp/py/Test_py.ts +5 -7
  59. package/project/.sdk/src/cmp/py-data/Main_py-data.ts +14 -19
  60. package/project/.sdk/src/cmp/rb/Package_rb.ts +3 -3
  61. package/project/.sdk/src/cmp/rb/ReadmeInstall_rb.ts +1 -1
  62. package/project/.sdk/src/cmp/rb/Test_rb.ts +5 -7
  63. package/project/.sdk/src/cmp/rust/Test_rust.ts +5 -7
  64. package/project/.sdk/src/cmp/scala/Test_scala.ts +5 -2
  65. package/project/.sdk/src/cmp/seneca-provider/Gitignore_seneca-provider.ts +161 -0
  66. package/project/.sdk/src/cmp/seneca-provider/Main_seneca-provider.ts +20 -11
  67. package/project/.sdk/src/cmp/swift/Test_swift.ts +5 -7
  68. package/project/.sdk/src/cmp/ts/Package_ts.ts +1 -1
  69. package/project/.sdk/src/cmp/ts/Test_ts.ts +8 -7
  70. package/project/.sdk/src/cmp/zig/Test_zig.ts +3 -7
  71. package/project/.sdk/tm/c/utility/make_point.c +64 -3
  72. package/project/.sdk/tm/clojure/src/sdk/core.clj +49 -14
  73. package/project/.sdk/tm/cpp/utility/pipeline.hpp +55 -3
  74. package/project/.sdk/tm/csharp/feature/TestFeature.cs +36 -2
  75. package/project/.sdk/tm/csharp/utility/MakePoint.cs +65 -2
  76. package/project/.sdk/tm/dart/lib/feature/test/TestFeature.dart +34 -4
  77. package/project/.sdk/tm/dart/lib/utility/MakePointUtility.dart +48 -2
  78. package/project/.sdk/tm/elixir/lib/projectname/utility.ex +70 -10
  79. package/project/.sdk/tm/go/feature/test_feature.go +34 -2
  80. package/project/.sdk/tm/go/utility/make_point.go +66 -2
  81. package/project/.sdk/tm/java/feature/TestFeature.java +42 -2
  82. package/project/.sdk/tm/java/utility/MakePoint.java +61 -2
  83. package/project/.sdk/tm/js/src/feature/test/TestFeature.js +22 -3
  84. package/project/.sdk/tm/js/src/utility/MakePointUtility.js +72 -2
  85. package/project/.sdk/tm/kotlin/feature/TestFeature.kt +31 -2
  86. package/project/.sdk/tm/kotlin/utility/MakePoint.kt +58 -2
  87. package/project/.sdk/tm/lean/src/SdkUtility.lean +28 -0
  88. package/project/.sdk/tm/lua/utility/make_point.lua +65 -2
  89. package/project/.sdk/tm/ocaml/sdk_runtime.ml +40 -2
  90. package/project/.sdk/tm/perl/utility/make_point.pm +55 -2
  91. package/project/.sdk/tm/php/feature/TestFeature.php +30 -1
  92. package/project/.sdk/tm/php/utility/MakePoint.php +49 -1
  93. package/project/.sdk/tm/py/pkg/utility/make_point.py +56 -2
  94. package/project/.sdk/tm/rb/utility/make_point.rb +42 -2
  95. package/project/.sdk/tm/rust/utility/make_point.rs +69 -2
  96. package/project/.sdk/tm/scala/feature/TestFeature.scala +36 -1
  97. package/project/.sdk/tm/scala/utility/Make.scala +57 -3
  98. package/project/.sdk/tm/swift/Sources/ProjectNameSDK/feature/TestFeature.swift +29 -1
  99. package/project/.sdk/tm/swift/Sources/ProjectNameSDK/utility/Make.swift +51 -2
  100. package/project/.sdk/tm/ts/src/feature/test/TestFeature.ts +22 -3
  101. package/project/.sdk/tm/ts/src/utility/MakePointUtility.ts +72 -2
  102. package/project/.sdk/tm/zig/core/utility.zig +66 -3
  103. package/project/sdkgen-package.json +1 -2
  104. package/src/cmp/ExternalTarget.ts +1 -1
  105. package/src/helpers/naming.ts +129 -5
  106. package/src/helpers/opShape.ts +85 -0
  107. package/src/helpers/packageMeta.ts +59 -20
  108. package/src/sdkgen.ts +68 -20
  109. package/src/testkit.ts +863 -0
  110. package/testkit.d.ts +3 -0
  111. package/testkit.js +12 -0
  112. package/project/.sdk/model/target/haskell.aontu +0 -72
  113. package/project/.sdk/src/cmp/haskell/Config_haskell.ts +0 -98
  114. package/project/.sdk/src/cmp/haskell/Entity_haskell.ts +0 -13
  115. package/project/.sdk/src/cmp/haskell/Gitignore_haskell.ts +0 -27
  116. package/project/.sdk/src/cmp/haskell/Main_haskell.ts +0 -123
  117. package/project/.sdk/src/cmp/haskell/Package_haskell.ts +0 -60
  118. package/project/.sdk/src/cmp/haskell/ReadmeEntity_haskell.ts +0 -186
  119. package/project/.sdk/src/cmp/haskell/ReadmeExamplesTest_haskell.ts +0 -135
  120. package/project/.sdk/src/cmp/haskell/ReadmeExplanation_haskell.ts +0 -48
  121. package/project/.sdk/src/cmp/haskell/ReadmeHowto_haskell.ts +0 -168
  122. package/project/.sdk/src/cmp/haskell/ReadmeInstall_haskell.ts +0 -51
  123. package/project/.sdk/src/cmp/haskell/ReadmeIntro_haskell.ts +0 -67
  124. package/project/.sdk/src/cmp/haskell/ReadmeModel_haskell.ts +0 -162
  125. package/project/.sdk/src/cmp/haskell/ReadmeOptions_haskell.ts +0 -74
  126. package/project/.sdk/src/cmp/haskell/ReadmeQuick_haskell.ts +0 -241
  127. package/project/.sdk/src/cmp/haskell/ReadmeRef_haskell.ts +0 -408
  128. package/project/.sdk/src/cmp/haskell/ReadmeTopHowto_haskell.ts +0 -31
  129. package/project/.sdk/src/cmp/haskell/ReadmeTopQuick_haskell.ts +0 -104
  130. package/project/.sdk/src/cmp/haskell/ReadmeTopTest_haskell.ts +0 -80
  131. package/project/.sdk/src/cmp/haskell/Test_haskell.ts +0 -300
  132. package/project/.sdk/src/cmp/haskell/tsconfig.json +0 -15
  133. package/project/.sdk/src/cmp/haskell/utility_haskell.ts +0 -166
  134. package/project/.sdk/tm/haskell/LICENSE +0 -21
  135. package/project/.sdk/tm/haskell/Makefile +0 -22
  136. package/project/.sdk/tm/haskell/VERSION +0 -1
  137. package/project/.sdk/tm/haskell/src/SdkFeatures.hs +0 -1440
  138. package/project/.sdk/tm/haskell/src/SdkHelpers.hs +0 -337
  139. package/project/.sdk/tm/haskell/src/SdkJson.hs +0 -111
  140. package/project/.sdk/tm/haskell/src/SdkRuntime.hs +0 -1219
  141. package/project/.sdk/tm/haskell/src/SdkTypes.hs +0 -195
  142. package/project/.sdk/tm/haskell/src/VoxgigStruct.hs +0 -2299
  143. package/project/.sdk/tm/haskell/src/Vregex.hs +0 -237
  144. package/project/.sdk/tm/haskell/src/feature/README.md +0 -5
  145. package/project/.sdk/tm/haskell/src/feature/audit/.gitkeep +0 -0
  146. package/project/.sdk/tm/haskell/src/feature/base/.gitkeep +0 -0
  147. package/project/.sdk/tm/haskell/src/feature/cache/.gitkeep +0 -0
  148. package/project/.sdk/tm/haskell/src/feature/clienttrack/.gitkeep +0 -0
  149. package/project/.sdk/tm/haskell/src/feature/debug/.gitkeep +0 -0
  150. package/project/.sdk/tm/haskell/src/feature/idempotency/.gitkeep +0 -0
  151. package/project/.sdk/tm/haskell/src/feature/log/.gitkeep +0 -0
  152. package/project/.sdk/tm/haskell/src/feature/metrics/.gitkeep +0 -0
  153. package/project/.sdk/tm/haskell/src/feature/netsim/.gitkeep +0 -0
  154. package/project/.sdk/tm/haskell/src/feature/paging/.gitkeep +0 -0
  155. package/project/.sdk/tm/haskell/src/feature/proxy/.gitkeep +0 -0
  156. package/project/.sdk/tm/haskell/src/feature/ratelimit/.gitkeep +0 -0
  157. package/project/.sdk/tm/haskell/src/feature/rbac/.gitkeep +0 -0
  158. package/project/.sdk/tm/haskell/src/feature/retry/.gitkeep +0 -0
  159. package/project/.sdk/tm/haskell/src/feature/streaming/.gitkeep +0 -0
  160. package/project/.sdk/tm/haskell/src/feature/telemetry/.gitkeep +0 -0
  161. package/project/.sdk/tm/haskell/src/feature/test/.gitkeep +0 -0
  162. package/project/.sdk/tm/haskell/src/feature/timeout/.gitkeep +0 -0
  163. package/project/.sdk/tm/haskell/test/Harness.hs +0 -281
  164. package/project/.sdk/tm/haskell/test/Runner.hs +0 -47
  165. package/project/.sdk/tm/haskell/test/StructCorpus.hs +0 -449
  166. package/project/.sdk/tm/haskell/test/TCustomUtility.hs +0 -38
  167. package/project/.sdk/tm/haskell/test/TFeature.hs +0 -794
  168. package/project/.sdk/tm/haskell/test/TNetsim.hs +0 -50
  169. package/project/.sdk/tm/haskell/test/TPipeline.hs +0 -334
  170. package/project/.sdk/tm/haskell/test/TPrimaryUtility.hs +0 -386
  171. package/project/.sdk/tm/haskell/test/Testutil.hs +0 -59
@@ -34,9 +34,10 @@ dynamic makePoint(dynamic ctx) {
34
34
  final selector = 'data' == op.input ? ctx.data : ctx.match;
35
35
 
36
36
  dynamic point;
37
+ var matched = false;
37
38
  for (var i = 0; i < op.points.length; i++) {
38
- point = op.points[i];
39
- final select = point.select;
39
+ final cand = op.points[i];
40
+ final select = cand.select;
40
41
  var found = true;
41
42
 
42
43
  final exist = vs.getprop(select, 'exist');
@@ -60,10 +61,55 @@ dynamic makePoint(dynamic ctx) {
60
61
  }
61
62
 
62
63
  if (found) {
64
+ point = cand;
65
+ matched = true;
63
66
  break;
64
67
  }
65
68
  }
66
69
 
70
+ // select.exist can list more than the params needed to pick a point, so
71
+ // nothing matches — fall back to the entity's own route rather than
72
+ // whichever point came last.
73
+ if (!matched) {
74
+ // A request naming an action reaches here only because that action's
75
+ // own point failed its exist test, so it is unbuildable whatever we
76
+ // pick. Refuse it BEFORE choosing a fallback: the guard below compares
77
+ // the chosen point's $action and would wave the request through
78
+ // whenever the fallback lands on the action point itself.
79
+ if (null != vs.getprop(reqselector, r'$action')) {
80
+ return ctx.error(
81
+ 'point_action_invalid',
82
+ 'Operation "' +
83
+ op.name.toString() +
84
+ '" action "' +
85
+ vs.getprop(reqselector, r'$action').toString() +
86
+ '" is not valid.');
87
+ }
88
+
89
+ // A terminal parameter marks a record route (/boards/{id}); a
90
+ // cross-reference ends in the relationship's name
91
+ // (/posts/{id}/author). Failing that, the shallower path wins.
92
+ bool terminalParam(dynamic p) {
93
+ final parts = p.parts;
94
+ if (parts.isEmpty) {
95
+ return false;
96
+ }
97
+ final last = parts[parts.length - 1];
98
+ return last is String && last.startsWith('{');
99
+ }
100
+
101
+ point = op.points[0];
102
+ for (final cand in op.points) {
103
+ if (terminalParam(cand) != terminalParam(point)) {
104
+ if (terminalParam(cand)) {
105
+ point = cand;
106
+ }
107
+ } else if (cand.parts.length < point.parts.length) {
108
+ point = cand;
109
+ }
110
+ }
111
+ }
112
+
67
113
  if (null != vs.getprop(reqselector, r'$action') &&
68
114
  null != point &&
69
115
  vs.getprop(reqselector, r'$action') !=
@@ -465,21 +465,81 @@ defmodule ProjectName.Utility do
465
465
 
466
466
  if found, do: {:halt, point}, else: {:cont, point}
467
467
  end)
468
- |> elem(1)
469
468
 
470
- err =
471
- if reqselector != nil do
472
- req_action = S.getprop(reqselector, "$action")
469
+ matched? = match?({:halt, _}, point)
470
+
471
+ point =
472
+ if matched? do
473
+ elem(point, 1)
474
+ else
475
+ # select.exist can list more than the params needed to pick a
476
+ # point, so nothing matched. Fall back to the entity's own
477
+ # route: a terminal parameter marks a record route
478
+ # (/boards/{id}) where a cross-reference ends in the
479
+ # relationship's name (/posts/{id}/author), and failing that
480
+ # the shallower path wins. The same rule runs at generation
481
+ # time, in helpers/opShape.ts — both sides must move together.
482
+ parts_len = fn p ->
483
+ parts = S.getprop(p, "parts")
484
+ if S.islist(parts), do: S.size(parts), else: 0
485
+ end
473
486
 
474
- if req_action != nil and point != nil do
475
- point_select = H.to_map(S.getprop(point, "select"))
476
- point_action = S.getprop(point_select, "$action")
487
+ terminal_param? = fn p ->
488
+ parts = S.getprop(p, "parts")
477
489
 
478
- if req_action != point_action do
479
- Context.make_error(ctx, "point_action_invalid",
480
- "Operation \"" <> opname <> "\" action \"" <> S.stringify(req_action) <> "\" is not valid.")
490
+ if S.islist(parts) and S.size(parts) > 0 do
491
+ last = S.getelem(parts, S.size(parts) - 1)
492
+ is_binary(last) and String.starts_with?(last, "{")
493
+ else
494
+ false
481
495
  end
482
496
  end
497
+
498
+ Enum.reduce(0..(npoints - 1), S.getelem(points, 0), fn i, best ->
499
+ cand = S.getelem(points, i)
500
+ ct = terminal_param?.(cand)
501
+ bt = terminal_param?.(best)
502
+
503
+ cond do
504
+ ct != bt -> if ct, do: cand, else: best
505
+ parts_len.(cand) < parts_len.(best) -> cand
506
+ true -> best
507
+ end
508
+ end)
509
+ end
510
+
511
+ unmatched_action =
512
+ if not matched? and reqselector != nil,
513
+ do: S.getprop(reqselector, "$action"),
514
+ else: nil
515
+
516
+ err =
517
+ cond do
518
+ # A request naming an action reaches the fallback only because
519
+ # that action's own point failed its exist test, so it is
520
+ # unbuildable whatever we pick. Refuse it BEFORE the guard
521
+ # below, which compares the chosen point's $action and would
522
+ # wave the request through whenever the fallback lands on the
523
+ # action point itself.
524
+ unmatched_action != nil ->
525
+ Context.make_error(ctx, "point_action_invalid",
526
+ "Operation \"" <> opname <> "\" action \"" <> S.stringify(unmatched_action) <> "\" is not valid.")
527
+
528
+ reqselector != nil ->
529
+ req_action = S.getprop(reqselector, "$action")
530
+
531
+ if req_action != nil and point != nil do
532
+ point_select = H.to_map(S.getprop(point, "select"))
533
+ point_action = S.getprop(point_select, "$action")
534
+
535
+ if req_action != point_action do
536
+ Context.make_error(ctx, "point_action_invalid",
537
+ "Operation \"" <> opname <> "\" action \"" <> S.stringify(req_action) <> "\" is not valid.")
538
+ end
539
+ end
540
+
541
+ true ->
542
+ nil
483
543
  end
484
544
 
485
545
  if err != nil do
@@ -4,6 +4,7 @@ import (
4
4
  "fmt"
5
5
  "math/rand"
6
6
  "regexp"
7
+ "strings"
7
8
 
8
9
  vs "github.com/voxgig/struct"
9
10
 
@@ -310,9 +311,40 @@ func (f *TestFeature) makeNetsim(net map[string]any, inner core.FetcherFunc) cor
310
311
  func (f *TestFeature) buildArgs(ctx *core.Context, op *core.Operation, args map[string]any) any {
311
312
  opname := op.Name
312
313
 
313
- // Get last point from config.
314
+ // Pick the entity's own endpoint from config, not a cross-reference from
315
+ // another resource that also returns it — the same rule makePoint falls
316
+ // back to, so the seed-data query is built from the endpoint the request
317
+ // will actually be sent to: a terminal `{id}` marks a record route, and
318
+ // failing that the shallower path wins.
314
319
  points := vs.GetPath([]any{"entity", ctx.Entity.GetName(), "op", opname, "points"}, ctx.Config)
315
- point := vs.GetElem(points, -1)
320
+ point := vs.GetElem(points, 0)
321
+ if plist, ok := points.([]any); ok {
322
+ partsLen := func(p any) int {
323
+ if parts, ok := vs.GetProp(p, "parts").([]any); ok {
324
+ return len(parts)
325
+ }
326
+ return 0
327
+ }
328
+ terminalParam := func(p any) bool {
329
+ parts, ok := vs.GetProp(p, "parts").([]any)
330
+ if !ok || 0 == len(parts) {
331
+ return false
332
+ }
333
+ last, _ := parts[len(parts)-1].(string)
334
+ return strings.HasPrefix(last, "{")
335
+ }
336
+ for _, cand := range plist {
337
+ candTerm := terminalParam(cand)
338
+ bestTerm := terminalParam(point)
339
+ if candTerm != bestTerm {
340
+ if candTerm {
341
+ point = cand
342
+ }
343
+ } else if partsLen(cand) < partsLen(point) {
344
+ point = cand
345
+ }
346
+ }
347
+ }
316
348
 
317
349
  // Get required params.
318
350
  paramsPath := vs.GetPath([]any{"args", "params"}, point)
@@ -8,6 +8,47 @@ import (
8
8
  "GOMODULE/core"
9
9
  )
10
10
 
11
+ // How many path segments a point has.
12
+ func partsLen(point map[string]any) int {
13
+ if parts, ok := vs.GetProp(point, "parts").([]any); ok {
14
+ return len(parts)
15
+ }
16
+ return 0
17
+ }
18
+
19
+ // Does this point's path end in a parameter? A record route ends in the
20
+ // record's identifier (`/boards/{id}`); a cross-reference that also returns
21
+ // the entity ends in the relationship's name (`/posts/{id}/author`).
22
+ func terminalParam(point map[string]any) bool {
23
+ parts, ok := vs.GetProp(point, "parts").([]any)
24
+ if !ok || 0 == len(parts) {
25
+ return false
26
+ }
27
+ last, _ := parts[len(parts)-1].(string)
28
+ return strings.HasPrefix(last, "{")
29
+ }
30
+
31
+ // The entity's OWN route among an op's points: a terminal parameter first,
32
+ // then the fewest path segments. Ties keep the earlier point, so the model's
33
+ // sorted-key order decides. The same rule runs at generation time, in
34
+ // helpers/opShape.ts — a template ships standalone, so both sides must move
35
+ // together.
36
+ func ownPoint(points []map[string]any) map[string]any {
37
+ best := points[0]
38
+ for _, cand := range points {
39
+ candTerm := terminalParam(cand)
40
+ bestTerm := terminalParam(best)
41
+ if candTerm != bestTerm {
42
+ if candTerm {
43
+ best = cand
44
+ }
45
+ } else if partsLen(cand) < partsLen(best) {
46
+ best = cand
47
+ }
48
+ }
49
+ return best
50
+ }
51
+
11
52
  func makePointUtil(ctx *core.Context) (map[string]any, error) {
12
53
  if ctx.Out["point"] != nil {
13
54
  // A PrePoint feature hook (e.g. rbac) may short-circuit the
@@ -52,9 +93,10 @@ func makePointUtil(ctx *core.Context) (map[string]any, error) {
52
93
  }
53
94
 
54
95
  var point map[string]any
96
+ matched := false
55
97
  for i := 0; i < len(op.Points); i++ {
56
- point = op.Points[i]
57
- selectDef := core.ToMapAny(vs.GetProp(point, "select"))
98
+ cand := op.Points[i]
99
+ selectDef := core.ToMapAny(vs.GetProp(cand, "select"))
58
100
  found := true
59
101
 
60
102
  if selector != nil && selectDef != nil {
@@ -82,10 +124,32 @@ func makePointUtil(ctx *core.Context) (map[string]any, error) {
82
124
  }
83
125
 
84
126
  if found {
127
+ point = cand
128
+ matched = true
85
129
  break
86
130
  }
87
131
  }
88
132
 
133
+ // select.exist can list more than the params needed to pick a point,
134
+ // so nothing matches — fall back to the entity's own route rather
135
+ // than whichever point came last.
136
+ if !matched {
137
+ // A request naming an action reaches here only because that
138
+ // action's own point failed its exist test, so it is unbuildable
139
+ // whatever we pick. Refuse it BEFORE choosing a fallback: the
140
+ // guard below compares the chosen point's $action and would wave
141
+ // the request through whenever the fallback lands on the action
142
+ // point itself.
143
+ if reqselector != nil && vs.GetProp(reqselector, "$action") != nil {
144
+ return nil, ctx.MakeError("point_action_invalid",
145
+ "Operation \""+op.Name+
146
+ "\" action \""+vs.Stringify(vs.GetProp(reqselector, "$action"))+
147
+ "\" is not valid.")
148
+ }
149
+
150
+ point = ownPoint(op.Points)
151
+ }
152
+
89
153
  if reqselector != nil {
90
154
  reqAction := vs.GetProp(reqselector, "$action")
91
155
  if reqAction != nil && point != nil {
@@ -26,6 +26,29 @@ public class TestFeature extends BaseFeature {
26
26
  private Map<String, Object> options;
27
27
  private int netcalls = 0;
28
28
 
29
+ // How many path segments a point has, and whether its path ends in a
30
+ // parameter — the rule makePoint uses to tell the entity's own route from a
31
+ // cross-reference that also returns it.
32
+ @SuppressWarnings("unchecked")
33
+ private static int pointPartsLen(Object point) {
34
+ Object parts = Struct.getprop(point, "parts");
35
+ return parts instanceof List ? ((List<Object>) parts).size() : 0;
36
+ }
37
+
38
+ @SuppressWarnings("unchecked")
39
+ private static boolean pointTerminalParam(Object point) {
40
+ Object parts = Struct.getprop(point, "parts");
41
+ if (!(parts instanceof List)) {
42
+ return false;
43
+ }
44
+ List<Object> list = (List<Object>) parts;
45
+ if (list.isEmpty()) {
46
+ return false;
47
+ }
48
+ Object last = list.get(list.size() - 1);
49
+ return last instanceof String && ((String) last).startsWith("{");
50
+ }
51
+
29
52
  public TestFeature() {
30
53
  super("test", "0.0.1", true);
31
54
  }
@@ -289,11 +312,28 @@ public class TestFeature extends BaseFeature {
289
312
  private Object buildArgs(Context ctx, Operation op, Map<String, Object> args) {
290
313
  String opname = op.name;
291
314
 
292
- // Get last point from config.
315
+ // Pick the entity's own endpoint from config, not a cross-reference from
316
+ // another resource that also returns it — the same rule makePoint falls
317
+ // back to, so the seed-data query is built from the endpoint the request
318
+ // will actually be sent to: a terminal `{id}` marks a record route, and
319
+ // failing that the shallower path wins.
293
320
  Object points = Struct.getpath(ctx.config,
294
321
  List.of("entity", ctx.entity == null ? "" : ctx.entity.getName(),
295
322
  "op", opname, "points"));
296
- Object point = Struct.getelem(points, -1);
323
+ Object point = Struct.getelem(points, 0);
324
+ if (points instanceof List) {
325
+ for (Object cand : (List<Object>) points) {
326
+ boolean candTerm = pointTerminalParam(cand);
327
+ boolean bestTerm = pointTerminalParam(point);
328
+ if (candTerm != bestTerm) {
329
+ if (candTerm) {
330
+ point = cand;
331
+ }
332
+ } else if (pointPartsLen(cand) < pointPartsLen(point)) {
333
+ point = cand;
334
+ }
335
+ }
336
+ }
297
337
 
298
338
  // Get required params.
299
339
  Object paramsPath = Struct.getpath(point, List.of("args", "params"));
@@ -14,6 +14,31 @@ final class MakePoint {
14
14
 
15
15
  private MakePoint() {}
16
16
 
17
+ // How many path segments a point has.
18
+ private static int partsLen(Map<String, Object> point) {
19
+ Object parts = Struct.getprop(point, "parts");
20
+ return parts instanceof List ? ((List<Object>) parts).size() : 0;
21
+ }
22
+
23
+ // Does this point's path end in a parameter? A record route ends in the
24
+ // record's identifier (/boards/{id}); a cross-reference that also returns
25
+ // the entity ends in the relationship's name (/posts/{id}/author). That,
26
+ // then fewest segments, is what tells the entity's own route from a
27
+ // cross-reference. The same rule runs at generation time, in
28
+ // helpers/opShape.ts — both sides must move together.
29
+ private static boolean terminalParam(Map<String, Object> point) {
30
+ Object parts = Struct.getprop(point, "parts");
31
+ if (!(parts instanceof List)) {
32
+ return false;
33
+ }
34
+ List<Object> list = (List<Object>) parts;
35
+ if (list.isEmpty()) {
36
+ return false;
37
+ }
38
+ Object last = list.get(list.size() - 1);
39
+ return last instanceof String && ((String) last).startsWith("{");
40
+ }
41
+
17
42
  static Map<String, Object> makePoint(Context ctx) {
18
43
  Object outPoint = ctx.out.get("point");
19
44
  if (outPoint != null) {
@@ -62,10 +87,11 @@ final class MakePoint {
62
87
  }
63
88
 
64
89
  Map<String, Object> point = null;
90
+ boolean matched = false;
65
91
  for (int i = 0; i < op.points.size(); i++) {
66
- point = op.points.get(i);
92
+ Map<String, Object> cand = op.points.get(i);
67
93
  Map<String, Object> selectDef =
68
- Helpers.toMapAny(Struct.getprop(point, "select"));
94
+ Helpers.toMapAny(Struct.getprop(cand, "select"));
69
95
  boolean found = true;
70
96
 
71
97
  if (selector != null && selectDef != null) {
@@ -92,10 +118,43 @@ final class MakePoint {
92
118
  }
93
119
 
94
120
  if (found) {
121
+ point = cand;
122
+ matched = true;
95
123
  break;
96
124
  }
97
125
  }
98
126
 
127
+ // select.exist can list more than the params needed to pick a point, so
128
+ // nothing matches — fall back to the entity's own route rather than
129
+ // whichever point came last.
130
+ if (!matched) {
131
+ // A request naming an action reaches here only because that action's
132
+ // own point failed its exist test, so it is unbuildable whatever we
133
+ // pick. Refuse it BEFORE choosing a fallback: the guard below
134
+ // compares the chosen point's $action and would wave the request
135
+ // through whenever the fallback lands on the action point itself.
136
+ Object unmatchedAction =
137
+ reqselector != null ? Struct.getprop(reqselector, "$action", null) : null;
138
+ if (unmatchedAction != null) {
139
+ throw ctx.makeError("point_action_invalid",
140
+ "Operation \"" + op.name
141
+ + "\" action \"" + Struct.stringify(unmatchedAction) + "\" is not valid.");
142
+ }
143
+
144
+ point = op.points.get(0);
145
+ for (Map<String, Object> cand : op.points) {
146
+ boolean candTerm = terminalParam(cand);
147
+ boolean bestTerm = terminalParam(point);
148
+ if (candTerm != bestTerm) {
149
+ if (candTerm) {
150
+ point = cand;
151
+ }
152
+ } else if (partsLen(cand) < partsLen(point)) {
153
+ point = cand;
154
+ }
155
+ }
156
+ }
157
+
99
158
  if (reqselector != null) {
100
159
  Object reqAction = Struct.getprop(reqselector, "$action", null);
101
160
  if (reqAction != null && point != null) {
@@ -251,9 +251,28 @@ class TestFeature extends BaseFeature {
251
251
  const isempty = struct.isempty
252
252
 
253
253
  const opname = getprop(op, 'name')
254
- const point =
255
- getelem(getpath(ctx.config, [
256
- 'entity', getprop(ctx.entity, 'name'), 'op', opname, 'points']), -1)
254
+ const points = getpath(ctx.config, [
255
+ 'entity', getprop(ctx.entity, 'name'), 'op', opname, 'points']) || []
256
+
257
+ // Pick the entity's own endpoint, not a cross-reference from another
258
+ // resource that also returns it — the same rule makePoint falls back to,
259
+ // so the seed-data query is built from the endpoint the request will
260
+ // actually be sent to: a terminal `{id}` marks a record route, and
261
+ // failing that the shallower path wins.
262
+ const isterm = (pt) => {
263
+ const parts = pt.parts
264
+ const last = 0 < parts.length ? parts[parts.length - 1] : ''
265
+ return 'string' === typeof last && 0 === last.indexOf('{')
266
+ }
267
+
268
+ let point = getelem(points, 0)
269
+ for (let i = 1; i < points.length; i++) {
270
+ const cand = getelem(points, i)
271
+ if (isterm(cand) !== isterm(point) ? isterm(cand) :
272
+ cand.parts.length < point.parts.length) {
273
+ point = cand
274
+ }
275
+ }
257
276
 
258
277
  const reqd = transform(
259
278
  select(getpath(point, ['args', 'params']), { reqd: true }),
@@ -1,4 +1,50 @@
1
1
 
2
+ // The entity's OWN route among an op's points, as opposed to a
3
+ // CROSS-REFERENCE: another resource's route that happens to return this
4
+ // entity (`/notifications/{id}/board` for a board). Both are plain GETs
5
+ // with no `$action`, so the path itself is all there is to go on.
6
+ //
7
+ // Two signals, in order:
8
+ //
9
+ // 1. A record route ends in the record's identifier (`/boards/{id}`,
10
+ // `/accounts/{account_id}/users/{id}`); a cross-reference ends in the
11
+ // relationship's name (`/posts/{id}/author`). A terminal parameter is
12
+ // therefore the stronger signal, and unlike depth it survives an entity
13
+ // nested more deeply than the route pointing at it.
14
+ // 2. Failing that, fewest path segments — the shallower route is the one
15
+ // the entity is named for.
16
+ //
17
+ // Ties keep the earlier point, so the sorted-key order of the model decides
18
+ // and the output stays byte-stable.
19
+ //
20
+ // The same rule runs at GENERATION time, in `ownPoint` in
21
+ // helpers/opShape.ts. It cannot be shared with this file — a template ships
22
+ // standalone, outside that package — so it is written twice on purpose, and
23
+ // both sides must move together.
24
+ function terminalParam(point) {
25
+ const parts = point.parts
26
+ const last = 0 < parts.length ? parts[parts.length - 1] : ''
27
+ return 'string' === typeof last && 0 === last.indexOf('{')
28
+ }
29
+
30
+
31
+ function ownPoint(points) {
32
+ let best = points[0]
33
+
34
+ for (const cand of points) {
35
+ const candterm = terminalParam(cand)
36
+ const bestterm = terminalParam(best)
37
+
38
+ if (candterm !== bestterm ? candterm :
39
+ cand.parts.length < best.parts.length) {
40
+ best = cand
41
+ }
42
+ }
43
+
44
+ return best
45
+ }
46
+
47
+
2
48
  function makePoint(ctx) {
3
49
  if (ctx.out.point) {
4
50
  return ctx.point = ctx.out.point
@@ -28,9 +74,10 @@ function makePoint(ctx) {
28
74
  const selector = getprop(ctx, op.input)
29
75
 
30
76
  let point
77
+ let matched = false
31
78
  for (let i = 0; i < op.points.length; i++) {
32
- point = op.points[i]
33
- const select = point.select
79
+ const cand = op.points[i]
80
+ const select = cand.select
34
81
  let found = true
35
82
 
36
83
  if (selector && select.exist) {
@@ -53,10 +100,33 @@ function makePoint(ctx) {
53
100
  }
54
101
 
55
102
  if (found) {
103
+ point = cand
104
+ matched = true
56
105
  break
57
106
  }
58
107
  }
59
108
 
109
+ // select.exist can list more than the params needed to pick a point (for
110
+ // /boards/{id} it is Trello's 17 optional query-includes), so a plain
111
+ // {id} call matches NOTHING. Fall back to the entity's OWN route rather
112
+ // than whichever point came last.
113
+ if (!matched) {
114
+ // A request naming an action reaches here only because that action's
115
+ // own point failed its exist test, so the request is unbuildable
116
+ // whatever we pick. Refuse it BEFORE choosing a fallback: the guard
117
+ // below compares the chosen point's $action, and would wave the
118
+ // request through whenever the action point is itself the one the
119
+ // fallback selects (a short action route folded into an entity whose
120
+ // canonical route is longer). An unbuildable action request is an
121
+ // error, never a silent request to some other endpoint.
122
+ if (null != reqselector.$action) {
123
+ return ctx.error('point_action_invalid', 'Operation "' + op.name +
124
+ '" action "' + reqselector.$action + '" is not valid.')
125
+ }
126
+
127
+ point = ownPoint(op.points)
128
+ }
129
+
60
130
  if (
61
131
  null != reqselector.$action &&
62
132
  null != point &&
@@ -262,12 +262,41 @@ class TestFeature : BaseFeature("test", "0.0.1", true) {
262
262
  private fun buildArgs(ctx: Context, op: Operation, args: MutableMap<String, Any?>?): Any? {
263
263
  val opname = op.name
264
264
 
265
- // Get last point from config.
265
+ // Pick the entity's own endpoint from config, not a cross-reference from
266
+ // another resource that also returns it — the same rule makePoint falls
267
+ // back to, so the seed-data query is built from the endpoint the request
268
+ // will actually be sent to: a terminal `{id}` marks a record route, and
269
+ // failing that the shallower path wins.
266
270
  val points = Struct.getpath(
267
271
  ctx.config,
268
272
  listOf("entity", if (ctx.entity == null) "" else ctx.entity!!.name, "op", opname, "points"),
269
273
  )
270
- val point = Struct.getelem(points, -1)
274
+ var point = Struct.getelem(points, 0)
275
+ if (points is List<*>) {
276
+ fun pointPartsLen(p: Any?): Int {
277
+ val parts = Struct.getprop(p, "parts")
278
+ return if (parts is List<*>) parts.size else 0
279
+ }
280
+ fun pointTerminalParam(p: Any?): Boolean {
281
+ val parts = Struct.getprop(p, "parts")
282
+ if (parts !is List<*> || parts.isEmpty()) {
283
+ return false
284
+ }
285
+ val last = parts[parts.size - 1]
286
+ return last is String && last.startsWith("{")
287
+ }
288
+ for (cand in points) {
289
+ val candTerm = pointTerminalParam(cand)
290
+ val bestTerm = pointTerminalParam(point)
291
+ if (candTerm != bestTerm) {
292
+ if (candTerm) {
293
+ point = cand
294
+ }
295
+ } else if (pointPartsLen(cand) < pointPartsLen(point)) {
296
+ point = cand
297
+ }
298
+ }
299
+ }
271
300
 
272
301
  // Get required params.
273
302
  val paramsPath = Struct.getpath(point, listOf("args", "params"))