@voxgig/sdkgen 4.2.3 → 4.2.5

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 (81) hide show
  1. package/bin/voxgig-sdkgen +1 -1
  2. package/dist/cmp/AgentGuideTop.js +15 -7
  3. package/dist/cmp/AgentGuideTop.js.map +1 -1
  4. package/dist/cmp/ReadmeStation.js +88 -12
  5. package/dist/cmp/ReadmeStation.js.map +1 -1
  6. package/dist/helpers/station.d.ts +3 -0
  7. package/dist/helpers/station.js +67 -0
  8. package/dist/helpers/station.js.map +1 -0
  9. package/dist/sdkgen.d.ts +2 -1
  10. package/dist/sdkgen.js +41 -15
  11. package/dist/sdkgen.js.map +1 -1
  12. package/dist/tsconfig.tsbuildinfo +1 -1
  13. package/dist/types.d.ts +2 -0
  14. package/dist/utility.js +15 -1
  15. package/dist/utility.js.map +1 -1
  16. package/model/sdkgen.aon +20 -0
  17. package/package.json +1 -1
  18. package/project/.sdk/model/feature/audit.aon +1 -0
  19. package/project/.sdk/model/feature/cache.aon +1 -0
  20. package/project/.sdk/model/feature/clienttrack.aon +1 -0
  21. package/project/.sdk/model/feature/debug.aon +1 -0
  22. package/project/.sdk/model/feature/idempotency.aon +1 -0
  23. package/project/.sdk/model/feature/log.aon +1 -0
  24. package/project/.sdk/model/feature/metrics.aon +1 -0
  25. package/project/.sdk/model/feature/netsim.aon +1 -0
  26. package/project/.sdk/model/feature/paging.aon +1 -0
  27. package/project/.sdk/model/feature/proxy.aon +1 -0
  28. package/project/.sdk/model/feature/ratelimit.aon +1 -0
  29. package/project/.sdk/model/feature/rbac.aon +1 -0
  30. package/project/.sdk/model/feature/retry.aon +1 -0
  31. package/project/.sdk/model/feature/streaming.aon +1 -0
  32. package/project/.sdk/model/feature/telemetry.aon +1 -0
  33. package/project/.sdk/model/feature/test.aon +1 -0
  34. package/project/.sdk/model/feature/timeout.aon +1 -0
  35. package/project/.sdk/src/cmp/c/Config_c.ts +13 -10
  36. package/project/.sdk/src/cmp/clojure/Config_clojure.ts +4 -1
  37. package/project/.sdk/src/cmp/dart/Config_dart.ts +4 -1
  38. package/project/.sdk/src/cmp/elixir/Config_elixir.ts +4 -1
  39. package/project/.sdk/src/cmp/elixir/Main_elixir.ts +21 -2
  40. package/project/.sdk/src/cmp/go/Config_go.ts +4 -1
  41. package/project/.sdk/src/cmp/java/Config_java.ts +11 -8
  42. package/project/.sdk/src/cmp/js/Config_js.ts +4 -1
  43. package/project/.sdk/src/cmp/js/Main_js.ts +20 -1
  44. package/project/.sdk/src/cmp/js/fragment/MainStation.fragment.js +36 -0
  45. package/project/.sdk/src/cmp/kotlin/Config_kotlin.ts +11 -7
  46. package/project/.sdk/src/cmp/lua/Config_lua.ts +4 -1
  47. package/project/.sdk/src/cmp/ocaml/Config_ocaml.ts +3 -1
  48. package/project/.sdk/src/cmp/php/Config_php.ts +4 -1
  49. package/project/.sdk/src/cmp/py/Config_py.ts +4 -1
  50. package/project/.sdk/src/cmp/rb/Config_rb.ts +4 -1
  51. package/project/.sdk/src/cmp/rust/ReadmeEntity_rust.ts +2 -2
  52. package/project/.sdk/src/cmp/rust/ReadmeHowto_rust.ts +2 -2
  53. package/project/.sdk/src/cmp/rust/ReadmeIntro_rust.ts +2 -2
  54. package/project/.sdk/src/cmp/rust/ReadmeModel_rust.ts +2 -2
  55. package/project/.sdk/src/cmp/rust/ReadmeQuick_rust.ts +3 -3
  56. package/project/.sdk/src/cmp/rust/ReadmeRef_rust.ts +3 -3
  57. package/project/.sdk/src/cmp/rust/ReadmeTopQuick_rust.ts +2 -2
  58. package/project/.sdk/src/cmp/rust/ReadmeTopTest_rust.ts +2 -2
  59. package/project/.sdk/src/cmp/rust/TestEntity_rust.ts +7 -7
  60. package/project/.sdk/src/cmp/swift/EntityTypes_swift.ts +2 -2
  61. package/project/.sdk/src/cmp/swift/Entity_swift.ts +3 -1
  62. package/project/.sdk/src/cmp/swift/Main_swift.ts +37 -2
  63. package/project/.sdk/src/cmp/swift/Package_swift.ts +9 -6
  64. package/project/.sdk/src/cmp/swift/ReadmeExamplesTest_swift.ts +1 -1
  65. package/project/.sdk/src/cmp/swift/ReadmeExplanation_swift.ts +5 -3
  66. package/project/.sdk/src/cmp/swift/Test_swift.ts +3 -1
  67. package/project/.sdk/src/cmp/swift/utility_swift.ts +16 -0
  68. package/project/.sdk/src/cmp/ts/Config_ts.ts +4 -1
  69. package/project/.sdk/src/cmp/ts/Main_ts.ts +20 -1
  70. package/project/.sdk/src/cmp/ts/fragment/MainStation.fragment.ts +36 -0
  71. package/project/.sdk/src/cmp/zig/Config_zig.ts +4 -2
  72. package/project/.sdk/tm/elixir/lib/utility/struct/voxgig_struct.ex +66 -12
  73. package/project/.sdk/tm/zig/Makefile +20 -0
  74. package/project/.sdk/tm/zig/feature/test.zig +1 -1
  75. package/project/sdkgen-package.json +1 -1
  76. package/src/cmp/AgentGuideTop.ts +15 -7
  77. package/src/cmp/ReadmeStation.ts +93 -13
  78. package/src/helpers/station.ts +80 -0
  79. package/src/sdkgen.ts +52 -15
  80. package/src/types.ts +6 -0
  81. package/src/utility.ts +15 -1
