@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
@@ -14,6 +14,7 @@ import {
14
14
  } from '@voxgig/apidef'
15
15
 
16
16
  import { Tests, Scripts, Workflow, Readme, Docs } from './Extras_seneca-provider'
17
+ import { Gitignore } from './Gitignore_seneca-provider'
17
18
 
18
19
 
19
20
  // The `seneca-provider` target: a Seneca plugin exposing this API's entities
@@ -206,9 +207,10 @@ function parentEntityOf(key: string, names: string[]): string {
206
207
  // published package to the SDK instead.
207
208
  //
208
209
  // Order: the project's `output: repo`, else the Seneca convention.
209
- function providerRepo(model: any, lower: string): { url: string, path: string } {
210
+ function providerRepo(model: any, lower: string, tname: string):
211
+ { url: string, path: string } {
210
212
  const host = model?.main?.[KIT]?.repo?.host || 'github.com'
211
- const declared = model?.main?.[KIT]?.target?.['seneca-provider']?.output?.repo
213
+ const declared = model?.main?.[KIT]?.target?.[tname]?.output?.repo
212
214
  const path = null != declared && '' !== declared ?
213
215
  String(declared) : `senecajs/seneca-${lower}-provider`
214
216
 
@@ -219,8 +221,8 @@ function providerRepo(model: any, lower: string): { url: string, path: string }
219
221
  // The provider's published package name: the project's pin when it has one,
220
222
  // else `@seneca/<name>-provider`. packageName() cannot answer this — its
221
223
  // derivations are all SDK-shaped (`@<origin>/<slug>-sdk`).
222
- function providerPackage(model: any, lower: string): string {
223
- const declared = model?.main?.[KIT]?.target?.['seneca-provider']
224
+ function providerPackage(model: any, lower: string, tname: string): string {
225
+ const declared = model?.main?.[KIT]?.target?.[tname]
224
226
  ?.publish?.registry?.package
225
227
  return null != declared && '' !== declared ?
226
228
  String(declared) : `@seneca/${lower}-provider`
@@ -253,6 +255,8 @@ const Main = cmp(function Main(props: any) {
253
255
  // The SDK is a PUBLISHED dependency, not a path: this package lives in its
254
256
  // own repo. Its name is whatever the ts target publishes under, pin
255
257
  // included, so the two can never disagree.
258
+ // The TypeScript SDK this provider WRAPS — a different target, so it
259
+ // keeps its own name and does not follow this provider's alias.
256
260
  const sdkPkg = packageName(model, 'npm')
257
261
  const sdkVersion = packageVersion(model, 'ts')
258
262
 
@@ -374,7 +378,7 @@ const Main = cmp(function Main(props: any) {
374
378
  'all. Remove the target, or add an entity with CRUD ops.')
375
379
  }
376
380
 
377
- const repo = providerRepo(model, lower)
381
+ const repo = providerRepo(model, lower, target.name)
378
382
 
379
383
  // The companion test server lives in the SDK repo's `app/` and is NOT
380
384
  // published, so the only way to reach it is the local checkout. The path
@@ -440,7 +444,7 @@ const Main = cmp(function Main(props: any) {
440
444
  // The provider's OWN published name. Not derived from the SDK slug the
441
445
  // way a language target's is — a provider is `@seneca/<name>-provider` —
442
446
  // so read the project's pin directly and default to that shape.
443
- pkgName: providerPackage(model, lower),
447
+ pkgName: providerPackage(model, lower, target.name),
444
448
  // Whether this API authenticates at all. Decides if the plugin plumbs a
445
449
  // credential: the SDK's auth stage emits nothing for an auth-inactive
446
450
  // model and strips the authorization header regardless of options, so
@@ -448,14 +452,20 @@ const Main = cmp(function Main(props: any) {
448
452
  authActive: isAuthActive(model),
449
453
  }
450
454
 
451
- // Static furniture: LICENSE, CODE_OF_CONDUCT, Makefile, .gitignore,
452
- // tsfmt.json and both tsconfigs. Same for every provider.
455
+ // `.gitignore` is EMITTED rather than copied — npm strips that filename
456
+ // from the tarball, so as a template it reached only checkout users. See
457
+ // Gitignore_seneca-provider. Called before the Copy, as every language
458
+ // target calls its own.
459
+ Gitignore({})
460
+
461
+ // Static furniture: LICENSE, CODE_OF_CONDUCT, Makefile, tsfmt.json and
462
+ // both tsconfigs. Same for every provider.
453
463
  Copy({
454
464
  from: 'tm/' + target.name,
455
465
  replace: { ...ctx$.stdrep },
456
466
  })
457
467
 
458
- PackageJson({ provider })
468
+ PackageJson({ provider, target })
459
469
  ProviderSource({ provider })
460
470
  ProviderDoc({ provider })
461
471
  Tests({ provider })
@@ -469,9 +479,8 @@ const Main = cmp(function Main(props: any) {
469
479
  // --- package.json -----------------------------------------------------------
470
480
 
471
481
  const PackageJson = cmp(function PackageJson(props: any) {
472
- const { provider } = props
482
+ const { provider, target } = props
473
483
  const { model } = props.ctx$
474
- const target = model.main[KIT].target['seneca-provider']
475
484
 
476
485
  const deps = collectDeps(model, target.name, target.deps, props.ctx$.log)
477
486
 
@@ -1,14 +1,9 @@
1
1
 
2
- import {
3
- KIT,
4
- getModelPath
5
- } from '@voxgig/apidef'
6
-
7
2
  import type {
8
3
  ModelEntity
9
4
  } from '@voxgig/apidef'
10
5
 
11
- import { cmp, each, Folder } from '@voxgig/sdkgen'
6
+ import { cmp, each, Folder, entityCollection } from '@voxgig/sdkgen'
12
7
 
13
8
 
14
9
  import { TestEntity } from './TestEntity_swift'
@@ -26,7 +21,10 @@ const Test = cmp(function Test(props: any) {
26
21
  // are generated here.
27
22
  Folder({ name: 'Tests' }, () => {
28
23
  Folder({ name: 'ProjectNameSDKTests' }, () => {
29
- each(getModelPath(model, `main.${KIT}.entity`), (entity: ModelEntity) => {
24
+ const entity = each(entityCollection(model))
25
+ .filter((e: any) => false !== e.active)
26
+
27
+ each(entity, (entity: ModelEntity) => {
30
28
  TestEntity({ target, entity })
31
29
  TestDirect({ target, entity })
32
30
  })
@@ -61,7 +61,7 @@ const Package = cmp(async function Package(props: any) {
61
61
  const { repoUrl, issuesUrl } = repoInfo(model)
62
62
 
63
63
  const pkg = {
64
- name: packageName(model, 'npm'),
64
+ name: packageName(model, target.name),
65
65
  version: packageVersion(model, target.name),
66
66
  description: pkgDescription(model, target.name),
67
67
  keywords: keywords(model),
@@ -1,14 +1,9 @@
1
1
 
2
- import {
3
- KIT,
4
- getModelPath
5
- } from '@voxgig/apidef'
6
-
7
2
  import type {
8
3
  ModelEntity
9
4
  } from '@voxgig/apidef'
10
5
 
11
- import { cmp, each, Folder } from '@voxgig/sdkgen'
6
+ import { cmp, each, Folder, entityCollection } from '@voxgig/sdkgen'
12
7
 
13
8
 
14
9
  // import { Quick } from './Quick_ts'
@@ -31,7 +26,13 @@ const Test = cmp(function Test(props: any) {
31
26
  ReadmeExamplesTest({ target })
32
27
 
33
28
  Folder({ name: 'entity' }, () => {
34
- each(model.main[KIT].entity, (entity: ModelEntity) => {
29
+ // entityCollection is the cached, UNFILTERED collection (AGENTS.md), so
30
+ // the active filter the raw model read this replaced never applied is
31
+ // written out here. Tests follow Main: an inactive entity has no source.
32
+ const entity = each(entityCollection(model))
33
+ .filter((e: any) => false !== e.active)
34
+
35
+ each(entity, (entity: ModelEntity) => {
35
36
  TestEntity({ target, entity })
36
37
  TestDirect({ target, entity })
37
38
  })
@@ -1,14 +1,9 @@
1
1
 
2
- import {
3
- KIT,
4
- getModelPath
5
- } from '@voxgig/apidef'
6
-
7
2
  import type {
8
3
  ModelEntity
9
4
  } from '@voxgig/apidef'
10
5
 
11
- import { cmp, each, Folder, File, Content } from '@voxgig/sdkgen'
6
+ import { cmp, each, Folder, File, Content, entityCollection } from '@voxgig/sdkgen'
12
7
 
13
8
  import { TestEntity } from './TestEntity_zig'
14
9
  import { TestDirect } from './TestDirect_zig'
@@ -30,7 +25,8 @@ const Test = cmp(function Test(props: any) {
30
25
  const { model } = props.ctx$
31
26
  const { target } = props
32
27
 
33
- const entity = getModelPath(model, `main.${KIT}.entity`)
28
+ const entity = each(entityCollection(model))
29
+ .filter((e: any) => false !== e.active)
34
30
 
35
31
  Folder({ name: 'test' }, () => {
36
32
  File({ name: 'generated_test.' + target.ext }, () => {
@@ -13,6 +13,29 @@ static voxgig_value* get_elem_i(voxgig_value* list, int64_t i) {
13
13
  return r ? r : voxgig_new_undef();
14
14
  }
15
15
 
16
+ // How many path segments a point has.
17
+ static size_t parts_len(voxgig_value* point) {
18
+ voxgig_value* parts = getp(point, "parts");
19
+ return voxgig_is_list(parts) ? voxgig_as_list(parts)->len : 0;
20
+ }
21
+
22
+ // Does this point's path end in a parameter? A record route ends in the
23
+ // record's identifier (/boards/{id}); a cross-reference that also returns
24
+ // the entity ends in the relationship's name (/posts/{id}/author). That,
25
+ // then fewest segments, is what tells the entity's own route from a
26
+ // cross-reference. The same rule runs at generation time, in
27
+ // helpers/opShape.ts — both sides must move together.
28
+ static bool terminal_param(voxgig_value* point) {
29
+ voxgig_value* parts = getp(point, "parts");
30
+ if (!voxgig_is_list(parts)) return false;
31
+ voxgig_list* pl = voxgig_as_list(parts);
32
+ if (0 == pl->len) return false;
33
+ voxgig_value* last = pl->items[pl->len - 1];
34
+ if (!voxgig_is_string(last)) return false;
35
+ const char* s = voxgig_as_string(last);
36
+ return NULL != s && '{' == s[0];
37
+ }
38
+
16
39
  voxgig_value* make_point_util(Context* ctx, PNError** err) {
17
40
  *err = NULL;
18
41
 
@@ -58,9 +81,10 @@ voxgig_value* make_point_util(Context* ctx, PNError** err) {
58
81
  voxgig_value* selector = is_data ? ctx->data : ctx->mtch;
59
82
 
60
83
  voxgig_value* point = voxgig_new_undef();
84
+ bool matched = false;
61
85
  for (int64_t i = 0; i < plen; i++) {
62
- point = get_elem_i(points, i);
63
- voxgig_value* select_def = to_map(getp(point, "select"));
86
+ voxgig_value* cand = get_elem_i(points, i);
87
+ voxgig_value* select_def = to_map(getp(cand, "select"));
64
88
  bool found = true;
65
89
 
66
90
  if (!v_is_noval(selector) && !v_is_noval(select_def)) {
@@ -85,7 +109,44 @@ voxgig_value* make_point_util(Context* ctx, PNError** err) {
85
109
  if (!v_eq(req_action, select_action)) found = false;
86
110
  }
87
111
 
88
- if (found) break;
112
+ if (found) {
113
+ point = cand;
114
+ matched = true;
115
+ break;
116
+ }
117
+ }
118
+
119
+ // select.exist can list more than the params needed to pick a point, so
120
+ // nothing matches — fall back to the entity's own route rather than
121
+ // whichever point came last.
122
+ if (!matched) {
123
+ // A request naming an action reaches here only because that action's
124
+ // own point failed its exist test, so it is unbuildable whatever we
125
+ // pick. Refuse it BEFORE choosing a fallback: the guard below compares
126
+ // the chosen point's $action and would wave the request through
127
+ // whenever the fallback lands on the action point itself.
128
+ voxgig_value* unmatched_action = getp(reqselector, "$action");
129
+ if (!v_is_noval(unmatched_action)) {
130
+ char* actstr = voxgig_stringify(unmatched_action, -1);
131
+ char buf[512];
132
+ snprintf(buf, sizeof(buf), "Operation \"%s\" action \"%s\" is not valid.",
133
+ op->name, actstr ? actstr : "");
134
+ *err = context_make_error(ctx, "point_action_invalid", buf);
135
+ return NULL;
136
+ }
137
+
138
+ point = get_elem_i(points, 0);
139
+ for (int64_t i = 0; i < plen; i++) {
140
+ voxgig_value* cand = get_elem_i(points, i);
141
+ bool cand_term = terminal_param(cand);
142
+ bool best_term = terminal_param(point);
143
+ if (cand_term != best_term) {
144
+ if (cand_term) point = cand;
145
+ }
146
+ else if (parts_len(cand) < parts_len(point)) {
147
+ point = cand;
148
+ }
149
+ }
89
150
  }
90
151
 
91
152
  voxgig_value* req_action = getp(reqselector, "$action");
@@ -641,21 +641,56 @@
641
641
  sa (vs/getprop select-def "$action")]
642
642
  (when (not= ra sa) (reset! found false))))
643
643
  @found))
644
- ;; "first matching point, else the last point" (mirrors the
645
- ;; reference loop that keeps the last-visited point).
646
- chosen (loop [ps (vec (op-points op)) last nil]
647
- (if (empty? ps) last
648
- (let [p (first ps)]
649
- (if (point-matches? p) p (recur (rest ps) p)))))
644
+ ;; How many path segments a point has, and whether its path
645
+ ;; ends in a parameter. A record route ends in the record's
646
+ ;; identifier (/boards/{id}); a cross-reference that also
647
+ ;; returns the entity ends in the relationship's name
648
+ ;; (/posts/{id}/author). That, then fewest segments, is what
649
+ ;; tells the entity's own route from a cross-reference. The
650
+ ;; same rule runs at generation time, in helpers/opShape.ts —
651
+ ;; both sides must move together.
652
+ parts-len (fn [p]
653
+ (let [parts (vs/getprop p "parts")]
654
+ (if (vs/islist parts) (vs/size parts) 0)))
655
+ terminal-param? (fn [p]
656
+ (let [parts (vs/getprop p "parts")]
657
+ (and (vs/islist parts)
658
+ (pos? (vs/size parts))
659
+ (let [last-part (vs/getelem parts (dec (vs/size parts)))]
660
+ (and (string? last-part)
661
+ (str/starts-with? last-part "{"))))))
662
+ own-point (fn [ps]
663
+ (reduce (fn [best cand]
664
+ (let [ct (terminal-param? cand)
665
+ bt (terminal-param? best)]
666
+ (if (not= ct bt)
667
+ (if ct cand best)
668
+ (if (< (parts-len cand) (parts-len best)) cand best))))
669
+ (first ps) ps))
670
+ matched (first (filter point-matches? (vec (op-points op))))
650
671
  req-action (vs/getprop reqselector "$action")]
651
- (if (and reqselector req-action chosen)
652
- (let [point-select (to-map (vs/getprop chosen "select"))
653
- point-action (vs/getprop point-select "$action")]
654
- (if (not= req-action point-action)
655
- [nil (ctx-error ctx "point_action_invalid"
656
- (str "Operation \"" (op-name op) "\" action \"" (vs/stringify req-action) "\" is not valid."))]
657
- (do (oset! ctx :point chosen) [(oget ctx :point) nil])))
658
- (do (oset! ctx :point chosen) [(oget ctx :point) nil]))))))))
672
+ (cond
673
+ ;; select.exist can list more than the params needed to pick a
674
+ ;; point, so nothing matches. A request naming an action reaches
675
+ ;; here only because that action's own point failed its exist
676
+ ;; test, so it is unbuildable whatever we pick — refuse it
677
+ ;; BEFORE falling back, since the guard below compares the
678
+ ;; chosen point's $action and would wave the request through
679
+ ;; whenever the fallback lands on the action point itself.
680
+ (and (nil? matched) reqselector req-action)
681
+ [nil (ctx-error ctx "point_action_invalid"
682
+ (str "Operation \"" (op-name op) "\" action \"" (vs/stringify req-action) "\" is not valid."))]
683
+
684
+ :else
685
+ (let [chosen (or matched (own-point (vec (op-points op))))]
686
+ (if (and reqselector req-action chosen)
687
+ (let [point-select (to-map (vs/getprop chosen "select"))
688
+ point-action (vs/getprop point-select "$action")]
689
+ (if (not= req-action point-action)
690
+ [nil (ctx-error ctx "point_action_invalid"
691
+ (str "Operation \"" (op-name op) "\" action \"" (vs/stringify req-action) "\" is not valid."))]
692
+ (do (oset! ctx :point chosen) [(oget ctx :point) nil])))
693
+ (do (oset! ctx :point chosen) [(oget ctx :point) nil]))))))))))
659
694
 
660
695
  (defn u-make-spec [ctx]
661
696
  (if (some? (out-get ctx "spec"))
@@ -330,9 +330,10 @@ inline Value makePoint(CtxPtr ctx) {
330
330
  }
331
331
 
332
332
  Value point;
333
+ bool matched = false;
333
334
  for (size_t i = 0; i < op->points.size(); i++) {
334
- point = op->points[i];
335
- Value selectDef = Helpers::toMapAny(getp(point, "select"));
335
+ Value cand = op->points[i];
336
+ Value selectDef = Helpers::toMapAny(getp(cand, "select"));
336
337
  bool found = true;
337
338
 
338
339
  if (selector.is_map() && selectDef.is_map()) {
@@ -356,7 +357,58 @@ inline Value makePoint(CtxPtr ctx) {
356
357
  if (reqAction != selectAction) found = false;
357
358
  }
358
359
 
359
- if (found) break;
360
+ if (found) {
361
+ point = cand;
362
+ matched = true;
363
+ break;
364
+ }
365
+ }
366
+
367
+ // select.exist can list more than the params needed to pick a point (for
368
+ // /boards/{id} it is Trello's 17 optional query-includes), so a plain
369
+ // {id} call matches NOTHING. Fall back to the entity's own route rather
370
+ // than whichever point came last.
371
+ if (!matched) {
372
+ // A request naming an action reaches here only because that action's
373
+ // own point failed its exist test, so it is unbuildable whatever we
374
+ // pick. Refuse it BEFORE choosing a fallback: the guard below compares
375
+ // the chosen point's $action and would wave the request through
376
+ // whenever the fallback lands on the action point itself.
377
+ Value unmatchedAction = getp(reqselector, "$action", Value(nullptr));
378
+ if (!unmatchedAction.is_null()) {
379
+ throw ctx->makeError("point_action_invalid",
380
+ "Operation \"" + op->name + "\" action \"" +
381
+ Struct::stringify(unmatchedAction) + "\" is not valid.");
382
+ }
383
+
384
+ // A terminal parameter marks a record route (/boards/{id}); a
385
+ // cross-reference ends in the relationship's name (/posts/{id}/author).
386
+ // Failing that, the shallower path wins. The same rule runs at
387
+ // generation time, in helpers/opShape.ts — both sides must move
388
+ // together.
389
+ auto partsLen = [](const Value& p) -> size_t {
390
+ Value parts = getp(p, "parts");
391
+ return parts.is_list() ? parts.as_list()->size() : 0;
392
+ };
393
+ auto terminalParam = [](const Value& p) -> bool {
394
+ Value parts = getp(p, "parts");
395
+ if (!parts.is_list() || parts.as_list()->empty()) return false;
396
+ const Value& last = parts.as_list()->back();
397
+ return last.is_string() && 0 == last.as_string().rfind("{", 0);
398
+ };
399
+
400
+ point = op->points[0];
401
+ for (size_t i = 0; i < op->points.size(); i++) {
402
+ const Value& cand = op->points[i];
403
+ bool candTerm = terminalParam(cand);
404
+ bool bestTerm = terminalParam(point);
405
+ if (candTerm != bestTerm) {
406
+ if (candTerm) point = cand;
407
+ }
408
+ else if (partsLen(cand) < partsLen(point)) {
409
+ point = cand;
410
+ }
411
+ }
360
412
  }
361
413
 
362
414
  if (reqselector.is_map()) {
@@ -313,10 +313,44 @@ public class TestFeature : BaseFeature
313
313
  {
314
314
  var opname = op.Name;
315
315
 
316
- // Get last point from config.
316
+ // Pick the entity's own endpoint from config, not a cross-reference
317
+ // from another resource that also returns it — the same rule
318
+ // MakePoint falls back to, so the seed-data query is built from the
319
+ // endpoint the request will actually be sent to: a terminal `{id}`
320
+ // marks a record route, and failing that the shallower path wins.
317
321
  var points = StructUtils.GetPath(ctx.Config,
318
322
  StructUtils.Jt("entity", ctx.Entity!.GetName(), "op", opname, "points"));
319
- var point = StructUtils.GetElem(points, -1);
323
+ var point = StructUtils.GetElem(points, 0);
324
+ if (points is List<object?> pointList)
325
+ {
326
+ static int PointPartsLen(object? p) =>
327
+ StructUtils.GetProp(p, "parts") is List<object?> parts ? parts.Count : 0;
328
+ static bool PointTerminalParam(object? p)
329
+ {
330
+ if (StructUtils.GetProp(p, "parts") is not List<object?> parts || parts.Count == 0)
331
+ {
332
+ return false;
333
+ }
334
+ return parts[parts.Count - 1] is string last && last.StartsWith("{");
335
+ }
336
+
337
+ foreach (var candidate in pointList)
338
+ {
339
+ var candTerm = PointTerminalParam(candidate);
340
+ var bestTerm = PointTerminalParam(point);
341
+ if (candTerm != bestTerm)
342
+ {
343
+ if (candTerm)
344
+ {
345
+ point = candidate;
346
+ }
347
+ }
348
+ else if (PointPartsLen(candidate) < PointPartsLen(point))
349
+ {
350
+ point = candidate;
351
+ }
352
+ }
353
+ }
320
354
 
321
355
  // Get required params.
322
356
  var paramsPath = StructUtils.GetPath(point, StructUtils.Jt("args", "params"));
@@ -6,6 +6,27 @@ namespace ProjectNameSdk.Util;
6
6
 
7
7
  public static partial class SdkUtility
8
8
  {
9
+ // How many path segments a point has.
10
+ private static int PartsLen(Dictionary<string, object?>? point)
11
+ {
12
+ return StructUtils.GetProp(point, "parts") is List<object?> parts ? parts.Count : 0;
13
+ }
14
+
15
+ // Does this point's path end in a parameter? A record route ends in the
16
+ // record's identifier (/boards/{id}); a cross-reference that also returns
17
+ // the entity ends in the relationship's name (/posts/{id}/author). That,
18
+ // then fewest segments, is what tells the entity's own route from a
19
+ // cross-reference. The same rule runs at generation time, in
20
+ // helpers/opShape.ts — both sides must move together.
21
+ private static bool TerminalParam(Dictionary<string, object?>? point)
22
+ {
23
+ if (StructUtils.GetProp(point, "parts") is not List<object?> parts || parts.Count == 0)
24
+ {
25
+ return false;
26
+ }
27
+ return parts[parts.Count - 1] is string last && last.StartsWith("{");
28
+ }
29
+
9
30
  internal static Dictionary<string, object?>? MakePointUtil(Context ctx)
10
31
  {
11
32
  if (ctx.Out.TryGetValue("point", out var outPoint) && outPoint != null)
@@ -62,10 +83,10 @@ public static partial class SdkUtility
62
83
  }
63
84
 
64
85
  Dictionary<string, object?>? point = null;
86
+ var matched = false;
65
87
  foreach (var candidate in op.Points)
66
88
  {
67
- point = candidate;
68
- var selectDef = Helpers.ToMapAny(StructUtils.GetProp(point, "select"));
89
+ var selectDef = Helpers.ToMapAny(StructUtils.GetProp(candidate, "select"));
69
90
  var found = true;
70
91
 
71
92
  if (selector != null && selectDef != null)
@@ -98,10 +119,52 @@ public static partial class SdkUtility
98
119
 
99
120
  if (found)
100
121
  {
122
+ point = candidate;
123
+ matched = true;
101
124
  break;
102
125
  }
103
126
  }
104
127
 
128
+ // select.exist can list more than the params needed to pick a
129
+ // point, so nothing matches — fall back to the entity's own
130
+ // route rather than the last point.
131
+ if (!matched)
132
+ {
133
+ // A request naming an action reaches here only because that
134
+ // action's own point failed its exist test, so it is
135
+ // unbuildable whatever we pick. Refuse it BEFORE choosing a
136
+ // fallback: the guard below compares the chosen point's
137
+ // $action and would wave the request through whenever the
138
+ // fallback lands on the action point itself.
139
+ var unmatchedAction = reqselector != null
140
+ ? StructUtils.GetProp(reqselector, "$action") : null;
141
+ if (unmatchedAction != null)
142
+ {
143
+ throw ctx.MakeError("point_action_invalid",
144
+ "Operation \"" + op.Name +
145
+ "\" action \"" + StructUtils.Stringify(unmatchedAction) +
146
+ "\" is not valid.");
147
+ }
148
+
149
+ point = op.Points[0];
150
+ foreach (var candidate in op.Points)
151
+ {
152
+ var candTerm = TerminalParam(candidate);
153
+ var bestTerm = TerminalParam(point);
154
+ if (candTerm != bestTerm)
155
+ {
156
+ if (candTerm)
157
+ {
158
+ point = candidate;
159
+ }
160
+ }
161
+ else if (PartsLen(candidate) < PartsLen(point))
162
+ {
163
+ point = candidate;
164
+ }
165
+ }
166
+ }
167
+
105
168
  if (reqselector != null)
106
169
  {
107
170
  var reqAction = StructUtils.GetProp(reqselector, "$action");
@@ -214,10 +214,40 @@ class TestFeature extends BaseFeature {
214
214
  final param = ctx.utility.param;
215
215
 
216
216
  final opname = op.name;
217
- final point = vs.getelem(
218
- vs.getpath(ctx.config,
219
- ['entity', _entname(ctx.entity), 'op', opname, 'points']),
220
- -1);
217
+
218
+ // Pick the entity's own endpoint from config, not a cross-reference from
219
+ // another resource that also returns it — the same rule makePoint falls
220
+ // back to, so the seed-data query is built from the endpoint the request
221
+ // will actually be sent to: a terminal `{id}` marks a record route, and
222
+ // failing that the shallower path wins.
223
+ final points = vs.getpath(
224
+ ctx.config, ['entity', _entname(ctx.entity), 'op', opname, 'points']);
225
+ var point = vs.getelem(points, 0);
226
+ if (points is List) {
227
+ int pointPartsLen(dynamic p) {
228
+ final parts = vs.getprop(p, 'parts');
229
+ return parts is List ? parts.length : 0;
230
+ }
231
+
232
+ bool pointTerminalParam(dynamic p) {
233
+ final parts = vs.getprop(p, 'parts');
234
+ if (parts is! List || parts.isEmpty) {
235
+ return false;
236
+ }
237
+ final last = parts[parts.length - 1];
238
+ return last is String && last.startsWith('{');
239
+ }
240
+
241
+ for (final cand in points) {
242
+ if (pointTerminalParam(cand) != pointTerminalParam(point)) {
243
+ if (pointTerminalParam(cand)) {
244
+ point = cand;
245
+ }
246
+ } else if (pointPartsLen(cand) < pointPartsLen(point)) {
247
+ point = cand;
248
+ }
249
+ }
250
+ }
221
251
 
222
252
  final reqd = vs.transform(
223
253
  vs.select(vs.getpath(point, ['args', 'params']), {'reqd': true}),