@voxgig/sdkgen 3.5.0 → 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
@@ -4,6 +4,27 @@ import KOTLINPACKAGE.core.Context
4
4
  import KOTLINPACKAGE.core.Helpers
5
5
  import KOTLINPACKAGE.utility.struct.Struct
6
6
 
7
+ // How many path segments a point has.
8
+ private fun partsLen(point: Map<String, Any?>?): Int {
9
+ val parts = Struct.getprop(point, "parts")
10
+ return if (parts is List<*>) parts.size else 0
11
+ }
12
+
13
+ // Does this point's path end in a parameter? A record route ends in the
14
+ // record's identifier (/boards/{id}); a cross-reference that also returns the
15
+ // entity ends in the relationship's name (/posts/{id}/author). That, then
16
+ // fewest segments, is what tells the entity's own route from a
17
+ // cross-reference. The same rule runs at generation time, in
18
+ // helpers/opShape.ts — both sides must move together.
19
+ private fun terminalParam(point: Map<String, Any?>?): Boolean {
20
+ val parts = Struct.getprop(point, "parts")
21
+ if (parts !is List<*> || parts.isEmpty()) {
22
+ return false
23
+ }
24
+ val last = parts[parts.size - 1]
25
+ return last is String && last.startsWith("{")
26
+ }
27
+
7
28
  @Suppress("UNCHECKED_CAST")