@@ -7,7 +7,7 @@ import {
7
7
  nom,
8
8
  } from '@voxgig/apidef'
9
9
 
10
- import { crateIdent, rustVarName } from './utility_rust'
10
+ import { crateIdent, rustVarName, rustMethodName } from './utility_rust'
11
11
 
12
12
 
13
13
  const ReadmeQuick = cmp(function ReadmeQuick(props: any) {
@@ -58,7 +58,7 @@ let client = ${ctor};
58
58
  const eName = nom(exampleEntity, 'Name')
59
59
  const article = /^[aeiou]/i.test(eName) ? 'an' : 'a'
60
60
  const eVar = rustVarName(exampleEntity.name)
61
- const method = rustVarName(exampleEntity.name)
61
+ const method = rustMethodName(exampleEntity.name)
62
62
  const opnames = entityOps(exampleEntity)
63
63
  // Model-driven id key: `idF` is the entity's id-like MATCH field name, or
64
64
  // null. `dataIdF` is the id on the RETURNED record's data type.
@@ -91,7 +91,7 @@ match client.${method}(Value::Noval).list(Value::Noval, Value::Noval) {
91
91
  const neName = nom(nestedEntity, 'Name')
92
92
  const neArticle = /^[aeiou]/i.test(neName) ? 'an' : 'a'
93
93
  const neVar = rustVarName(nestedEntity.name)
94
- const neMethod = rustVarName(nestedEntity.name)
94
+ const neMethod = rustMethodName(nestedEntity.name)
95
95
 
96
96
  const neIdF = entityIdField(nestedEntity)
97
97
  const neRequired = opRequestShape(nestedEntity, 'load').items
@@ -6,7 +6,7 @@ import {
6
6
  getModelPath,
7
7
  } from '@voxgig/apidef'
8
8
 
9
- import { crateIdent, rustVarName } from './utility_rust'
9
+ import { crateIdent, rustVarName, rustMethodName } from './utility_rust'
10
10
 
11
11
 
12
12
  // Type names come from the shared canonToType 'rust' column (single source of truth).
@@ -120,7 +120,7 @@ let client = test_sdk(Value::Noval, Value::Noval);
120
120
 
121
121
  // Entity factory methods
122
122
  publishedEntities.map((ent: any) => {
123
- Content(`#### \`${rustVarName(ent.name)}(entopts: Value) -> Rc<${ent.Name}Entity>\`
123
+ Content(`#### \`${rustMethodName(ent.name)}(entopts: Value) -> Rc<${ent.Name}Entity>\`
124
124
 
125
125
  Create a new \`${ent.Name}Entity\` instance. Pass \`Value::Noval\` for no
126
126
  initial options.
@@ -168,7 +168,7 @@ Prepare a fetch definition without sending. Returns the fetchdef on \`Ok\`.
168
168
  const fields = ent.fields || []
169
169
  const idF = entityIdField(ent)
170
170
  const eVar = rustVarName(ent.name)
171
- const method = rustVarName(ent.name)
171
+ const method = rustMethodName(ent.name)
172
172
 
173
173
  Content(`
174
174
  ---
@@ -7,7 +7,7 @@ import {
7
7
  nom,
8
8
  } from '@voxgig/apidef'
9
9
 
10
- import { crateIdent, rustVarName } from './utility_rust'
10
+ import { crateIdent, rustVarName, rustMethodName } from './utility_rust'
11
11
 
12
12
 
13
13
  // A type-correct rust expression constructing a voxgig struct Value for a
@@ -46,7 +46,7 @@ let client = ${ctor};
46
46
  if (exampleEntity) {
47
47
  const eName = nom(exampleEntity, 'Name')
48
48
  const eVar = rustVarName(exampleEntity.name)
49
- const method = rustVarName(exampleEntity.name)
49
+ const method = rustMethodName(exampleEntity.name)
50
50
  const opnames = Object.keys(exampleEntity.op || {})
51
51
  // Model-driven id key: null when the entity has no id-like field, in which
52
52
  // case the load example takes no match argument.
@@ -7,7 +7,7 @@ import {
7
7
  nom,
8
8
  } from '@voxgig/apidef'
9
9
 
10
- import { crateIdent, rustVarName } from './utility_rust'
10
+ import { crateIdent, rustVarName, rustMethodName } from './utility_rust'
11
11
 
12
12
 
13
13
  // A type-correct rust expression constructing a voxgig struct Value for a
@@ -40,7 +40,7 @@ let client = test_sdk(Value::Noval, Value::Noval);
40
40
 
41
41
  if (exampleEntity && primaryOp) {
42
42
  const eName = nom(exampleEntity, 'Name')
43
- const method = rustVarName(exampleEntity.name)
43
+ const method = rustMethodName(exampleEntity.name)
44
44
  const idF = entityIdField(exampleEntity)
45
45
  const isMatchOp = 'load' === primaryOp || 'remove' === primaryOp
46
46
  let arg = 'Value::Noval'
@@ -21,7 +21,7 @@ import {
21
21
  } from '@voxgig/sdkgen'
22
22
 
23
23
 
24
- import { rustVarName } from './utility_rust'
24
+ import { rustVarName, rustMethodName } from './utility_rust'
25
25
 
26
26
 
27
27
  // Rust's GenCtx mirrors the shared shape (see TestEntity_ts.ts) plus the
@@ -100,7 +100,7 @@ const TestEntity = cmp(function TestEntity(props: any) {
100
100
  }
101
101
  `
102
102
 
103
- const method = rustVarName(entity.name)
103
+ const method = rustMethodName(entity.name)
104
104
  const evar = rustVarName(entity.name)
105
105
 
106
106
  File({ name: entity.name + '_entity_test.' + target.ext }, () => {
@@ -330,7 +330,7 @@ const generateCreate: OpGen = (ctx, step, index) => {
330
330
  Content(` // CREATE
331
331
  `)
332
332
  if (needsEnt) {
333
- Content(` let ${entvar} = client.${rustVarName(entity.name)}(Value::Noval);
333
+ Content(` let ${entvar} = client.${rustMethodName(entity.name)}(Value::Noval);
334
334
  `)
335
335
  }
336
336
 
@@ -381,7 +381,7 @@ const generateList: OpGen = (ctx, step, index) => {
381
381
  Content(` // LIST
382
382
  `)
383
383
  if (needsEnt) {
384
- Content(` let ${entvar} = client.${rustVarName(entity.name)}(Value::Noval);
384
+ Content(` let ${entvar} = client.${rustMethodName(entity.name)}(Value::Noval);
385
385
  `)
386
386
  }
387
387
 
@@ -464,7 +464,7 @@ const generateUpdate: OpGen = (ctx, step, index) => {
464
464
  Content(` // UPDATE
465
465
  `)
466
466
  if (needsEnt) {
467
- Content(` let ${entvar} = client.${rustVarName(entity.name)}(Value::Noval);
467
+ Content(` let ${entvar} = client.${rustMethodName(entity.name)}(Value::Noval);
468
468
  `)
469
469
  }
470
470
  Content(` let ${datavar}_up = Value::empty_map();
@@ -568,7 +568,7 @@ const generateLoad: OpGen = (ctx, step, index) => {
568
568
  Content(` // LOAD
569
569
  `)
570
570
  if (!hasEntVar) {
571
- Content(` let ${entvar} = client.${rustVarName(entity.name)}(Value::Noval);
571
+ Content(` let ${entvar} = client.${rustMethodName(entity.name)}(Value::Noval);
572
572
  `)
573
573
  }
574
574
  if (!hasSrcData && hasEntId) {
@@ -626,7 +626,7 @@ const generateRemove: OpGen = (ctx, step, index) => {
626
626
  Content(` // REMOVE
627
627
  `)
628
628
  if (needsEnt) {
629
- Content(` let ${entvar} = client.${rustVarName(entity.name)}(Value::Noval);
629
+ Content(` let ${entvar} = client.${rustMethodName(entity.name)}(Value::Noval);
630
630
  `)
631
631
  }
632
632
  // Always match the prior-created entity by id to avoid mock-order flakes.
@@ -32,7 +32,7 @@ import {
32
32
  getModelPath,
33
33
  } from '@voxgig/apidef'
34
34
 
35
- import { swiftVarName } from './utility_swift'
35
+ import { swiftVarName , swiftTargetDir, swiftTestDir } from './utility_swift'
36
36
 
37
37
 
38
38
  const LANG = 'swift'
@@ -89,7 +89,7 @@ const EntityTypes = cmp(function EntityTypes(props: any) {
89
89
  warnEntityTypeCollisions(entity, log, LANG)
90
90
 
91
91
  Folder({ name: 'Sources' }, () => {
92
- Folder({ name: 'ProjectNameSDK' }, () => {
92
+ Folder({ name: swiftTargetDir(model) }, () => {
93
93
  Folder({ name: 'entity' }, () => {
94
94
 
95
95
  File({ name: model.const.Name + 'Types.' + target.ext }, () => {
@@ -1,6 +1,8 @@
1
1
 
2
2
  import * as Path from 'node:path'
3
3
 
4
+ import { swiftTargetDir, swiftTestDir } from './utility_swift'
5
+
4
6
  import {
5
7
  cmp, camelify, names,
6
8
  Content, File, Folder, Fragment,
@@ -33,7 +35,7 @@ const Entity = cmp(function Entity(props: any) {
33
35
 
34
36
  // Entity files join the copied runtime under Sources/ProjectNameSDK/entity.
35
37
  Folder({ name: 'Sources' }, () => {
36
- Folder({ name: 'ProjectNameSDK' }, () => {
38
+ Folder({ name: swiftTargetDir(model) }, () => {
37
39
  Folder({ name: 'entity' }, () => {
38
40
 
39
41
  File({ name: cls + '.' + target.ext }, () => {
@@ -1,6 +1,8 @@
1
1
 
2
2
  import * as Path from 'node:path'
3
3
 
4
+ import { swiftTargetDir, swiftTestDir } from './utility_swift'
5
+
4
6
  import {
5
7
  cmp, each,
6
8
  File, Content, Copy, Folder, Fragment,
@@ -40,18 +42,51 @@ const Main = cmp(async function Main(props: any) {
40
42
 
41
43
  // Copy tm/swift files with replacements. `src/` holds only the per-feature
42
44
  // extension folders (not shipped into the SDK output).
45
+ // THE COPIED TREE HAS TO CARRY THE API NAME TOO.
46
+ //
47
+ // Copy substitutes file CONTENTS, never path components, so a blanket copy
48
+ // of tm/swift landed the runtime in a directory literally called
49
+ // ProjectNameSDK. Package.swift papered over it with an explicit `path:`,
50
+ // so it compiled and every swift suite passed — while every published SDK
51
+ // shipped `Sources/ProjectNameSDK/`, and SwiftPM's own convention
52
+ // (Sources/<target>) was broken in all of them.
53
+ //
54
+ // Copy's `to` prop names the destination, so the two placeholder subtrees
55
+ // are copied explicitly and the rest of tm/swift blanket-copied as before.
43
56
  Copy({
44
57
  from: 'tm/' + target.name,
45
- exclude: [/src\//],
58
+ exclude: [/src\//, /Sources\//, /Tests\//],
46
59
  replace: {
47
60
  ...props.ctx$.stdrep,
48
61
  ProjectName: model.const.Name,
49
62
  }
50
63
  })
51
64
 
65
+ Folder({ name: 'Sources' }, () => {
66
+ Copy({
67
+ from: 'tm/' + target.name + '/Sources/ProjectNameSDK',
68
+ to: swiftTargetDir(model),
69
+ replace: {
70
+ ...props.ctx$.stdrep,
71
+ ProjectName: model.const.Name,
72
+ }
73
+ })
74
+ })
75
+
76
+ Folder({ name: 'Tests' }, () => {
77
+ Copy({
78
+ from: 'tm/' + target.name + '/Tests/ProjectNameSDKTests',
79
+ to: swiftTestDir(model),
80
+ replace: {
81
+ ...props.ctx$.stdrep,
82
+ ProjectName: model.const.Name,
83
+ }
84
+ })
85
+ })
86
+
52
87
  // Generated sources join the copied runtime under Sources/ProjectNameSDK.
53
88
  Folder({ name: 'Sources' }, () => {
54
- Folder({ name: 'ProjectNameSDK' }, () => {
89
+ Folder({ name: swiftTargetDir(model) }, () => {
55
90
  Folder({ name: 'core' }, () => {
56
91
 
57
92
  // Main SDK client class, with entity accessors injected at the SLOT.
@@ -1,4 +1,6 @@
1
1
 
2
+ import { swiftTargetDir, swiftTestDir } from './utility_swift'
3
+
2
4
  import {
3
5
  Content,
4
6
  File,
@@ -14,10 +16,11 @@ import type {
14
16
 
15
17
  // Emits Package.swift (the SwiftPM manifest; the Swift twin of Package_go's
16
18
  // go.mod / Package_csharp's csproj). The library target compiles everything
17
- // under Sources/ProjectNameSDK (the copied runtime + generated sources); the
18
- // test target compiles Tests/ProjectNameSDKTests. Directory names are the
19
- // verbatim copied paths (Copy does not rewrite path components); the target
20
- // NAMES carry the API name.
19
+ // under Sources/<Name>Sdk (the copied runtime + generated sources); the test
20
+ // target compiles Tests/<Name>SdkTests. Both directories carry the API name:
21
+ // Copy does not rewrite path components, so Main_swift copies the two
22
+ // placeholder subtrees explicitly via Copy's `to` prop rather than letting a
23
+ // blanket copy ship them as ProjectNameSDK.
21
24
  //
22
25
  // Dependencies: the runtime itself is dependency-free (Foundation + the
23
26
  // vendored struct), but declared target/feature deps flow into the manifest
@@ -91,11 +94,11 @@ let package = Package(
91
94
  targets: [
92
95
  .target(
93
96
  name: "${Name}Sdk",${targetdeps}
94
- path: "Sources/ProjectNameSDK"),
97
+ path: "Sources/${swiftTargetDir(model)}"),
95
98
  .testTarget(
96
99
  name: "${Name}SdkTests",
97
100
  dependencies: ["${Name}Sdk"],
98
- path: "Tests/ProjectNameSDKTests"),
101
+ path: "Tests/${swiftTestDir(model)}"),
99
102
  ]
100
103
  )
101
104
  `)
@@ -64,7 +64,7 @@ final class ReadmeExamplesTest: XCTestCase {
64
64
  // The three documents to scan, resolved relative to this test file.
65
65
  private func docs() -> [Doc] {
66
66
  let here = URL(fileURLWithPath: #filePath)
67
- .deletingLastPathComponent() // ProjectNameSDKTests
67
+ .deletingLastPathComponent() // the test target dir
68
68
  .deletingLastPathComponent() // Tests
69
69
  .deletingLastPathComponent() // swift (module dir)
70
70
  let project = here.deletingLastPathComponent() // project (holds root README)
@@ -1,4 +1,6 @@
1
1
 
2
+ import { swiftTargetDir, swiftTestDir } from './utility_swift'
3
+
2
4
  import { cmp, Content } from '@voxgig/sdkgen'
3
5
 
4
6
 
@@ -24,16 +26,16 @@ generated for editor documentation.
24
26
  \`\`\`
25
27
  swift/
26
28
  ├── Package.swift -- SwiftPM manifest (zero runtime deps)
27
- ├── Sources/ProjectNameSDK/
29
+ ├── Sources/${swiftTargetDir(model)}/
28
30
  │ ├── core/ -- Main client, config, entity base, error type
29
31
  │ ├── entity/ -- Generated entity clients
30
32
  │ ├── feature/ -- Built-in features (Base, Test, Log, ...)
31
33
  │ ├── utility/ -- Utility functions
32
34
  │ └── Struct/ -- Vendored Voxgig Struct port
33
- └── Tests/ProjectNameSDKTests/ -- Test suites (XCTest)
35
+ └── Tests/${swiftTestDir(model)}/ -- Test suites (XCTest)
34
36
  \`\`\`
35
37
 
36
- The main client class (\`${SDK}\`, under \`Sources/ProjectNameSDK/core\`)
38
+ The main client class (\`${SDK}\`, under \`Sources/${swiftTargetDir(model)}/core\`)
37
39
  exposes the entity accessors. Reference entity or utility types directly only
38
40
  when needed. The SDK is dependency-free: JSON parsing is the vendored
39
41
  \`Struct/JSON.swift\`, HTTP transport is Foundation's \`URLSession\`, and the
@@ -3,6 +3,8 @@ import type {
3
3
  ModelEntity
4
4
  } from '@voxgig/apidef'
5
5
 
6
+ import { swiftTargetDir, swiftTestDir } from './utility_swift'
7
+
6
8
  import { cmp, each, Folder, entityCollection } from '@voxgig/sdkgen'
7
9
 
8
10
 
@@ -20,7 +22,7 @@ const Test = cmp(function Test(props: any) {
20
22
  // templates in tm/swift/Tests. Only the API-specific entity/direct tests
21
23
  // are generated here.
22
24
  Folder({ name: 'Tests' }, () => {
23
- Folder({ name: 'ProjectNameSDKTests' }, () => {
25
+ Folder({ name: swiftTestDir(model) }, () => {
24
26
  const entity = each(entityCollection(model))
25
27
  .filter((e: any) => false !== e.active)
26
28
 
@@ -132,10 +132,26 @@ function clean(o: any, dropDefaults?: boolean): any {
132
132
  }
133
133
 
134
134
 
135
+ // SwiftPM's convention is Sources/<target>/ and Tests/<target>Tests/, and the
136
+ // target is named <Name>Sdk in Package_swift. These name the two directories
137
+ // so the copied runtime lands under the API's own name rather than under the
138
+ // template's placeholder — see the note in Main_swift.
139
+ function swiftTargetDir(model: any): string {
140
+ return model.const.Name + 'Sdk'
141
+ }
142
+
143
+
144
+ function swiftTestDir(model: any): string {
145
+ return swiftTargetDir(model) + 'Tests'
146
+ }
147
+
148
+
135
149
  export {
136
150
  clean,
137
151
  projectPath,
138
152
  swiftPascalName,
153
+ swiftTargetDir,
154
+ swiftTestDir,
139
155
  swiftVarName,
140
156
  swiftString,
141
157
  }
@@ -142,8 +142,11 @@ const Config = cmp(async function Config(props: any) {
142
142
  Line(` ${f.name}: ${nom(f, 'Name')}Feature,`)
143
143
  }),
144
144
 
145
+ // Rendered from configDefinition's def, not from f.config, so the
146
+ // literal carries the feature's `transport` role (station design
147
+ // §8.4) beside its options and cannot drift from the data rep.
145
148
  '// #FeatureConfigs': () => each(feature, (f: any) => {
146
- Line(` ${f.name}: ${formatJson(f.config, { margin: 4 })},`)
149
+ Line(` ${f.name}: ${formatJson(configDef.feature[f.name], { margin: 4 })},`)
147
150
  }),
148
151
 
149
152
 
@@ -4,7 +4,7 @@ import * as Path from 'node:path'
4
4
  import {
5
5
  cmp, each, names, cmap,
6
6
  List, File, Content, Copy, Folder, Fragment, Line, FeatureHook,
7
- entityClassName, entityCollection, srcFeatureExcludes,
7
+ entityClassName, entityCollection, srcFeatureExcludes, stationLibrary,
8
8
  } from '@voxgig/sdkgen'
9
9
 
10
10
 
@@ -106,6 +106,25 @@ if (fres instanceof Promise) { await fres }
106
106
  MainEntity(entprops)
107
107
  })
108
108
  })
109
+
110
+ // Station self-registration (station design §6.2 path 1;
111
+ // station-declarative-config §11 item 2): emitted ONLY when the model
112
+ // carries an ACTIVE station feature (installed via
113
+ // `package add @voxgig/sdkgen-station`). The library package name
114
+ // comes from the feature model's own `deps.ts` block — the same entry
115
+ // collectDeps flows into package.json — so the manifest dependency
116
+ // and the emitted require cannot disagree.
117
+ const stationPkg = stationLibrary(model, target.name)
118
+ if (null != stationPkg) {
119
+ Fragment({
120
+ from: Path.normalize(
121
+ __dirname + '/../../../src/cmp/ts/fragment/MainStation.fragment.ts'),
122
+ replace: {
123
+ ...props.ctx$.stdrep,
124
+ "'STATIONPKG'": JSON.stringify(stationPkg),
125
+ }
126
+ })
127
+ }
109
128
  })
110
129
 
111
130
  Config({ target })
@@ -0,0 +1,36 @@
1
+
2
+
3
+ // Station self-registration (voxgig/station design §6.2, path 1): linking
4
+ // this package is the whole bootstrap. When the station library is
5
+ // installed beside this SDK, the {construct, config} factory pair is
6
+ // registered under the api slug (config.main.slug, the descriptor slug) at
7
+ // module init, so `station.sdk('<name>')` needs no imports in application
8
+ // code. Registration is SOFT — the station library is a peer dependency of
9
+ // the station feature, and a project without it skips registration and
10
+ // gets exactly the SDK it always had. Only a genuine module-not-found is
11
+ // swallowed: a station that RESOLVES but throws while loading must
12
+ // propagate (a silently empty factory table has no visible cause), and so
13
+ // must station_factory_conflict — two different builds of one SDK in one
14
+ // process is not a thing to pick between quietly.
15
+ let stationInstalled = true
16
+ try { require.resolve('STATIONPKG') }
17
+ catch (err: any) {
18
+ // MODULE_NOT_FOUND is the absent case. Anything else - an invalid
19
+ // package config, a root entry the package does not export - is a
20
+ // real failure to load a station that IS installed, and swallowing
21
+ // it would leave an empty factory table with no visible cause.
22
+ if ('MODULE_NOT_FOUND' !== err?.code) { throw err }
23
+ stationInstalled = false
24
+ }
25
+
26
+ if (stationInstalled) {
27
+ const { provide } = require('STATIONPKG')
28
+ // A station library predating the factory table has no provide() —
29
+ // there is nothing to register with, which is not this SDK's error.
30
+ if ('function' === typeof provide) {
31
+ provide(config.main.slug, {
32
+ construct: (options?: any) => new ProjectNameSDK(options),
33
+ config,
34
+ })
35
+ }
36
+ }
@@ -38,8 +38,10 @@ const Config = cmp(async function Config(props: any) {
38
38
  // representations render from the same `def`, so they cannot describe
39
39
  // different configs - and this target picks up `options.server` (the
40
40
  // OpenAPI server-variable defaults), which the hand-rolled build here
41
- // omitted entirely.
42
- const { def: config, json: configJson } = configDefinition(model)
41
+ // omitted entirely. Passing target.name opts this target into the main
42
+ // slug/version/target identity fields (read by station's descriptor -
43
+ // see configDefinition).
44
+ const { def: config, json: configJson } = configDefinition(model, target.name)
43
45
  const asData = isConfigData(configJson, configReprSetting(model))
44
46
 
45
47
  File({ name: 'config.' + target.ext }, () => {
@@ -26,17 +26,74 @@ defmodule Voxgig.Struct do
26
26
  # ---------------------------------------------------------------------------
27
27
 
28
28
  @heap :vox_struct_heap
29
-
29
+ @heap_owner :vox_struct_heap_owner
30
+
31
+ # A named ETS table is owned by the process that created it and dies with
32
+ # that process. The values here are HANDLES into it - {:vmap, id} - and they
33
+ # are passed across process boundaries freely: ExUnit runs every test in its
34
+ # own process, and a generated SDK caches its config in :persistent_term,
35
+ # which outlives all of them. So the table has to outlive its creator, or a
36
+ # handle that is still very much alive resolves into a fresh empty table.
37
+ #
38
+ # Adding ensure_heap to hget stopped that raising ArgumentError, but a silent
39
+ # nil turned out to be worse than the raise: the generated config's staleness
40
+ # check (`usable?`) was written to read the raise as "rebuild me", so once the
41
+ # raise stopped, a dead handle was cached as good and clone/1 crashed further
42
+ # down with "Enumerable not implemented for nil of type Atom" - nothing in the
43
+ # message pointing at the heap.
44
+ #
45
+ # The table is therefore owned by a dedicated process that never exits, and
46
+ # every id stays valid for the life of the VM.
30
47
  defp ensure_heap do
31
48
  case :ets.whereis(@heap) do
32
- :undefined ->
49
+ :undefined -> start_heap()
50
+ _ -> :ok
51
+ end
52
+ end
53
+
54
+ defp start_heap do
55
+ caller = self()
56
+
57
+ spawn(fn ->
58
+ owner =
33
59
  try do
34
- :ets.new(@heap, [:set, :public, :named_table])
60
+ Process.register(self(), @heap_owner)
61
+ true
35
62
  rescue
36
- ArgumentError -> :ok
63
+ ArgumentError -> false
37
64
  end
38
65
 
39
- :ok
66
+ if owner do
67
+ case :ets.whereis(@heap) do
68
+ :undefined -> :ets.new(@heap, [:set, :public, :named_table])
69
+ _ -> :ok
70
+ end
71
+ end
72
+
73
+ send(caller, :vox_heap_ready)
74
+
75
+ # Hold the table for the life of the VM. This process does nothing else.
76
+ if owner, do: Process.sleep(:infinity)
77
+ end)
78
+
79
+ receive do
80
+ :vox_heap_ready -> :ok
81
+ after
82
+ 5_000 -> :ok
83
+ end
84
+
85
+ await_heap(1_000)
86
+ end
87
+
88
+ # The loser of a registration race reports ready before the winner has
89
+ # necessarily reached :ets.new, so wait on the table, not on the message.
90
+ defp await_heap(0), do: :ok
91
+
92
+ defp await_heap(n) do
93
+ case :ets.whereis(@heap) do
94
+ :undefined ->
95
+ Process.sleep(1)
96
+ await_heap(n - 1)
40
97
 
41
98
  _ ->
42
99
  :ok
@@ -50,13 +107,10 @@ defmodule Voxgig.Struct do
50
107
  id
51
108
  end
52
109
 
53
- # ensure_heap here too, not only in alloc. A named ETS table belongs to the
54
- # process that created it and dies with it and ExUnit runs each test in its
55
- # own process, so a table created by an alloc in one test is gone by the
56
- # next. hget then raised ArgumentError ("the table identifier does not refer
57
- # to an existing ETS table") from whichever unrelated test happened to run
58
- # second, which is why this surfaced as a pipeline/netsim failure rather than
59
- # as anything to do with the heap.
110
+ # ensure_heap here too, not only in alloc, so a read can never hit a missing
111
+ # table. With a permanent owner (above) that should not arise; before there
112
+ # was one, it raised ArgumentError from whichever unrelated test happened to
113
+ # run second.
60
114
  defp hget(id) do
61
115
  ensure_heap()
62
116
 
@@ -8,10 +8,30 @@ build:
8
8
  # tests pass, due to *MapRef/*ListRef cross-references in arena teardown. We
9
9
  # filter the output: if "N/N tests passed" with N==total, treat as success.
10
10
  # `--summary all` is required so the count is printed.
11
+ #
12
+ # A TEST THAT DID NOT COMPILE IS NOT IN THE TALLY. The tolerance above is for a
13
+ # crash AFTER the tests ran, and it must never cover a build that failed: a
14
+ # compile error means those test binaries never ran, so they never reach
15
+ # "total", and "60/66 passed; 6 skipped" reads as a clean 66/66 while five of
16
+ # thirteen build steps are red. That is exactly how a `.undef` tag that does
17
+ # not exist in JsonValue shipped in 4.2.2 and stayed green in every sweep
18
+ # afterwards. So: if any build step failed, this target fails, whatever the
19
+ # test counts say.
11
20
  test:
12
21
  @out=$$(zig build test --summary all 2>&1); code=$$?; \
13
22
  echo "$$out"; \
14
23
  if [ $$code -eq 0 ]; then exit 0; fi; \
24
+ steps=$$(echo "$$out" | grep -oE '[0-9]+/[0-9]+ steps succeeded' | tail -1); \
25
+ sdone=$$(echo "$$steps" | sed -E 's#([0-9]+)/([0-9]+).*#\1#'); \
26
+ stotal=$$(echo "$$steps" | sed -E 's#([0-9]+)/([0-9]+).*#\2#'); \
27
+ if [ -n "$$steps" ] && [ "$$sdone" != "$$stotal" ]; then \
28
+ echo "zig: $$steps — a build step failed, so the test tally is incomplete"; \
29
+ exit 1; \
30
+ fi; \
31
+ if echo "$$out" | grep -q 'error: the following command failed with .* compilation errors'; then \
32
+ echo "zig: compilation errors — not a teardown crash"; \
33
+ exit 1; \
34
+ fi; \
15
35
  summary=$$(echo "$$out" | grep -oE '[0-9]+/[0-9]+ tests passed(; [0-9]+ skipped)?' | tail -1); \
16
36
  passed=$$(echo "$$summary" | sed -E 's#([0-9]+)/([0-9]+).*#\1#'); \
17
37
  total=$$(echo "$$summary" | sed -E 's#([0-9]+)/([0-9]+).*#\2#'); \
@@ -105,7 +105,7 @@ fn fixIds(_: Allocator, key: ?[]const u8, val: Value, _: Value, path: []const []
105
105
  // while the fixture plainly held two. Mirrors the go/ts/lua/php mocks, which
106
106
  // already wrap; rust, c and zig were the three that did not.
107
107
  fn envelope(ctx: *Context, data: Value) Value {
108
- if (data == .undef or data == .null) return data;
108
+ if (data == .null) return data;
109
109
  const restf = h.getpath(&.{ "transform", "res" }, ctx.point);
110
110
  if (restf != .string) return data;
111
111
  const spec = restf.string;
@@ -3,7 +3,7 @@
3
3
  "package": 1
4
4
  },
5
5
  "name": "@voxgig/sdkgen",
6
- "version": "4.2.3",
6
+ "version": "4.2.5",
7
7
  "provides": {
8
8
  "target": [
9
9
  "c",