@voxgig/sdkgen 4.6.0 → 4.8.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 (144) hide show
  1. package/bin/voxgig-sdkgen +1 -1
  2. package/dist/cmp/Feature.js +5 -0
  3. package/dist/cmp/Feature.js.map +1 -1
  4. package/dist/cmp/TestControl.d.ts +4 -0
  5. package/dist/cmp/TestControl.js +51 -0
  6. package/dist/cmp/TestControl.js.map +1 -0
  7. package/dist/helpers/featureSource.d.ts +3 -1
  8. package/dist/helpers/featureSource.js +105 -0
  9. package/dist/helpers/featureSource.js.map +1 -1
  10. package/dist/helpers/opShape.js +8 -11
  11. package/dist/helpers/opShape.js.map +1 -1
  12. package/dist/helpers/pointPath.d.ts +10 -0
  13. package/dist/helpers/pointPath.js +54 -0
  14. package/dist/helpers/pointPath.js.map +1 -0
  15. package/dist/sdkgen.d.ts +6 -3
  16. package/dist/sdkgen.js +14 -4
  17. package/dist/sdkgen.js.map +1 -1
  18. package/dist/tsconfig.tsbuildinfo +1 -1
  19. package/dist/utility.d.ts +2 -1
  20. package/dist/utility.js +78 -1
  21. package/dist/utility.js.map +1 -1
  22. package/model/sdkgen.aon +58 -0
  23. package/package.json +2 -2
  24. package/project/.sdk/model/feature/secrets.aon +82 -0
  25. package/project/.sdk/src/cmp/c/Config_c.ts +7 -8
  26. package/project/.sdk/src/cmp/c/TestDirect_c.ts +4 -3
  27. package/project/.sdk/src/cmp/cpp/Main_cpp.ts +2 -1
  28. package/project/.sdk/src/cmp/cpp/TestDirect_cpp.ts +4 -3
  29. package/project/.sdk/src/cmp/cpp/Test_cpp.ts +5 -1
  30. package/project/.sdk/src/cmp/csharp/Main_csharp.ts +2 -1
  31. package/project/.sdk/src/cmp/csharp/TestDirect_csharp.ts +34 -9
  32. package/project/.sdk/src/cmp/csharp/TestEntity_csharp.ts +29 -5
  33. package/project/.sdk/src/cmp/csharp/Test_csharp.ts +5 -1
  34. package/project/.sdk/src/cmp/dart/Config_dart.ts +7 -8
  35. package/project/.sdk/src/cmp/dart/Main_dart.ts +2 -1
  36. package/project/.sdk/src/cmp/dart/TestDirect_dart.ts +28 -10
  37. package/project/.sdk/src/cmp/dart/TestEntity_dart.ts +28 -7
  38. package/project/.sdk/src/cmp/dart/Test_dart.ts +5 -1
  39. package/project/.sdk/src/cmp/elixir/Config_elixir.ts +9 -8
  40. package/project/.sdk/src/cmp/go/Config_go.ts +1 -7
  41. package/project/.sdk/src/cmp/go/Main_go.ts +2 -1
  42. package/project/.sdk/src/cmp/go/TestDirect_go.ts +5 -4
  43. package/project/.sdk/src/cmp/go/Test_go.ts +5 -1
  44. package/project/.sdk/src/cmp/java/Config_java.ts +7 -7
  45. package/project/.sdk/src/cmp/java/Main_java.ts +2 -1
  46. package/project/.sdk/src/cmp/java/TestDirect_java.ts +5 -4
  47. package/project/.sdk/src/cmp/java/Test_java.ts +5 -1
  48. package/project/.sdk/src/cmp/js/Config_js.ts +7 -8
  49. package/project/.sdk/src/cmp/js/Main_js.ts +2 -1
  50. package/project/.sdk/src/cmp/js/TestDirect_js.ts +34 -8
  51. package/project/.sdk/src/cmp/js/TestEntity_js.ts +35 -5
  52. package/project/.sdk/src/cmp/js/Test_js.ts +5 -1
  53. package/project/.sdk/src/cmp/js/fragment/Direct.test.fragment.js +7 -1
  54. package/project/.sdk/src/cmp/js/fragment/Entity.test.fragment.js +7 -1
  55. package/project/.sdk/src/cmp/kotlin/Config_kotlin.ts +7 -7
  56. package/project/.sdk/src/cmp/kotlin/Main_kotlin.ts +2 -1
  57. package/project/.sdk/src/cmp/kotlin/TestDirect_kotlin.ts +5 -4
  58. package/project/.sdk/src/cmp/kotlin/Test_kotlin.ts +5 -1
  59. package/project/.sdk/src/cmp/lean/Config_lean.ts +9 -8
  60. package/project/.sdk/src/cmp/lua/Config_lua.ts +1 -7
  61. package/project/.sdk/src/cmp/lua/Main_lua.ts +2 -1
  62. package/project/.sdk/src/cmp/lua/TestDirect_lua.ts +32 -7
  63. package/project/.sdk/src/cmp/lua/TestEntity_lua.ts +24 -4
  64. package/project/.sdk/src/cmp/lua/Test_lua.ts +5 -1
  65. package/project/.sdk/src/cmp/ocaml/Gitignore_ocaml.ts +1 -0
  66. package/project/.sdk/src/cmp/perl/Main_perl.ts +2 -1
  67. package/project/.sdk/src/cmp/perl/TestDirect_perl.ts +29 -7
  68. package/project/.sdk/src/cmp/perl/TestEntity_perl.ts +24 -4
  69. package/project/.sdk/src/cmp/perl/Test_perl.ts +5 -1
  70. package/project/.sdk/src/cmp/perl/utility_perl.ts +10 -0
  71. package/project/.sdk/src/cmp/php/Config_php.ts +1 -7
  72. package/project/.sdk/src/cmp/php/Main_php.ts +2 -1
  73. package/project/.sdk/src/cmp/php/TestDirect_php.ts +28 -8
  74. package/project/.sdk/src/cmp/php/TestEntity_php.ts +28 -4
  75. package/project/.sdk/src/cmp/php/Test_php.ts +5 -1
  76. package/project/.sdk/src/cmp/py/Config_py.ts +1 -7
  77. package/project/.sdk/src/cmp/py/Main_py.ts +2 -1
  78. package/project/.sdk/src/cmp/py/TestDirect_py.ts +5 -4
  79. package/project/.sdk/src/cmp/py/Test_py.ts +5 -1
  80. package/project/.sdk/src/cmp/rb/Config_rb.ts +1 -7
  81. package/project/.sdk/src/cmp/rb/Main_rb.ts +2 -1
  82. package/project/.sdk/src/cmp/rb/TestDirect_rb.ts +28 -8
  83. package/project/.sdk/src/cmp/rb/TestEntity_rb.ts +24 -4
  84. package/project/.sdk/src/cmp/rb/Test_rb.ts +5 -1
  85. package/project/.sdk/src/cmp/rb/utility_rb.ts +3 -1
  86. package/project/.sdk/src/cmp/rust/Main_rust.ts +2 -1
  87. package/project/.sdk/src/cmp/rust/TestDirect_rust.ts +35 -7
  88. package/project/.sdk/src/cmp/rust/TestEntity_rust.ts +37 -4
  89. package/project/.sdk/src/cmp/rust/Test_rust.ts +5 -1
  90. package/project/.sdk/src/cmp/scala/Config_scala.ts +9 -7
  91. package/project/.sdk/src/cmp/scala/Main_scala.ts +2 -1
  92. package/project/.sdk/src/cmp/scala/TestDirect_scala.ts +4 -3
  93. package/project/.sdk/src/cmp/scala/Test_scala.ts +5 -1
  94. package/project/.sdk/src/cmp/seneca-provider/Extras_seneca-provider.ts +3 -1
  95. package/project/.sdk/src/cmp/seneca-provider/Main_seneca-provider.ts +9 -5
  96. package/project/.sdk/src/cmp/ts/Config_ts.ts +57 -16
  97. package/project/.sdk/src/cmp/ts/Main_ts.ts +10 -2
  98. package/project/.sdk/src/cmp/ts/TestDirect_ts.ts +15 -7
  99. package/project/.sdk/src/cmp/ts/TestEntity_ts.ts +11 -3
  100. package/project/.sdk/src/cmp/ts/Test_ts.ts +5 -1
  101. package/project/.sdk/tm/csharp/test/Runner.cs +45 -0
  102. package/project/.sdk/tm/dart/test/utility.dart +39 -0
  103. package/project/.sdk/tm/js/test/sdk-test-control.json +19 -0
  104. package/project/.sdk/tm/js/test/utility.js +91 -1
  105. package/project/.sdk/tm/lua/test/runner.lua +40 -0
  106. package/project/.sdk/tm/ocaml/test/harness.ml +14 -4
  107. package/project/.sdk/tm/perl/t/runner.pm +30 -0
  108. package/project/.sdk/tm/php/test/Runner.php +36 -0
  109. package/project/.sdk/tm/rb/test/runner.rb +25 -0
  110. package/project/.sdk/tm/rust/tests/common/mod.rs +32 -0
  111. package/project/.sdk/tm/rust/utility/make_options.rs +1 -1
  112. package/project/.sdk/tm/ts/src/feature/secrets/sekreto/Sekreto.ts +8 -2
  113. package/project/.sdk/tm/ts/src/feature/secrets/sekreto/Sigv4.ts +29 -5
  114. package/project/.sdk/tm/ts/src/feature/secrets/sekreto/index.ts +30 -21
  115. package/project/.sdk/tm/ts/src/feature/secrets/sekreto/provider/Registry.ts +90 -0
  116. package/project/.sdk/tm/ts/src/feature/secrets/sekreto/provider/addr.ts +48 -0
  117. package/project/.sdk/tm/ts/src/feature/secrets/sekreto/provider/aws.ts +219 -0
  118. package/project/.sdk/tm/ts/src/feature/secrets/sekreto/provider/azuresecrets.ts +144 -0
  119. package/project/.sdk/tm/ts/src/feature/secrets/sekreto/provider/boru.ts +113 -0
  120. package/project/.sdk/tm/ts/src/feature/secrets/sekreto/provider/doppler.ts +77 -0
  121. package/project/.sdk/tm/ts/src/feature/secrets/sekreto/provider/dotenv.ts +51 -0
  122. package/project/.sdk/tm/ts/src/feature/secrets/sekreto/provider/env.ts +31 -0
  123. package/project/.sdk/tm/ts/src/feature/secrets/sekreto/provider/file.ts +54 -0
  124. package/project/.sdk/tm/ts/src/feature/secrets/sekreto/provider/gcpsecrets.ts +118 -0
  125. package/project/.sdk/tm/ts/src/feature/secrets/sekreto/provider/hashicorp.ts +159 -0
  126. package/project/.sdk/tm/ts/src/feature/secrets/sekreto/provider/http.ts +66 -0
  127. package/project/.sdk/tm/ts/src/feature/secrets/sekreto/provider/infisical.ts +107 -0
  128. package/project/.sdk/tm/ts/src/feature/secrets/sekreto/provider/memory.ts +33 -0
  129. package/project/.sdk/tm/ts/src/feature/secrets/sekreto/provider/onepassword.ts +120 -0
  130. package/project/.sdk/tm/ts/src/feature/secrets/sekreto/provider/support.ts +176 -0
  131. package/project/.sdk/tm/ts/test/vendor/omni/Runner.ts +15 -13
  132. package/project/.sdk/tm/ts/test/vendor/omni/Util.ts +25 -19
  133. package/project/.sdk/tm/ts/test/vendor/omni/compat.ts +1 -1
  134. package/project/.sdk/tm/ts/test/vendor/omni/index.ts +1 -1
  135. package/project/.sdk/tm/zig/core/utility.zig +1 -1
  136. package/project/sdkgen-package.json +1 -1
  137. package/src/cmp/Feature.ts +5 -0
  138. package/src/cmp/TestControl.ts +64 -0
  139. package/src/helpers/featureSource.ts +130 -0
  140. package/src/helpers/opShape.ts +9 -14
  141. package/src/helpers/pointPath.ts +88 -0
  142. package/src/sdkgen.ts +18 -0
  143. package/src/utility.ts +92 -1
  144. package/project/.sdk/tm/ts/src/feature/secrets/sekreto/Providers.ts +0 -1221