8
29
  fun makePoint(ctx: Context): Map<String, Any?> {
9
30
  val outPoint = ctx.out["point"]
@@ -55,9 +76,10 @@ fun makePoint(ctx: Context): Map<String, Any?> {
55
76
  }
56
77
 
57
78
  var point: MutableMap<String, Any?>? = null
79
+ var matched = false
58
80
  for (i in op.points.indices) {
59
- point = op.points[i]
60
- val selectDef = Helpers.toMapAny(Struct.getprop(point, "select"))
81
+ val cand = op.points[i]
82
+ val selectDef = Helpers.toMapAny(Struct.getprop(cand, "select"))
61
83
  var found = true
62
84
 
63
85
  if (selectDef != null) {
@@ -84,10 +106,44 @@ fun makePoint(ctx: Context): Map<String, Any?> {
84
106
  }
85
107
 
86
108
  if (found) {
109
+ point = cand
110
+ matched = true
87
111
  break
88
112
  }
89
113
  }
90
114
 
115
+ // select.exist can list more than the params needed to pick a point, so
116
+ // nothing matches — fall back to the entity's own route rather than
117
+ // whichever point came last.
118
+ if (!matched) {
119
+ // A request naming an action reaches here only because that action's
120
+ // own point failed its exist test, so it is unbuildable whatever we
121
+ // pick. Refuse it BEFORE choosing a fallback: the guard below compares
122
+ // the chosen point's $action and would wave the request through
123
+ // whenever the fallback lands on the action point itself.
124
+ val unmatchedAction = Struct.getprop(reqselector, "\$action", null)
125
+ if (unmatchedAction != null) {
126
+ throw ctx.makeError(
127
+ "point_action_invalid",
128
+ "Operation \"" + op.name +
129
+ "\" action \"" + Struct.stringify(unmatchedAction) + "\" is not valid.",
130
+ )
131
+ }
132
+
133
+ point = op.points[0]
134
+ for (cand in op.points) {
135
+ val candTerm = terminalParam(cand)
136
+ val bestTerm = terminalParam(point)
137
+ if (candTerm != bestTerm) {
138
+ if (candTerm) {
139
+ point = cand
140
+ }
141
+ } else if (partsLen(cand) < partsLen(point)) {
142
+ point = cand
143
+ }
144
+ }
145
+ }
146
+
91
147
  val reqAction = Struct.getprop(reqselector, "\$action", null)
92
148
  if (reqAction != null && point != null) {
93
149
  val pointSelect = Helpers.toMapAny(Struct.getprop(point, "select"))
@@ -597,6 +597,20 @@ def makeResult (ctx : Value) : SIO Value := do
597
597
 
598
598
  /-- Select the endpoint for this operation: the single point, else the first
599
599
  whose `select.exist` keys are all present and whose `$action` agrees. -/
600
+ /-- How many path segments a point has, and whether its path ends in a
601
+ parameter. A record route ends in the record's identifier (/boards/{id});
602
+ a cross-reference that also returns the entity ends in the relationship's
603
+ name (/posts/{id}/author). That, then fewest segments, is what tells the
604
+ entity's own route from a cross-reference. The same rule runs at
605
+ generation time, in helpers/opShape.ts — both sides must move together. -/
606
+ def pointShape (pt : Value) : SIO (Nat × Bool) := do
607
+ match (← gp pt "parts") with
608
+ | .list i =>
609
+ let items ← listItems i
610
+ if items.size == 0 then pure (0, false)
611
+ else pure (items.size, (vs items[items.size - 1]!).startsWith "{")
612
+ | _ => pure (0, false)
613
+
600
614
  def makePoint (ctx : Value) : SIO Value := do
601
615
  let op ← gp ctx "op"
602
616
  let matchV ← gp ctx "reqmatch"
@@ -622,6 +636,20 @@ def makePoint (ctx : Value) : SIO Value := do
622
636
  | _ => pure ()
623
637
  if ok && ((← gp sel "$action") == (← gp matchV "$action")) then
624
638
  chosen := pt
639
+ -- select.exist can list more than the params needed to pick a point (for
640
+ -- /boards/{id} it is Trello's 17 optional query-includes), so a plain
641
+ -- {id} call matches NOTHING and this loop chose no point at all. Fall
642
+ -- back to the entity's own route rather than sending nowhere.
643
+ if isNov chosen then
644
+ let mut best := pts[0]!
645
+ for cand in pts do
646
+ let (candLen, candTerm) ← pointShape cand
647
+ let (bestLen, bestTerm) ← pointShape best
648
+ if candTerm && !bestTerm then
649
+ best := cand
650
+ else if candTerm == bestTerm && candLen < bestLen then
651
+ best := cand
652
+ chosen := best
625
653
  if !(isNov chosen) then sp ctx "point" chosen
626
654
  pure chosen
627
655
 
@@ -3,6 +3,32 @@
3
3
  local vs = require("utility.struct.struct")
4
4
  local helpers = require("core.helpers")
5
5
 
6
+ -- How many path segments a point has.
7
+ local function parts_len(point)
8
+ local parts = vs.getprop(point, "parts")
9
+ if type(parts) == "table" then
10
+ return #parts
11
+ end
12
+ return 0
13
+ end
14
+
15
+
16
+ -- Does this point's path end in a parameter? A record route ends in the
17
+ -- record's identifier (/boards/{id}); a cross-reference that also returns
18
+ -- the entity ends in the relationship's name (/posts/{id}/author). That,
19
+ -- then fewest segments, is what tells the entity's own route from a
20
+ -- cross-reference. The same rule runs at generation time, in
21
+ -- helpers/opShape.ts — both sides must move together.
22
+ local function terminal_param(point)
23
+ local parts = vs.getprop(point, "parts")
24
+ if type(parts) ~= "table" or #parts == 0 then
25
+ return false
26
+ end
27
+ local last = parts[#parts]
28
+ return type(last) == "string" and last:sub(1, 1) == "{"
29
+ end
30
+
31
+
6
32
  local function make_point_util(ctx)
7
33
  if ctx.out["point"] ~= nil then
8
34
  local preset = ctx.out["point"]
@@ -44,9 +70,10 @@ local function make_point_util(ctx)
44
70
  end
45
71
 
46
72
  local point = nil
73
+ local matched = false
47
74
  for i = 1, #op.points do
48
- point = op.points[i]
49
- local select_def = helpers.to_map(vs.getprop(point, "select"))
75
+ local cand = op.points[i]
76
+ local select_def = helpers.to_map(vs.getprop(cand, "select"))
50
77
  local found = true
51
78
 
52
79
  if selector ~= nil and select_def ~= nil then
@@ -73,10 +100,46 @@ local function make_point_util(ctx)
73
100
  end
74
101
 
75
102
  if found then
103
+ point = cand
104
+ matched = true
76
105
  break
77
106
  end
78
107
  end
79
108
 
109
+ -- select.exist can list more than the params needed to pick a point, so
110
+ -- nothing matches — fall back to the entity's own route rather than
111
+ -- whichever point came last.
112
+ if not matched then
113
+ -- A request naming an action reaches here only because that action's
114
+ -- own point failed its exist test, so it is unbuildable whatever we
115
+ -- pick. Refuse it BEFORE choosing a fallback: the guard below compares
116
+ -- the chosen point's $action and would wave the request through
117
+ -- whenever the fallback lands on the action point itself.
118
+ local unmatched_action = nil
119
+ if reqselector ~= nil then
120
+ unmatched_action = vs.getprop(reqselector, "$action")
121
+ end
122
+ if unmatched_action ~= nil then
123
+ return nil, ctx:make_error("point_action_invalid",
124
+ 'Operation "' .. op.name ..
125
+ '" action "' .. vs.stringify(unmatched_action) .. '" is not valid.')
126
+ end
127
+
128
+ point = op.points[1]
129
+ for i = 1, #op.points do
130
+ local cand = op.points[i]
131
+ local cand_term = terminal_param(cand)
132
+ local best_term = terminal_param(point)
133
+ if cand_term ~= best_term then
134
+ if cand_term then
135
+ point = cand
136
+ end
137
+ elseif parts_len(cand) < parts_len(point) then
138
+ point = cand
139
+ end
140
+ end
141
+ end
142
+
80
143
  if reqselector ~= nil then
81
144
  local req_action = vs.getprop(reqselector, "$action")
82
145
  if req_action ~= nil and point ~= nil then
@@ -636,12 +636,12 @@ let make_point_util (ctx : ctx) : (value * sdk_error option) =
636
636
  if op.op_input = "data" then ctx.c_reqdata, ctx.c_data
637
637
  else ctx.c_reqmatch, ctx.c_match in
638
638
  let chosen = ref Noval in
639
+ let matched = ref false in
639
640
  let n = List.length points in
640
641
  let arr = Array.of_list points in
641
642
  let i = ref 0 and stop = ref false in
642
643
  while not !stop && !i < n do
643
644
  let point = arr.(!i) in
644
- chosen := point;
645
645
  let select_def = to_map (getp point "select") in
646
646
  let found = ref true in
647
647
  (match select_def with
@@ -661,8 +661,46 @@ let make_point_util (ctx : ctx) : (value * sdk_error option) =
661
661
  let select_action = getp select_def "$action" in
662
662
  if req_action <> select_action then found := false
663
663
  end;
664
- if !found then stop := true else incr i
664
+ if !found then begin chosen := point; matched := true; stop := true end
665
+ else incr i
665
666
  done;
667
+ (* select.exist can list more than the params needed to pick a point
668
+ (for /boards/{id} it is Trello's 17 optional query-includes), so a
669
+ plain {id} call matches NOTHING. Fall back to the entity's own
670
+ route rather than whichever point came last. *)
671
+ if not !matched then begin
672
+ (* A request naming an action reaches here only because that
673
+ action's own point failed its exist test, so it is unbuildable
674
+ whatever we pick. Refuse it BEFORE choosing a fallback: the guard
675
+ below compares the chosen point's $action and would wave the
676
+ request through whenever the fallback lands on the action point
677
+ itself. *)
678
+ let unmatched_action = getp reqselector "$action" in
679
+ if not (is_noval unmatched_action) then
680
+ raise (Sdk_error_exc (ctx_make_error ctx "point_action_invalid"
681
+ ("Operation \"" ^ op.op_name ^ "\" action \"" ^ (stringify unmatched_action) ^ "\" is not valid.")));
682
+ (* A terminal parameter marks a record route (/boards/{id}); a
683
+ cross-reference ends in the relationship's name
684
+ (/posts/{id}/author). Failing that, the shallower path wins. The
685
+ same rule runs at generation time, in helpers/opShape.ts — both
686
+ sides must move together. *)
687
+ let parts_len p =
688
+ match getp p "parts" with List r -> List.length !r | _ -> 0 in
689
+ let terminal_param p =
690
+ match getp p "parts" with
691
+ | List r ->
692
+ (match List.rev !r with
693
+ | last :: _ ->
694
+ let s = vstring last in
695
+ String.length s > 0 && s.[0] = '{'
696
+ | [] -> false)
697
+ | _ -> false in
698
+ chosen := arr.(0);
699
+ Array.iter (fun cand ->
700
+ let ct = terminal_param cand and bt = terminal_param !chosen in
701
+ if ct <> bt then (if ct then chosen := cand)
702
+ else if parts_len cand < parts_len !chosen then chosen := cand) arr
703
+ end;
666
704
  let req_action = getp reqselector "$action" in
667
705
  if not (is_noval req_action) && not (is_noval !chosen) then begin
668
706
  let point_select = to_map (getp !chosen "select") in
@@ -17,6 +17,27 @@ package ProjectNameUtilities;
17
17
 
18
18
  our %REGISTRY;
19
19
 
20
+ # How many path segments a point has.
21
+ sub _parts_len {
22
+ my ($point) = @_;
23
+ my $parts = ProjectNameHelpers::gp($point, 'parts');
24
+ return Voxgig::Struct::islist($parts) ? scalar(@$parts) : 0;
25
+ }
26
+
27
+ # Does this point's path end in a parameter? A record route ends in the
28
+ # record's identifier (/boards/{id}); a cross-reference that also returns the
29
+ # entity ends in the relationship's name (/posts/{id}/author). That, then
30
+ # fewest segments, is what tells the entity's own route from a
31
+ # cross-reference. The same rule runs at generation time, in
32
+ # helpers/opShape.ts — both sides must move together.
33
+ sub _terminal_param {
34
+ my ($point) = @_;
35
+ my $parts = ProjectNameHelpers::gp($point, 'parts');
36
+ return 0 unless Voxgig::Struct::islist($parts) && 0 < scalar(@$parts);
37
+ my $last = $parts->[-1];
38
+ return (defined $last && !ref $last && $last =~ /^\{/) ? 1 : 0;
39
+ }
40
+
20
41
  $REGISTRY{make_point} = sub {
21
42
  my ($ctx) = @_;
22
43
 
@@ -54,8 +75,8 @@ $REGISTRY{make_point} = sub {
54
75
  my $selector = ('data' eq $op->{input}) ? $ctx->{data} : $ctx->{match};
55
76
 
56
77
  my $point;
78
+ my $matched = 0;
57
79
  for my $p (@{ $op->{points} }) {
58
- $point = $p;
59
80
  my $select_def = ProjectNameHelpers::to_map(ProjectNameHelpers::gp($p, 'select'));
60
81
  my $found = 1;
61
82
 
@@ -79,7 +100,39 @@ $REGISTRY{make_point} = sub {
79
100
  $found = 0 unless ProjectNameHelpers::eqv($req_action, $select_action);
80
101
  }
81
102
 
82
- last if $found;
103
+ if ($found) {
104
+ $point = $p;
105
+ $matched = 1;
106
+ last;
107
+ }
108
+ }
109
+
110
+ # select.exist can list more than the params needed to pick a point, so
111
+ # nothing matches — fall back to the entity's own route rather than
112
+ # whichever point came last.
113
+ unless ($matched) {
114
+ # A request naming an action reaches here only because that action's
115
+ # own point failed its exist test, so it is unbuildable whatever we
116
+ # pick. Refuse it BEFORE choosing a fallback: the guard below compares
117
+ # the chosen point's $action and would wave the request through
118
+ # whenever the fallback lands on the action point itself.
119
+ my $unmatched_action = $reqselector
120
+ ? ProjectNameHelpers::gp($reqselector, '$action') : undef;
121
+ if (defined $unmatched_action) {
122
+ return (undef, $ctx->make_error('point_action_invalid',
123
+ 'Operation "' . $op->{name} . '" action "' .
124
+ Voxgig::Struct::stringify($unmatched_action) . '" is not valid.'));
125
+ }
126
+
127
+ $point = $op->{points}[0];
128
+ for my $p (@{ $op->{points} }) {
129
+ if (_terminal_param($p) != _terminal_param($point)) {
130
+ $point = $p if _terminal_param($p);
131
+ }
132
+ elsif (_parts_len($p) < _parts_len($point)) {
133
+ $point = $p;
134
+ }
135
+ }
83
136
  }
84
137
 
85
138
  if ($reqselector) {
@@ -369,11 +369,40 @@ class ProjectNameTestFeature extends ProjectNameBaseFeature
369
369
  // any missing piece falls back to "no required params".
370
370
  $reqd_names = [];
371
371
  if (is_string($opname) && is_string($entityName) && isset($ctx->config)) {
372
+ // Pick the entity's own endpoint, not a cross-reference from
373
+ // another resource that also returns it — the same rule
374
+ // MakePoint falls back to, so the seed-data query is built from
375
+ // the endpoint the request will actually be sent to: a terminal
376
+ // `{id}` marks a record route, and failing that the shallower
377
+ // path wins.
372
378
  $points = \Voxgig\Struct\Struct::getpath(
373
379
  ['entity', $entityName, 'op', $opname, 'points'],
374
380
  $ctx->config
375
381
  );
376
- $point = \Voxgig\Struct\Struct::getelem($points, -1);
382
+ $point = \Voxgig\Struct\Struct::getelem($points, 0);
383
+ if (is_array($points)) {
384
+ $point_parts_len = function ($p) {
385
+ $parts = \Voxgig\Struct\Struct::getprop($p, 'parts');
386
+ return is_array($parts) ? count($parts) : 0;
387
+ };
388
+ $point_terminal_param = function ($p) {
389
+ $parts = \Voxgig\Struct\Struct::getprop($p, 'parts');
390
+ if (!is_array($parts) || 0 === count($parts)) {
391
+ return false;
392
+ }
393
+ $last = $parts[count($parts) - 1];
394
+ return is_string($last) && 0 === strpos($last, '{');
395
+ };
396
+ foreach ($points as $cand) {
397
+ if ($point_terminal_param($cand) !== $point_terminal_param($point)) {
398
+ if ($point_terminal_param($cand)) {
399
+ $point = $cand;
400
+ }
401
+ } elseif ($point_parts_len($cand) < $point_parts_len($point)) {
402
+ $point = $cand;
403
+ }
404
+ }
405
+ }
377
406
  $params = is_array($point) ? ($point['args']['params'] ?? null) : null;
378
407
  if (is_array($params)) {
379
408
  foreach ($params as $p) {
@@ -42,8 +42,8 @@ class ProjectNameMakePoint
42
42
  $selector = $op->input === 'data' ? $ctx->data : $ctx->match;
43
43
 
44
44
  $point = null;
45
+ $matched = false;
45
46
  foreach ($op->points as $p) {
46
- $point = $p;
47
47
  $select_def = ProjectNameHelpers::to_map(\Voxgig\Struct\Struct::getprop($p, 'select'));
48
48
  $found = true;
49
49
 
@@ -70,10 +70,58 @@ class ProjectNameMakePoint
70
70
  }
71
71
 
72
72
  if ($found) {
73
+ $point = $p;
74
+ $matched = true;
73
75
  break;
74
76
  }
75
77
  }
76
78
 
79
+ // select.exist can list more than the params needed to pick a
80
+ // point, so nothing matches — fall back to the entity's own
81
+ // route rather than the last point.
82
+ if (!$matched) {
83
+ // A request naming an action reaches here only because that
84
+ // action's own point failed its exist test, so it is
85
+ // unbuildable whatever we pick. Refuse it BEFORE choosing a
86
+ // fallback: the guard below compares the chosen point's
87
+ // $action and would wave the request through whenever the
88
+ // fallback lands on the action point itself.
89
+ $unmatched_action = $reqselector
90
+ ? \Voxgig\Struct\Struct::getprop($reqselector, '$action') : null;
91
+ if (null !== $unmatched_action) {
92
+ return [null, $ctx->make_error('point_action_invalid',
93
+ "Operation \"{$op->name}\" action \"" .
94
+ \Voxgig\Struct\Struct::stringify($unmatched_action) .
95
+ "\" is not valid.")];
96
+ }
97
+
98
+ // A terminal parameter marks a record route (/boards/{id});
99
+ // a cross-reference ends in the relationship's name
100
+ // (/posts/{id}/author). Failing that, the shallower wins.
101
+ $parts_len = function ($p) {
102
+ $parts = \Voxgig\Struct\Struct::getprop($p, 'parts');
103
+ return is_array($parts) ? count($parts) : 0;
104
+ };
105
+ $terminal_param = function ($p) {
106
+ $parts = \Voxgig\Struct\Struct::getprop($p, 'parts');
107
+ if (!is_array($parts) || 0 === count($parts)) {
108
+ return false;
109
+ }
110
+ $last = $parts[count($parts) - 1];
111
+ return is_string($last) && 0 === strpos($last, '{');
112
+ };
113
+ $point = $op->points[0];
114
+ foreach ($op->points as $p) {
115
+ if ($terminal_param($p) !== $terminal_param($point)) {
116
+ if ($terminal_param($p)) {
117
+ $point = $p;
118
+ }
119
+ } elseif ($parts_len($p) < $parts_len($point)) {
120
+ $point = $p;
121
+ }
122
+ }
123
+ }
124
+
77
125
  if ($reqselector) {
78
126
  $req_action = \Voxgig\Struct\Struct::getprop($reqselector, '$action');
79
127
  if ($req_action && $point) {
@@ -5,6 +5,38 @@ from projectname_sdk.utility.voxgig_struct import voxgig_struct as vs
5
5
  from projectname_sdk.core.helpers import to_map
6
6
 
7
7
 
8
+ def _parts_len(point):
9
+ parts = vs.getprop(point, "parts")
10
+ return len(parts) if isinstance(parts, list) else 0
11
+
12
+
13
+ def _terminal_param(point):
14
+ # A record route ends in the record's identifier (/boards/{id}); a
15
+ # cross-reference that also returns the entity ends in the
16
+ # relationship's name (/posts/{id}/author).
17
+ parts = vs.getprop(point, "parts")
18
+ if not isinstance(parts, list) or 0 == len(parts):
19
+ return False
20
+ return str(parts[-1]).startswith("{")
21
+
22
+
23
+ def _own_point(points):
24
+ # The entity's OWN route: a terminal parameter first, then the fewest
25
+ # path segments. Ties keep the earlier point, so the model's sorted-key
26
+ # order decides. The same rule runs at generation time, in
27
+ # helpers/opShape.ts — both sides must move together.
28
+ best = points[0]
29
+ for cand in points:
30
+ cand_term = _terminal_param(cand)
31
+ best_term = _terminal_param(best)
32
+ if cand_term != best_term:
33
+ if cand_term:
34
+ best = cand
35
+ elif _parts_len(cand) < _parts_len(best):
36
+ best = cand
37
+ return best
38
+
39
+
8
40
  def make_point_util(ctx):
9
41
  pre = ctx.out.get("point")
10
42
  if pre is not None:
@@ -40,9 +72,10 @@ def make_point_util(ctx):
40
72
  selector = ctx.match
41
73
 
42
74
  point = None
75
+ matched = False
43
76
  for i in range(len(op.points)):
44
- point = op.points[i]
45
- select_def = to_map(vs.getprop(point, "select"))
77
+ cand = op.points[i]
78
+ select_def = to_map(vs.getprop(cand, "select"))
46
79
  found = True
47
80
 
48
81
  if selector is not None and select_def is not None:
@@ -63,8 +96,29 @@ def make_point_util(ctx):
63
96
  found = False
64
97
 
65
98
  if found:
99
+ point = cand
100
+ matched = True
66
101
  break
67
102
 
103
+ # select.exist can list more than the params needed to pick a point,
104
+ # so nothing matches — fall back to the entity's own route rather
105
+ # than whichever point came last.
106
+ if not matched:
107
+ # A request naming an action reaches here only because that
108
+ # action's own point failed its exist test, so it is unbuildable
109
+ # whatever we pick. Refuse it BEFORE choosing a fallback: the
110
+ # guard below compares the chosen point's $action and would wave
111
+ # the request through whenever the fallback lands on the action
112
+ # point itself.
113
+ req_action = vs.getprop(reqselector, "$action") \
114
+ if reqselector is not None else None
115
+ if req_action is not None:
116
+ return None, ctx.make_error("point_action_invalid",
117
+ 'Operation "' + op.name + '" action "' +
118
+ vs.stringify(req_action) + '" is not valid.')
119
+
120
+ point = _own_point(op.points)
121
+
68
122
  if reqselector is not None:
69
123
  req_action = vs.getprop(reqselector, "$action")
70
124
  if req_action is not None and point is not None:
@@ -35,8 +35,8 @@ module ProjectNameUtilities
35
35
  selector = op.input == "data" ? ctx.data : ctx.match
36
36
 
37
37
  point = nil
38
+ matched = false
38
39
  op.points.each do |p|
39
- point = p
40
40
  select_def = ProjectNameHelpers.to_map(VoxgigStruct.getprop(p, "select"))
41
41
  found = true
42
42
 
@@ -60,7 +60,47 @@ module ProjectNameUtilities
60
60
  found = false if req_action != select_action
61
61
  end
62
62
 
63
- break if found
63
+ if found
64
+ point = p
65
+ matched = true
66
+ break
67
+ end
68
+ end
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
+ unless 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
77
+ # compares the chosen point's $action and would wave the request
78
+ # through whenever the fallback lands on the action point itself.
79
+ req_action = reqselector ? VoxgigStruct.getprop(reqselector, "$action") : nil
80
+ if req_action
81
+ return nil, ctx.make_error("point_action_invalid",
82
+ "Operation \"#{op.name}\" action \"#{VoxgigStruct.stringify(req_action)}\" is not valid.")
83
+ end
84
+
85
+ # A terminal parameter marks a record route (/boards/{id}); a
86
+ # cross-reference ends in the relationship's name
87
+ # (/posts/{id}/author). Failing that, the shallower path wins.
88
+ parts_len = ->(p) {
89
+ parts = VoxgigStruct.getprop(p, "parts")
90
+ parts.is_a?(Array) ? parts.length : 0
91
+ }
92
+ terminal_param = ->(p) {
93
+ parts = VoxgigStruct.getprop(p, "parts")
94
+ parts.is_a?(Array) && parts.length > 0 && parts[-1].to_s.start_with?("{")
95
+ }
96
+ point = op.points[0]
97
+ op.points.each do |p|
98
+ if terminal_param.call(p) != terminal_param.call(point)
99
+ point = p if terminal_param.call(p)
100
+ elsif parts_len.call(p) < parts_len.call(point)
101
+ point = p
102
+ end
103
+ end
64
104
  end
65
105
 
66
106
  if reqselector