@@ -11,7 +11,10 @@ import {
11
11
  File,
12
12
  cmp,
13
13
  snakify,
14
- isAuthActive, envName, envToken
14
+ isAuthActive, envName, envToken,
15
+ serverVarEnv,
16
+ serverVariables,
17
+ pointParts,
15
18
  } from '@voxgig/sdkgen'
16
19
 
17
20
 
@@ -67,12 +70,28 @@ const TestDirect = cmp(function TestDirect(props: any) {
67
70
 
68
71
  const authActive = isAuthActive(model)
69
72
  const apikeyEnvEntry = authActive
70
- ? `\n ("${PROJECTNAME}_APIKEY", Value::str("NONE")),`
73
+ ? `\n ("${PROJECTNAME}_APIKEY", Value::str("")),`
71
74
  : ''
72
75
  const apikeyLiveField = authActive
73
76
  ? `("apikey", getp(&env, "${PROJECTNAME}_APIKEY"))`
74
77
  : ''
75
78
 
79
+ // A templated server URL (OpenAPI server variables) makes a LIVE client
80
+ // impossible to construct without values: makeOptions raises rather than
81
+ // request a URL with a literal `{account_id}` in it. So the live suite
82
+ // takes them from the environment the same way it takes the apikey.
83
+ //
84
+ // Emitted as a jo() tuple entry, so it carries its own leading comma only
85
+ // when an apikey entry precedes it - otherwise `jo(vec![, ...])`.
86
+ const svars = serverVariables(model)
87
+ const serverEnvEntry = svars
88
+ .map((v: any) => `\n ("${serverVarEnv(PROJECTNAME, v.name)}", Value::str(${JSON.stringify(v.dflt)})),`).join('')
89
+ const serverLiveEntry = 0 === svars.length ? '' :
90
+ `("server", jo(vec![${svars
91
+ .map((v: any) => `("${v.name}", getp(&env, "${serverVarEnv(PROJECTNAME, v.name)}"))`).join(', ')}]))`
92
+ const serverLiveField = '' === serverLiveEntry ? '' :
93
+ ('' === apikeyLiveField ? serverLiveEntry : ', ' + serverLiveEntry)
94
+
76
95
  const opnames = Object.keys(entity.op || {})
77
96
  const hasLoad = opnames.includes('load')
78
97
  const hasList = opnames.includes('list')
@@ -86,12 +105,12 @@ const TestDirect = cmp(function TestDirect(props: any) {
86
105
 
87
106
  // Load point info.
88
107
  const loadPoint = loadOp?.points?.[0]
89
- const loadPath = loadPoint ? normalizePathParams(loadPoint.parts || [], loadPoint?.args?.params || [], loadPoint?.rename?.param) : ''
108
+ const loadPath = loadPoint ? normalizePathParams(pointParts(loadPoint), loadPoint?.args?.params || [], loadPoint?.rename?.param) : ''
90
109
  const allLoadParams = loadPoint?.args?.params || []
91
110
  // Only path params that actually appear in the URL template drive the
92
111
  // direct-test path-param setup and URL-substitution asserts.
93
112
  const _pathPlaceholders = new Set<string>()
94
- for (const part of (loadPoint?.parts || [])) {
113
+ for (const part of pointParts(loadPoint)) {
95
114
  if (typeof part === 'string' && part.startsWith('{') && part.endsWith('}')) {
96
115
  _pathPlaceholders.add(part.slice(1, -1))
97
116
  }
@@ -109,7 +128,7 @@ const TestDirect = cmp(function TestDirect(props: any) {
109
128
 
110
129
  // List point info.
111
130
  const listPoint = listOp?.points?.[0]
112
- const listPath = listPoint ? normalizePathParams(listPoint.parts || [], listPoint?.args?.params || [], listPoint?.rename?.param) : ''
131
+ const listPath = listPoint ? normalizePathParams(pointParts(listPoint), listPoint?.args?.params || [], listPoint?.rename?.param) : ''
113
132
  const listParams = listPoint?.args?.params || []
114
133
 
115
134
  const entidEnvVar = `${PROJECTNAME}_TEST_${envToken(entity.name)}_ENTID`
@@ -148,13 +167,22 @@ fn ${evar}_direct_setup(mockres: Value) -> ${entity.Name}DirectSetup {
148
167
 
149
168
  let env = env_override(jo(vec![
150
169
  ("${entidEnvVar}", Value::empty_map()),
151
- ("${PROJECTNAME}_TEST_LIVE", Value::str("FALSE")),${apikeyEnvEntry}
170
+ ("${PROJECTNAME}_TEST_LIVE", Value::str("FALSE")),${apikeyEnvEntry}${serverEnvEntry}
152
171
  ]));
153
172
 
154
173
  let live = getp(&env, "${PROJECTNAME}_TEST_LIVE") == Value::str("TRUE");
155
174
 
156
175
  if live {
157
- let client = ${model.const.Name}SDK::new(jo(vec![${apikeyLiveField}]));
176
+ // live_client_options() FIRST, so the generated entries below win:
177
+ // sdk-test-control.json's test.client.options adds to the live
178
+ // client, it does not redirect it.
179
+ let client = ${model.const.Name}SDK::new(to_map(&vs::merge(
180
+ &ja(vec![
181
+ live_client_options(),
182
+ jo(vec![${apikeyLiveField}${serverLiveField}]),
183
+ ]),
184
+ None,
185
+ )));
158
186
  let idmap = match to_map(&getp(&env, "${entidEnvVar}")) {
159
187
  Value::Map(m) => Value::Map(m),
160
188
  _ => Value::empty_map(),
@@ -17,7 +17,9 @@ import {
17
17
  each,
18
18
  buildIdNames,
19
19
  getMatchEntries,
20
- isAuthActive, envName, envToken
20
+ isAuthActive, envName, envToken,
21
+ serverVarEnv,
22
+ serverVariables
21
23
  } from '@voxgig/sdkgen'
22
24
 
23
25
 
@@ -56,12 +58,28 @@ const TestEntity = cmp(function TestEntity(props: any) {
56
58
 
57
59
  const authActive = isAuthActive(model)
58
60
  const apikeyEnvEntry = authActive
59
- ? `\n ("${PROJUPPER}_APIKEY", Value::str("NONE")),`
61
+ ? `\n ("${PROJUPPER}_APIKEY", Value::str("")),`
60
62
  : ''
61
63
  const apikeyLiveField = authActive
62
64
  ? `("apikey", getp(&env, "${PROJUPPER}_APIKEY"))`
63
65
  : ''
64
66
 
67
+ // A templated server URL (OpenAPI server variables) makes a LIVE client
68
+ // impossible to construct without values: makeOptions raises rather than
69
+ // request a URL with a literal `{account_id}` in it. So the live suite
70
+ // takes them from the environment the same way it takes the apikey.
71
+ //
72
+ // Emitted as a jo() tuple entry, so it carries its own leading comma only
73
+ // when an apikey entry precedes it - otherwise `jo(vec![, ...])`.
74
+ const svars = serverVariables(model)
75
+ const serverEnvEntry = svars
76
+ .map((v: any) => `\n ("${serverVarEnv(PROJUPPER, v.name)}", Value::str(${JSON.stringify(v.dflt)})),`).join('')
77
+ const serverLiveEntry = 0 === svars.length ? '' :
78
+ `("server", jo(vec![${svars
79
+ .map((v: any) => `("${v.name}", getp(&env, "${serverVarEnv(PROJUPPER, v.name)}"))`).join(', ')}]))`
80
+ const serverLiveField = '' === serverLiveEntry ? '' :
81
+ ('' === apikeyLiveField ? serverLiveEntry : ', ' + serverLiveEntry)
82
+
65
83
  const idnames = buildIdNames(entity, basicflow)
66
84
  const idnamesStr = idnames.map(n => `Value::str("${n}")`).join(', ')
67
85
 
@@ -268,7 +286,7 @@ ${allSteps.length > 0 ? ' let client = setup.client.clone();\n' : ''}`)
268
286
  let env = env_override(jo(vec![
269
287
  ("${PROJUPPER}_TEST_${ENTUPPER}_ENTID", idmap.clone()),
270
288
  ("${PROJUPPER}_TEST_LIVE", Value::str("FALSE")),
271
- ("${PROJUPPER}_TEST_EXPLAIN", Value::str("FALSE")),${apikeyEnvEntry}
289
+ ("${PROJUPPER}_TEST_EXPLAIN", Value::str("FALSE")),${apikeyEnvEntry}${serverEnvEntry}
272
290
  ]));
273
291
 
274
292
  let idmap_resolved = match to_map(&getp(&env, "${PROJUPPER}_TEST_${ENTUPPER}_ENTID")) {
@@ -293,7 +311,22 @@ ${allSteps.length > 0 ? ' let client = setup.client.clone();\n' : ''}`)
293
311
 
294
312
  let client = if live {
295
313
  let merged = vs::merge(
296
- &ja(vec![jo(vec![${apikeyLiveField}]), extra]),
314
+ // live_client_options() FIRST, so the generated entries below win:
315
+ // sdk-test-control.json's test.client.options adds to the live
316
+ // client, it does not redirect it.
317
+ &ja(vec![
318
+ live_client_options(),
319
+ jo(vec![${apikeyLiveField}${serverLiveField}]),
320
+ // A NON-NODE later entry REPLACES the accumulated map in
321
+ // vs::merge, and the normal call passes Value::Noval - so a
322
+ // a bare extra discarded live_client_options() and the
323
+ // apikey/server map above it, and the live client was
324
+ // constructed with nothing.
325
+ match extra {
326
+ Value::Map(m) => Value::Map(m),
327
+ _ => Value::empty_map(),
328
+ },
329
+ ]),
297
330
  None,
298
331
  );
299
332
  ${model.const.Name}SDK::new(to_map(&merged))
@@ -3,7 +3,8 @@ import type {
3
3
  ModelEntity
4
4
  } from '@voxgig/apidef'
5
5
 
6
- import { cmp, each, Folder, File, Content, entityCollection } from '@voxgig/sdkgen'
6
+ import { cmp, each, Folder, File, Content, entityCollection,
7
+ TestControl } from '@voxgig/sdkgen'
7
8
 
8
9
 
9
10
  import { TestEntity } from './TestEntity_rust'
@@ -20,6 +21,9 @@ const Test = cmp(function Test(props: any) {
20
21
 
21
22
  Folder({ name: 'tests' }, () => {
22
23
 
24
+ // Write-once: a project's edited control file survives regeneration.
25
+ TestControl({ target, dir: 'tests' })
26
+
23
27
  // exists test — the SDK constructs in test mode.
24
28
  File({ name: 'exists_test.' + target.ext }, () => {
25
29
  Content(`// Generated existence test: the SDK constructs in test mode.
@@ -7,6 +7,7 @@ import {
7
7
  isAuthActive,
8
8
  resolveAuthPrefix,
9
9
  targetFeatures,
10
+ configDefinition,
10
11
  } from '@voxgig/sdkgen'
11
12
 
12
13
 
@@ -36,6 +37,7 @@ const Config = cmp(async function Config(props: any) {
36
37
  const scalapackage = scalaPackage(model)
37
38
 
38
39
  const entity = getModelPath(model, `main.${KIT}.entity`)
40
+ const { def: configDef } = configDefinition(model, target.name)
39
41
  // Gated by the applicability tags, so this target never imports or
40
42
  // registers a feature it has no source for. One rule, one place:
41
43
  // helpers/applicability.
@@ -68,13 +70,13 @@ const Config = cmp(async function Config(props: any) {
68
70
  options.headers = headers
69
71
  options.entity = optionsEntity
70
72
 
71
- const entityConfig = Object.values(entity).reduce((a: any, n: any) => (
72
- a[n.name] = cleanModel({
73
- fields: n.fields,
74
- name: n.name,
75
- op: n.op,
76
- relations: n.relations,
77
- }, true), a), {})
73
+ // configDefinition's `def.entity` verbatim, NOT rebuilt here. This reduce
74
+ // was one of fourteen copies of that function's entityDefs loop, and when
75
+ // configDefinition started reconstructing a point's `parts` from apidef's
76
+ // segment vector (its ADR-003), only the copies that read `configDef` got
77
+ // it — this target's literal config emitted paths with no parts at all
78
+ // while its data config had them. One rule, one place.
79
+ const entityConfig = configDef.entity
78
80
 
79
81
  const config = {
80
82
  main: { name: model.const.Name },
@@ -5,6 +5,7 @@ import {
5
5
  cmp, each,
6
6
  File, Content, Copy, Folder, Fragment,
7
7
  targetFeatures,
8
+ TEST_CONTROL_EXCLUDE
8
9
  } from '@voxgig/sdkgen'
9
10
 
10
11
 
@@ -51,7 +52,7 @@ const Main = cmp(async function Main(props: any) {
51
52
  // ProjectName carries the SDK name into vendored template strings.
52
53
  Copy({
53
54
  from: 'tm/' + target.name,
54
- exclude: [/src\//],
55
+ exclude: [/src\//, TEST_CONTROL_EXCLUDE],
55
56
  replace: {
56
57
  ...props.ctx$.stdrep,
57
58
  ProjectName: model.const.Name,
@@ -11,6 +11,7 @@ import {
11
11
  File,
12
12
  cmp,
13
13
  snakify,
14
+ pointParts,
14
15
  } from '@voxgig/sdkgen'
15
16
 
16
17
 
@@ -79,13 +80,13 @@ const TestDirect = cmp(function TestDirect(props: any) {
79
80
  // Load point info.
80
81
  const loadPoint = loadOp?.points?.[0]
81
82
  const loadPath = loadPoint
82
- ? normalizePathParams(loadPoint.parts || [], loadPoint?.args?.params || [], loadPoint?.rename?.param)
83
+ ? normalizePathParams(pointParts(loadPoint), loadPoint?.args?.params || [], loadPoint?.rename?.param)
83
84
  : ''
84
85
  const allLoadParams = loadPoint?.args?.params || []
85
86
  // Only path params that actually appear in the URL template drive the
86
87
  // direct-test path-param setup and URL-substitution asserts.
87
88
  const _pathPlaceholders = new Set<string>()
88
- for (const part of (loadPoint?.parts || [])) {
89
+ for (const part of pointParts(loadPoint)) {
89
90
  if (typeof part === 'string' && part.startsWith('{') && part.endsWith('}')) {
90
91
  _pathPlaceholders.add(part.slice(1, -1))
91
92
  }
@@ -104,7 +105,7 @@ const TestDirect = cmp(function TestDirect(props: any) {
104
105
  // List point info.
105
106
  const listPoint = listOp?.points?.[0]
106
107
  const listPath = listPoint
107
- ? normalizePathParams(listPoint.parts || [], listPoint?.args?.params || [], listPoint?.rename?.param)
108
+ ? normalizePathParams(pointParts(listPoint), listPoint?.args?.params || [], listPoint?.rename?.param)
108
109
  : ''
109
110
  const listParams = listPoint?.args?.params || []
110
111
 
@@ -9,7 +9,8 @@ import type {
9
9
  ModelEntity
10
10
  } from '@voxgig/apidef'
11
11
 
12
- import { cmp, each, Folder, File, Content, entityCollection } from '@voxgig/sdkgen'
12
+ import { cmp, each, Folder, File, Content, entityCollection,
13
+ TestControl } from '@voxgig/sdkgen'
13
14
 
14
15
 
15
16
  import { TestEntity } from './TestEntity_scala'
@@ -31,6 +32,9 @@ const Test = cmp(function Test(props: any) {
31
32
  // template, and are driven by the generated SdkEntityTestMain aggregator.
32
33
  Folder({ name: 'sdktest' }, () => {
33
34
 
35
+ // Write-once: a project's edited control file survives regeneration.
36
+ TestControl({ target, dir: 'sdktest' })
37
+
34
38
  const entities: { Name: string; entity: boolean; direct: boolean }[] = []
35
39
 
36
40
  const entity = each(entityCollection(model))
@@ -2,6 +2,7 @@ import {
2
2
  cmp, each,
3
3
  File, Content, Folder,
4
4
  jsKey, jsProp,
5
+ pointSegments,
5
6
  } from '@voxgig/sdkgen'
6
7
 
7
8
 
@@ -22,7 +23,8 @@ import {
22
23
  function loadHasKey(ent: any): boolean {
23
24
  const point = (ent.op && ent.op.load && ent.op.load.points || [])[0]
24
25
  if (null == point) return false
25
- const hasPathParam = (point.parts || []).some((p: string) => p.startsWith('{'))
26
+ // apidef states which segments are variables (its ADR-003) no brace test.
27
+ const hasPathParam = pointSegments(point).some((seg: any) => null != seg.var)
26
28
  const hasQueryParam = (point.args && point.args.query || [])
27
29
  .some((q: any) => false !== q.reqd)
28
30
  return hasPathParam || hasQueryParam
@@ -7,6 +7,7 @@ import {
7
7
  authorInfo, contributorList, isAuthActive, isHttpBasicAuth, jsKey, jsProp,
8
8
  SdkGenError,
9
9
  PUBLISHER, PUBLISHER_URL,
10
+ pointSegments,
10
11
  } from '@voxgig/sdkgen'
11
12
 
12
13
  import {
@@ -102,11 +103,14 @@ function recordKey(ent: any): string {
102
103
  const canonical = op.points.filter((pt: any) =>
103
104
  null == (pt && pt.select && pt.select['$action']))
104
105
  const point = ownPoint(0 < canonical.length ? canonical : op.points)
105
- const parts: string[] = (point && point.parts) || []
106
- const lastParam = [...parts].reverse().find((p: string) => p.startsWith('{'))
107
-
108
- if (null != lastParam) {
109
- return lastParam.slice(1, -1)
106
+ // The LAST variable segment names the record's key. apidef states which
107
+ // segments are variables (its ADR-003), so this reads the name off the
108
+ // vector rather than finding a `{` and slicing the braces back off.
109
+ const vars = pointSegments(point)
110
+ .filter((seg: any) => null != seg.var)
111
+
112
+ if (0 < vars.length) {
113
+ return String(vars[vars.length - 1].var)
110
114
  }
111
115
 
112
116
  // No path param at all (e.g. GET /scan/async/result?trace_id=...): the
@@ -8,7 +8,6 @@ import {
8
8
  Fragment,
9
9
  Line,
10
10
  cmp,
11
- clean,
12
11
  configDefinition,
13
12
  configReprSetting,
14
13
  each,
@@ -99,10 +98,13 @@ const Config = cmp(async function Config(props: any) {
99
98
 
100
99
  replace: {
101
100
 
102
- '// #ImportFeatures': () => each(feature, (f: any) => {
103
- Line(`import { ${nom(f, 'Name')}Feature } from ` +
104
- `'./feature/${f.name}/${nom(f, 'Name')}Feature'`)
105
- }),
101
+ '// #ImportFeatures': () => {
102
+ each(feature, (f: any) => {
103
+ Line(`import { ${nom(f, 'Name')}Feature } from ` +
104
+ `'./feature/${f.name}/${nom(f, 'Name')}Feature'`)
105
+ })
106
+ pluginImports(feature)
107
+ },
106
108
 
107
109
  '// #FeatureClasses': () => each(feature, (f: any) => {
108
110
  Line(` ${f.name}: ${nom(f, 'Name')}Feature,`)
@@ -130,10 +132,13 @@ const Config = cmp(async function Config(props: any) {
130
132
 
131
133
  "'HEADERS'": indent(JSON.stringify(headers, null, 2), 4).trim(),
132
134
 
133
- '// #ImportFeatures': () => each(feature, (f: any) => {
134
- Line(`import { ${nom(f, 'Name')}Feature } from ` +
135
- `'./feature/${f.name}/${nom(f, 'Name')}Feature'`)
136
- }),
135
+ '// #ImportFeatures': () => {
136
+ each(feature, (f: any) => {
137
+ Line(`import { ${nom(f, 'Name')}Feature } from ` +
138
+ `'./feature/${f.name}/${nom(f, 'Name')}Feature'`)
139
+ })
140
+ pluginImports(feature)
141
+ },
137
142
 
138
143
  // Values from configDefinition's def, not re-derived here, so the
139
144
  // literal rep and the data rep cannot disagree on identity.
@@ -164,19 +169,55 @@ const Config = cmp(async function Config(props: any) {
164
169
  `)
165
170
  }),
166
171
 
167
- "'ENTITYMAP'": formatJson(Object.values(entity)
168
- .reduce((a: any, n: any) => (a[n.name] = clean({
169
- fields: n.fields,
170
- name: n.name,
171
- op: n.op,
172
- relations: n.relations,
173
- }, true), a), {}), { margin: 2 }).trim(),
172
+ // configDefinition's `def.entity` verbatim, NOT rebuilt here. This
173
+ // reduce was a second copy of that function's entityDefs loop, and
174
+ // when configDefinition started reconstructing a point's `parts`
175
+ // from apidef's segment vector (its ADR-003), only the data
176
+ // representation got it — the literal one emitted empty paths. The
177
+ // config-repr equivalence test caught it, which is what it is for.
178
+ "'ENTITYMAP'": formatJson(configDef.entity, { margin: 2 }).trim(),
174
179
  }
175
180
  })
176
181
  })
177
182
  })
178
183
 
179
184
 
185
+
186
+ // PLUGIN REGISTRATION IMPORTS.
187
+ //
188
+ // A sekreto provider module registers itself when it is imported, and
189
+ // nothing else imports it: the core surface deliberately does not reach a
190
+ // platform-dependent provider, and the full-set barrel — the file whose
191
+ // job was to reach all of them — is not vendored into an SDK at all.
192
+ //
193
+ // So without these lines an SDK generates `provider/dotenv.ts`, never
194
+ // imports it, and throws `unknown provider kind: dotenv` the first time a
195
+ // chain names it. Emitted here because Config already imports every active
196
+ // feature from the model, and this is the same list one level down.
197
+ //
198
+ // Import for SIDE EFFECT only — `import '...'`, no bindings. The module's
199
+ // top-level `register()` call is the whole point.
200
+ function pluginImports(feature: any) {
201
+ each(feature, (f: any) => {
202
+ each(f.plugin, (plugin: any) => {
203
+ // Filter on `active` HERE rather than trusting the feature object to
204
+ // arrive filtered. Whether a model path was read with `only_active`
205
+ // varies by call site, and getting it wrong in this direction emits
206
+ // an import for a module the trim just deleted — an SDK that does
207
+ // not compile, rather than one that merely carries too much.
208
+ if (false === plugin.active || null == plugin.active) return
209
+
210
+ for (const one of (plugin.path || [])) {
211
+ const path = String(one)
212
+ // Only a provider MODULE registers a kind; a plugin may own other
213
+ // files (Sigv4.ts is signing support, not a provider).
214
+ if (!/\/provider\/[^/]+\.ts$/.test(path)) continue
215
+ Line(`import '${'./' + path.replace(/^src\//, '').replace(/\.ts$/, '')}'`)
216
+ }
217
+ })
218
+ })
219
+ }
220
+
180
221
  export {
181
222
  Config
182
223
  }
@@ -4,8 +4,10 @@ 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, stationLibrary,
7
+ entityClassName, entityCollection, srcFeatureExcludes, pluginExcludes,
8
+ stationLibrary,
8
9
  targetFeatures,
10
+ TEST_CONTROL_EXCLUDE
9
11
  } from '@voxgig/sdkgen'
10
12
 
11
13
 
@@ -53,7 +55,13 @@ const Main = cmp(async function Main(props: any) {
53
55
  from: 'tm/' + target.name,
54
56
  // Root copies src/feature/<name>/ per ACTIVE feature; keep this blanket
55
57
  // copy from restoring one that was switched off after `target add`.
56
- exclude: srcFeatureExcludes(model),
58
+ // A feature's inactive plugins go too - same rule, one level
59
+ // deeper. See helpers/featureSource.pluginExcludes.
60
+ exclude: [
61
+ ...srcFeatureExcludes(model),
62
+ ...pluginExcludes(model),
63
+ TEST_CONTROL_EXCLUDE,
64
+ ],
57
65
  replace: {
58
66
  ...props.ctx$.stdrep,
59
67
  }
@@ -21,7 +21,9 @@ import {
21
21
  serverVariables,
22
22
  isHttpBasicAuth,
23
23
  jsProp,
24
- jsOptProp, envName, envToken, liveStrict
24
+ jsOptProp, envName, envToken, liveStrict,
25
+ jsKey,
26
+ pointParts,
25
27
  } from '@voxgig/sdkgen'
26
28
 
27
29
 
@@ -49,7 +51,7 @@ const TestDirect = cmp(function TestDirect(props: any) {
49
51
  const authActive = isAuthActive(model)
50
52
  const authBasic = authActive && isHttpBasicAuth(model)
51
53
  const apikeyEnvEntry = authActive
52
- ? `\n '${PROJECTNAME}_APIKEY': '',${authBasic ? `\n '${PROJECTNAME}_SECRET': 'NONE',` : ''}`
54
+ ? `\n '${PROJECTNAME}_APIKEY': '',${authBasic ? `\n '${PROJECTNAME}_SECRET': '',` : ''}`
53
55
  : ''
54
56
  const apikeyLiveField = authActive
55
57
  ? `
@@ -61,13 +63,19 @@ const TestDirect = cmp(function TestDirect(props: any) {
61
63
  // impossible to construct without values: makeOptions raises rather than
62
64
  // request a URL with a literal `{account_id}` in it. Taken from the
63
65
  // environment, the same way the apikey is.
66
+ //
67
+ // Keys are quoted and the env read is bracketed via jsKey/jsProp: a server
68
+ // variable name is spec-derived and need not be a JS identifier — the URL
69
+ // grammar admits a leading digit ({2fa}), and a declared-but-unreferenced
70
+ // variable ({edge-zone}) is not constrained at all. Bare `name:` and
71
+ // `env.PROJ_SERVER_EDGE-ZONE` are both syntax errors.
64
72
  const svars = serverVariables(model)
65
73
  const serverEnvEntry = svars
66
74
  .map((v: any) => `\n '${serverVarEnv(PROJECTNAME, v.name)}': ${JSON.stringify(v.dflt)},`).join('')
67
75
  const serverLiveField = 0 === svars.length ? '' : `
68
76
  server: {${svars
69
77
  .map((v: any) => `
70
- ${v.name}: env.${serverVarEnv(PROJECTNAME, v.name)},`).join('')}
78
+ ${jsKey(v.name)}: ${jsProp('env', serverVarEnv(PROJECTNAME, v.name))},`).join('')}
71
79
  },`
72
80
 
73
81
  const opnames = Object.keys(entity.op || {})
@@ -288,7 +296,7 @@ function generateDirectLoad(model: Model, entity: ModelEntity, strict: boolean)
288
296
  }
289
297
 
290
298
  const allLoadParams = loadPoint.args?.params || []
291
- const loadPath = normalizePathParams(loadPoint.parts || [], allLoadParams, loadPoint.rename?.param)
299
+ const loadPath = normalizePathParams(pointParts(loadPoint), allLoadParams, loadPoint.rename?.param)
292
300
 
293
301
  // Some upstream OpenAPI specs declare a parameter as `in: path` even when
294
302
  // that path has no `{name}` placeholder for it. Only path params that
@@ -296,7 +304,7 @@ function generateDirectLoad(model: Model, entity: ModelEntity, strict: boolean)
296
304
  // setup and URL-substitution asserts; otherwise the SDK silently drops
297
305
  // them and the URL-includes assert fails.
298
306
  const pathPlaceholders = new Set<string>()
299
- for (const part of (loadPoint.parts || [])) {
307
+ for (const part of pointParts(loadPoint)) {
300
308
  if (typeof part === 'string' && part.startsWith('{') && part.endsWith('}')) {
301
309
  pathPlaceholders.add(part.slice(1, -1))
302
310
  }
@@ -330,7 +338,7 @@ function generateDirectLoad(model: Model, entity: ModelEntity, strict: boolean)
330
338
  const listOp = entity.op?.list
331
339
  const listPoint = listOp?.points?.[0]
332
340
  const listParams = listPoint?.args?.params || []
333
- const listPath = listPoint ? normalizePathParams(listPoint.parts || [], listParams, listPoint.rename?.param) : ''
341
+ const listPath = listPoint ? normalizePathParams(pointParts(listPoint), listParams, listPoint.rename?.param) : ''
334
342
  const hasList = null != listPoint
335
343
 
336
344
  // Ancestor params (not 'id') for live mode
@@ -523,7 +531,7 @@ function generateDirectList(model: Model, entity: ModelEntity, strict: boolean)
523
531
  }
524
532
 
525
533
  const listParams = listPoint.args?.params || []
526
- const listPath = normalizePathParams(listPoint.parts || [], listParams, listPoint.rename?.param)
534
+ const listPath = normalizePathParams(pointParts(listPoint), listParams, listPoint.rename?.param)
527
535
 
528
536
  // Required query params with spec-provided examples — needed to satisfy
529
537
  // the API contract in live mode (see generateDirectLoad for rationale).
@@ -33,7 +33,9 @@ import {
33
33
  serverVarEnv,
34
34
  serverVariables,
35
35
  isHttpBasicAuth,
36
- entityDataIdField, envName, envToken
36
+ entityDataIdField, envName, envToken,
37
+ jsKey,
38
+ jsProp
37
39
  } from '@voxgig/sdkgen'
38
40
 
39
41
 
@@ -69,7 +71,7 @@ const TestEntity = cmp(function TestEntity(props: any) {
69
71
  const authActive = isAuthActive(model)
70
72
  const authBasic = authActive && isHttpBasicAuth(model)
71
73
  const apikeyEnvEntry = authActive
72
- ? `\n '${PROJENVNAME}_APIKEY': '',${authBasic ? `\n '${PROJENVNAME}_SECRET': 'NONE',` : ''}`
74
+ ? `\n '${PROJENVNAME}_APIKEY': '',${authBasic ? `\n '${PROJENVNAME}_SECRET': '',` : ''}`
73
75
  : ''
74
76
  const apikeyLiveField = authActive
75
77
  ? `
@@ -81,13 +83,19 @@ const TestEntity = cmp(function TestEntity(props: any) {
81
83
  // impossible to construct without values: makeOptions raises rather than
82
84
  // request a URL with a literal `{account_id}` in it. So the live suite
83
85
  // takes them from the environment the same way it takes the apikey.
86
+ //
87
+ // Keys are quoted and the env read is bracketed via jsKey/jsProp: a server
88
+ // variable name is spec-derived and need not be a JS identifier — the URL
89
+ // grammar admits a leading digit ({2fa}), and a declared-but-unreferenced
90
+ // variable ({edge-zone}) is not constrained at all. Bare `name:` and
91
+ // `env.PROJ_SERVER_EDGE-ZONE` are both syntax errors.
84
92
  const svars = serverVariables(model)
85
93
  const serverEnvEntry = svars
86
94
  .map((v: any) => `\n '${serverVarEnv(PROJENVNAME, v.name)}': ${JSON.stringify(v.dflt)},`).join('')
87
95
  const serverLiveField = 0 === svars.length ? '' : `
88
96
  server: {${svars
89
97
  .map((v: any) => `
90
- ${v.name}: env.${serverVarEnv(PROJENVNAME, v.name)},`).join('')}
98
+ ${jsKey(v.name)}: ${jsProp('env', serverVarEnv(PROJENVNAME, v.name))},`).join('')}
91
99
  },`
92
100
 
93
101
  // TODO: should be a utility function
@@ -3,7 +3,8 @@ import type {
3
3
  ModelEntity
4
4
  } from '@voxgig/apidef'
5
5
 
6
- import { cmp, each, Folder, entityCollection } from '@voxgig/sdkgen'
6
+ import { cmp, each, Folder, entityCollection,
7
+ TestControl } from '@voxgig/sdkgen'
7
8
 
8
9
 
9
10
  // import { Quick } from './Quick_ts'
@@ -19,6 +20,9 @@ const Test = cmp(function Test(props: any) {
19
20
  const { target } = props
20
21
 
21
22
  Folder({ name: 'test' }, () => {
23
+
24
+ // Write-once: a project's edited control file survives regeneration.
25
+ TestControl({ target, dir: 'test' })
22
26
  // Quick({ target })
23
27
  // TestMain({ target })
24
28