@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
@@ -17,9 +17,13 @@ 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
 
25
+ import { perlStringLiteral } from './utility_perl'
26
+
23
27
 
24
28
  // See TestEntity_ts.ts for the GenCtx/OpGen contract. Flow-step variables
25
29
  // are held in a single declared perl hash (%V) so generated steps never
@@ -54,12 +58,25 @@ const TestEntity = cmp(function TestEntity(props: any) {
54
58
 
55
59
  const authActive = isAuthActive(model)
56
60
  const apikeyEnvEntry = authActive
57
- ? `\n '${PROJUPPER}_APIKEY' => 'NONE',`
61
+ ? `\n '${PROJUPPER}_APIKEY' => '',`
58
62
  : ''
59
63
  const apikeyLiveField = authActive
60
64
  ? `\n 'apikey' => $env->{'${PROJUPPER}_APIKEY'},`
61
65
  : ''
62
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
+ const svars = serverVariables(model)
72
+ const serverEnvEntry = svars
73
+ .map((v: any) => `\n '${serverVarEnv(PROJUPPER, v.name)}' => ${perlStringLiteral(v.dflt)},`).join('')
74
+ const serverLiveField = 0 === svars.length ? '' : `
75
+ 'server' => {${svars
76
+ .map((v: any) => `
77
+ '${v.name}' => $env->{'${serverVarEnv(PROJUPPER, v.name)}'},`).join('')}
78
+ },`
79
+
63
80
  const idnames = buildIdNames(entity, basicflow)
64
81
  const idnamesStr = idnames.map(n => `'${n}'`).join(', ')
65
82
 
@@ -193,7 +210,7 @@ BASIC_FLOW: {
193
210
  my $env = ${N}TestRunner::env_override({
194
211
  '${ENTIDVAR}' => $idmap,
195
212
  '${PROJUPPER}_TEST_LIVE' => 'FALSE',
196
- '${PROJUPPER}_TEST_EXPLAIN' => 'FALSE',${apikeyEnvEntry}
213
+ '${PROJUPPER}_TEST_EXPLAIN' => 'FALSE',${apikeyEnvEntry}${serverEnvEntry}
197
214
  });
198
215
 
199
216
  my $idmap_resolved = ${N}Helpers::to_map($env->{'${ENTIDVAR}'});
@@ -213,7 +230,10 @@ BASIC_FLOW: {
213
230
  Content(`
214
231
  if ((($env->{'${PROJUPPER}_TEST_LIVE'}) || '') eq 'TRUE') {
215
232
  my $merged_opts = Voxgig::Struct::merge([
216
- {${apikeyLiveField}
233
+ # FIRST, so the generated fields below win: sdk-test-control.json's
234
+ # test.client.options adds to the live client, it does not redirect it.
235
+ ${N}TestRunner::live_client_options(),
236
+ {${apikeyLiveField}${serverLiveField}
217
237
  },
218
238
  (Voxgig::Struct::ismap($extra) ? $extra : {}),
219
239
  ]);
@@ -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_perl'
@@ -17,6 +18,9 @@ const Test = cmp(function Test(props: any) {
17
18
 
18
19
  Folder({ name: 't' }, () => {
19
20
 
21
+ // Write-once: a project's edited control file survives regeneration.
22
+ TestControl({ target, dir: 't' })
23
+
20
24
  // Generate exists test
21
25
  File({ name: 'exists.t' }, () => {
22
26
  Content(`#!perl
@@ -75,7 +75,17 @@ function clean(o: any, dropDefaults?: boolean): any {
75
75
  }
76
76
 
77
77
 
78
+ // A Perl SINGLE-quoted string literal. Single quotes do not interpolate, so a
79
+ // spec-derived value containing $foo or @foo survives verbatim; only the
80
+ // backslash and the quote itself need escaping. A double-quoted literal (what
81
+ // JSON.stringify emits) would interpolate them.
82
+ function perlStringLiteral(val: string): string {
83
+ return "'" + String(val).replace(/\\/g, '\\\\').replace(/'/g, "\\'") + "'"
84
+ }
85
+
86
+
78
87
  export {
88
+ perlStringLiteral,
79
89
  clean,
80
90
  projectPath,
81
91
  }
@@ -29,7 +29,6 @@ import {
29
29
 
30
30
 
31
31
  import {
32
- clean,
33
32
  formatPhpArray,
34
33
  } from './utility_php'
35
34
 
@@ -239,12 +238,7 @@ ${serverBlock}${authBlock} "headers" => ${formatPhpArray(headers,
239
238
  Content(` ],
240
239
  ],
241
240
  "entity" => ${formatPhpArray(
242
- Object.values(entity).reduce((a: any, n: any) => (a[n.name] = clean({
243
- fields: n.fields,
244
- name: n.name,
245
- op: n.op,
246
- relations: n.relations,
247
- }, true), a), {}), 3)},
241
+ configDef.entity, 3)},
248
242
  ];
249
243
  `)
250
244
  }
@@ -5,6 +5,7 @@ import {
5
5
  cmp, each, names, cmap,
6
6
  List, File, Content, Copy, Folder, Fragment, Line, FeatureHook,
7
7
  targetFeatures,
8
+ TEST_CONTROL_EXCLUDE
8
9
  } from '@voxgig/sdkgen'
9
10
 
10
11
 
@@ -44,7 +45,7 @@ const Main = cmp(async function Main(props: any) {
44
45
  // Copy tm/php files with replacements
45
46
  Copy({
46
47
  from: 'tm/' + target.name,
47
- exclude: [/src\//],
48
+ exclude: [/src\//, TEST_CONTROL_EXCLUDE],
48
49
  replace: {
49
50
  ...props.ctx$.stdrep,
50
51
  }
@@ -11,9 +11,14 @@ 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
 
20
+ import { formatPhpValue } from './utility_php'
21
+
17
22
 
18
23
  function normalizePathParams(
19
24
  parts: string[],
@@ -64,12 +69,25 @@ const TestDirect = cmp(function TestDirect(props: any) {
64
69
 
65
70
  const authActive = isAuthActive(model)
66
71
  const apikeyEnvEntry = authActive
67
- ? `\n "${PROJECTNAME}_APIKEY" => "NONE",`
72
+ ? `\n "${PROJECTNAME}_APIKEY" => "",`
68
73
  : ''
69
74
  const apikeyLiveField = authActive
70
75
  ? `\n "apikey" => $env["${PROJECTNAME}_APIKEY"],`
71
76
  : ''
72
77
 
78
+ // A templated server URL (OpenAPI server variables) makes a LIVE client
79
+ // impossible to construct without values: makeOptions raises rather than
80
+ // request a URL with a literal `{account_id}` in it. So the live suite
81
+ // takes them from the environment the same way it takes the apikey.
82
+ const svars = serverVariables(model)
83
+ const serverEnvEntry = svars
84
+ .map((v: any) => `\n "${serverVarEnv(PROJECTNAME, v.name)}" => ${formatPhpValue(v.dflt)},`).join('')
85
+ const serverLiveField = 0 === svars.length ? '' : `
86
+ "server" => [${svars
87
+ .map((v: any) => `
88
+ "${v.name}" => $env["${serverVarEnv(PROJECTNAME, v.name)}"],`).join('')}
89
+ ],`
90
+
73
91
  const opnames = Object.keys(entity.op || {})
74
92
  const hasLoad = opnames.includes('load')
75
93
  const hasList = opnames.includes('list')
@@ -82,7 +100,7 @@ const TestDirect = cmp(function TestDirect(props: any) {
82
100
  const listOp = entity.op?.list
83
101
 
84
102
  const loadPoint = loadOp?.points?.[0]
85
- const loadPath = loadPoint ? normalizePathParams(loadPoint.parts || [], loadPoint?.args?.params || [], loadPoint?.rename?.param) : ''
103
+ const loadPath = loadPoint ? normalizePathParams(pointParts(loadPoint), loadPoint?.args?.params || [], loadPoint?.rename?.param) : ''
86
104
  const allLoadParams = loadPoint?.args?.params || []
87
105
  // Some upstream OpenAPI specs declare a parameter as `in: path` even when
88
106
  // that path has no `{name}` placeholder for it. Only path params that
@@ -90,7 +108,7 @@ const TestDirect = cmp(function TestDirect(props: any) {
90
108
  // setup and URL-substitution asserts; otherwise the SDK silently drops
91
109
  // them and the URL-includes assert fails.
92
110
  const _pathPlaceholders = new Set<string>()
93
- for (const part of (loadPoint?.parts || [])) {
111
+ for (const part of pointParts(loadPoint)) {
94
112
  if (typeof part === 'string' && part.startsWith('{') && part.endsWith('}')) {
95
113
  _pathPlaceholders.add(part.slice(1, -1))
96
114
  }
@@ -107,7 +125,7 @@ const TestDirect = cmp(function TestDirect(props: any) {
107
125
  _renamedPlaceholders.has(p.name) || _renamedPlaceholders.has(p.orig))
108
126
 
109
127
  const listPoint = listOp?.points?.[0]
110
- const listPath = listPoint ? normalizePathParams(listPoint.parts || [], listPoint?.args?.params || [], listPoint?.rename?.param) : ''
128
+ const listPath = listPoint ? normalizePathParams(pointParts(listPoint), listPoint?.args?.params || [], listPoint?.rename?.param) : ''
111
129
  const listParams = listPoint?.args?.params || []
112
130
 
113
131
  // Required query params with spec-provided examples — needed in live mode.
@@ -356,14 +374,16 @@ function ${entity.name}_direct_setup($mockres)
356
374
 
357
375
  $env = Runner::env_override([
358
376
  "${entidEnvVar}" => [],
359
- "${PROJECTNAME}_TEST_LIVE" => "FALSE",${apikeyEnvEntry}
377
+ "${PROJECTNAME}_TEST_LIVE" => "FALSE",${apikeyEnvEntry}${serverEnvEntry}
360
378
  ]);
361
379
 
362
380
  $live = $env["${PROJECTNAME}_TEST_LIVE"] === "TRUE";
363
381
 
364
382
  if ($live) {
365
- $merged_opts = [${apikeyLiveField}
366
- ];
383
+ // Merged so the generated fields win: sdk-test-control.json's
384
+ // test.client.options adds to the live client, it does not redirect it.
385
+ $merged_opts = array_merge(Runner::live_client_options(), [${apikeyLiveField}${serverLiveField}
386
+ ]);
367
387
  $client = new ${model.const.Name}SDK($merged_opts);
368
388
  return [
369
389
  "client" => $client,
@@ -24,10 +24,14 @@ import {
24
24
  buildIdNames,
25
25
  getMatchEntries,
26
26
  isAuthActive,
27
+ serverVarEnv,
28
+ serverVariables,
27
29
  entityDataIdField, envName, envToken,
28
30
  phpEntityAccessor,
29
31
  } from '@voxgig/sdkgen'
30
32
 
33
+ import { formatPhpValue } from './utility_php'
34
+
31
35
 
32
36
  // PHP's GenCtx mirrors the shared shape (see TestEntity_ts.ts) plus an
33
37
  // `accessor` slot used to mangle the entity factory name when it collides
@@ -75,12 +79,25 @@ const TestEntity = cmp(function TestEntity(props: any) {
75
79
 
76
80
  const authActive = isAuthActive(model)
77
81
  const apikeyEnvEntry = authActive
78
- ? `\n "${PROJUPPER}_APIKEY" => "NONE",`
82
+ ? `\n "${PROJUPPER}_APIKEY" => "",`
79
83
  : ''
80
84
  const apikeyLiveField = authActive
81
85
  ? `\n "apikey" => $env["${PROJUPPER}_APIKEY"],`
82
86
  : ''
83
87
 
88
+ // A templated server URL (OpenAPI server variables) makes a LIVE client
89
+ // impossible to construct without values: makeOptions raises rather than
90
+ // request a URL with a literal `{account_id}` in it. So the live suite
91
+ // takes them from the environment the same way it takes the apikey.
92
+ const svars = serverVariables(model)
93
+ const serverEnvEntry = svars
94
+ .map((v: any) => `\n "${serverVarEnv(PROJUPPER, v.name)}" => ${formatPhpValue(v.dflt)},`).join('')
95
+ const serverLiveField = 0 === svars.length ? '' : `
96
+ "server" => [${svars
97
+ .map((v: any) => `
98
+ "${v.name}" => $env["${serverVarEnv(PROJUPPER, v.name)}"],`).join('')}
99
+ ],`
100
+
84
101
  const idnames = buildIdNames(entity, basicflow)
85
102
  const idnamesStr = idnames.map(n => `"${n}"`).join(', ')
86
103
 
@@ -246,7 +263,7 @@ ${hasList ? `
246
263
  $env = Runner::env_override([
247
264
  "${PROJUPPER}_TEST_${envToken(entity.name)}_ENTID" => $idmap,
248
265
  "${PROJUPPER}_TEST_LIVE" => "FALSE",
249
- "${PROJUPPER}_TEST_EXPLAIN" => "FALSE",${apikeyEnvEntry}
266
+ "${PROJUPPER}_TEST_EXPLAIN" => "FALSE",${apikeyEnvEntry}${serverEnvEntry}
250
267
  ]);
251
268
 
252
269
  $idmap_resolved = Helpers::to_map(
@@ -267,9 +284,16 @@ ${hasList ? `
267
284
  Content(`
268
285
  if ($env["${PROJUPPER}_TEST_LIVE"] === "TRUE") {
269
286
  $merged_opts = Vs::merge([
270
- [${apikeyLiveField}
287
+ // FIRST, so the generated fields below win: sdk-test-control.json's
288
+ // test.client.options adds to the live client, it does not redirect it.
289
+ Runner::live_client_options(),
290
+ [${apikeyLiveField}${serverLiveField}
271
291
  ],
272
- $extra ?? [],
292
+ // ismap, not a plain "?? []" default: an empty PHP array is a
293
+ // LIST, and a non-map later entry REPLACES the accumulated map in
294
+ // merge - so the no-extras call discarded live_client_options()
295
+ // and the apikey/server map above it.
296
+ Vs::ismap($extra) ? $extra : new \\stdClass(),
273
297
  ]);
274
298
  $client = new ${model.const.Name}SDK(Helpers::to_map($merged_opts));
275
299
  }
@@ -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_php'
@@ -17,6 +18,9 @@ const Test = cmp(function Test(props: any) {
17
18
 
18
19
  Folder({ name: 'test' }, () => {
19
20
 
21
+ // Write-once: a project's edited control file survives regeneration.
22
+ TestControl({ target, dir: 'test' })
23
+
20
24
  // Generate exists test
21
25
  File({ name: 'ExistsTest.' + target.ext }, () => {
22
26
  Content(`<?php
@@ -28,7 +28,6 @@ import {
28
28
 
29
29
 
30
30
  import {
31
- clean,
32
31
  formatPyDict,
33
32
  } from './utility_py'
34
33
 
@@ -180,12 +179,7 @@ ${serverBlock}${authBlock} "headers": ${formatPyDict(headers, 3)},
180
179
  Content(` },
181
180
  },
182
181
  "entity": ${formatPyDict(
183
- Object.values(entity).reduce((a: any, n: any) => (a[n.name] = clean({
184
- fields: n.fields,
185
- name: n.name,
186
- op: n.op,
187
- relations: n.relations,
188
- }, true), a), {}), 2)},
182
+ configDef.entity, 2)},
189
183
  }
190
184
  `)
191
185
  })
@@ -6,6 +6,7 @@ import {
6
6
  List, File, Content, Copy, Folder, Fragment, Line, FeatureHook,
7
7
  entityClassName, entityCollection,
8
8
  targetFeatures,
9
+ TEST_CONTROL_EXCLUDE
9
10
  } from '@voxgig/sdkgen'
10
11
 
11
12
 
@@ -50,7 +51,7 @@ const Main = cmp(async function Main(props: any) {
50
51
  // below.
51
52
  Copy({
52
53
  from: 'tm/' + target.name,
53
- exclude: [/src\//, /pkg\//],
54
+ exclude: [/src\//, /pkg\//, TEST_CONTROL_EXCLUDE],
54
55
  replace: {
55
56
  ...props.ctx$.stdrep,
56
57
  }
@@ -13,7 +13,8 @@ import {
13
13
  snakify,
14
14
  isAuthActive,
15
15
  serverVarEnv,
16
- serverVariables, envName, envToken
16
+ serverVariables, envName, envToken,
17
+ pointParts,
17
18
  } from '@voxgig/sdkgen'
18
19
 
19
20
 
@@ -98,7 +99,7 @@ const TestDirect = cmp(function TestDirect(props: any) {
98
99
  const listOp = entity.op?.list
99
100
 
100
101
  const loadPoint = loadOp?.points?.[0]
101
- const loadPath = loadPoint ? normalizePathParams(loadPoint.parts || [], loadPoint?.args?.params || [], loadPoint?.rename?.param) : ''
102
+ const loadPath = loadPoint ? normalizePathParams(pointParts(loadPoint), loadPoint?.args?.params || [], loadPoint?.rename?.param) : ''
102
103
  const allLoadParams = loadPoint?.args?.params || []
103
104
  // Some upstream OpenAPI specs declare a parameter as `in: path` even when
104
105
  // that path has no `{name}` placeholder for it. Only path params that
@@ -106,7 +107,7 @@ const TestDirect = cmp(function TestDirect(props: any) {
106
107
  // setup and URL-substitution asserts; otherwise the SDK silently drops
107
108
  // them and the URL-includes assert fails.
108
109
  const _pathPlaceholders = new Set<string>()
109
- for (const part of (loadPoint?.parts || [])) {
110
+ for (const part of pointParts(loadPoint)) {
110
111
  if (typeof part === 'string' && part.startsWith('{') && part.endsWith('}')) {
111
112
  _pathPlaceholders.add(part.slice(1, -1))
112
113
  }
@@ -123,7 +124,7 @@ const TestDirect = cmp(function TestDirect(props: any) {
123
124
  _renamedPlaceholders.has(p.name) || _renamedPlaceholders.has(p.orig))
124
125
 
125
126
  const listPoint = listOp?.points?.[0]
126
- const listPath = listPoint ? normalizePathParams(listPoint.parts || [], listPoint?.args?.params || [], listPoint?.rename?.param) : ''
127
+ const listPath = listPoint ? normalizePathParams(pointParts(listPoint), listPoint?.args?.params || [], listPoint?.rename?.param) : ''
127
128
  const listParams = listPoint?.args?.params || []
128
129
 
129
130
  // Required query params with spec-provided examples — needed in live mode
@@ -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_py'
@@ -17,6 +18,9 @@ const Test = cmp(function Test(props: any) {
17
18
 
18
19
  Folder({ name: 'test' }, () => {
19
20
 
21
+ // Write-once: a project's edited control file survives regeneration.
22
+ TestControl({ target, dir: 'test' })
23
+
20
24
  // Generate __init__.py for test package
21
25
  File({ name: '__init__.' + target.ext }, () => {
22
26
  Content(``)
@@ -29,7 +29,6 @@ import {
29
29
 
30
30
 
31
31
  import {
32
- clean,
33
32
  formatRubyHash,
34
33
  } from './utility_rb'
35
34
 
@@ -165,12 +164,7 @@ ${serverBlock}${authBlock} "headers" => ${formatRubyHash(headers, 4)},
165
164
  Content(` },
166
165
  },
167
166
  "entity" => ${formatRubyHash(
168
- Object.values(entity).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), {}), 3)},
167
+ configDef.entity, 3)},
174
168
  }
175
169
  end
176
170
  `)
@@ -5,6 +5,7 @@ import {
5
5
  cmp, each, names, cmap,
6
6
  List, File, Content, Copy, Folder, Fragment, Line, FeatureHook,
7
7
  targetFeatures,
8
+ TEST_CONTROL_EXCLUDE
8
9
  } from '@voxgig/sdkgen'
9
10
 
10
11
 
@@ -44,7 +45,7 @@ const Main = cmp(async function Main(props: any) {
44
45
  // Copy tm/rb files with replacements
45
46
  Copy({
46
47
  from: 'tm/' + target.name,
47
- exclude: [/src\//],
48
+ exclude: [/src\//, TEST_CONTROL_EXCLUDE],
48
49
  replace: {
49
50
  ...props.ctx$.stdrep,
50
51
  }
@@ -11,9 +11,14 @@ 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
 
20
+ import { formatRubyValue } from './utility_rb'
21
+
17
22
 
18
23
  function normalizePathParams(
19
24
  parts: string[],
@@ -64,12 +69,25 @@ const TestDirect = cmp(function TestDirect(props: any) {
64
69
 
65
70
  const authActive = isAuthActive(model)
66
71
  const apikeyEnvEntry = authActive
67
- ? `\n "${PROJECTNAME}_APIKEY" => "NONE",`
72
+ ? `\n "${PROJECTNAME}_APIKEY" => "",`
68
73
  : ''
69
74
  const apikeyLiveField = authActive
70
75
  ? `\n "apikey" => env["${PROJECTNAME}_APIKEY"],`
71
76
  : ''
72
77
 
78
+ // A templated server URL (OpenAPI server variables) makes a LIVE client
79
+ // impossible to construct without values: makeOptions raises rather than
80
+ // request a URL with a literal `{account_id}` in it. So the live suite
81
+ // takes them from the environment the same way it takes the apikey.
82
+ const svars = serverVariables(model)
83
+ const serverEnvEntry = svars
84
+ .map((v: any) => `\n "${serverVarEnv(PROJECTNAME, v.name)}" => ${formatRubyValue(v.dflt)},`).join('')
85
+ const serverLiveField = 0 === svars.length ? '' : `
86
+ "server" => {${svars
87
+ .map((v: any) => `
88
+ "${v.name}" => env["${serverVarEnv(PROJECTNAME, v.name)}"],`).join('')}
89
+ },`
90
+
73
91
  const opnames = Object.keys(entity.op || {})
74
92
  const hasLoad = opnames.includes('load')
75
93
  const hasList = opnames.includes('list')
@@ -82,7 +100,7 @@ const TestDirect = cmp(function TestDirect(props: any) {
82
100
  const listOp = entity.op?.list
83
101
 
84
102
  const loadPoint = loadOp?.points?.[0]
85
- const loadPath = loadPoint ? normalizePathParams(loadPoint.parts || [], loadPoint?.args?.params || [], loadPoint?.rename?.param) : ''
103
+ const loadPath = loadPoint ? normalizePathParams(pointParts(loadPoint), loadPoint?.args?.params || [], loadPoint?.rename?.param) : ''
86
104
  const allLoadParams = loadPoint?.args?.params || []
87
105
  // Some upstream OpenAPI specs declare a parameter as `in: path` even when
88
106
  // that path has no `{name}` placeholder for it. Only path params that
@@ -90,7 +108,7 @@ const TestDirect = cmp(function TestDirect(props: any) {
90
108
  // setup and URL-substitution asserts; otherwise the SDK silently drops
91
109
  // them and the URL-includes assert fails.
92
110
  const _pathPlaceholders = new Set<string>()
93
- for (const part of (loadPoint?.parts || [])) {
111
+ for (const part of pointParts(loadPoint)) {
94
112
  if (typeof part === 'string' && part.startsWith('{') && part.endsWith('}')) {
95
113
  _pathPlaceholders.add(part.slice(1, -1))
96
114
  }
@@ -107,7 +125,7 @@ const TestDirect = cmp(function TestDirect(props: any) {
107
125
  _renamedPlaceholders.has(p.name) || _renamedPlaceholders.has(p.orig))
108
126
 
109
127
  const listPoint = listOp?.points?.[0]
110
- const listPath = listPoint ? normalizePathParams(listPoint.parts || [], listPoint?.args?.params || [], listPoint?.rename?.param) : ''
128
+ const listPath = listPoint ? normalizePathParams(pointParts(listPoint), listPoint?.args?.params || [], listPoint?.rename?.param) : ''
111
129
  const listParams = listPoint?.args?.params || []
112
130
 
113
131
  // Required query params with spec-provided examples — needed in live mode.
@@ -349,14 +367,16 @@ def ${entity.name}_direct_setup(mockres)
349
367
 
350
368
  env = Runner.env_override({
351
369
  "${entidEnvVar}" => {},
352
- "${PROJECTNAME}_TEST_LIVE" => "FALSE",${apikeyEnvEntry}
370
+ "${PROJECTNAME}_TEST_LIVE" => "FALSE",${apikeyEnvEntry}${serverEnvEntry}
353
371
  })
354
372
 
355
373
  live = env["${PROJECTNAME}_TEST_LIVE"] == "TRUE"
356
374
 
357
375
  if live
358
- merged_opts = {${apikeyLiveField}
359
- }
376
+ # Merged so the generated fields win: sdk-test-control.json's
377
+ # test.client.options adds to the live client, it does not redirect it.
378
+ merged_opts = Runner.live_client_options.merge({${apikeyLiveField}${serverLiveField}
379
+ })
360
380
  client = ${model.const.Name}SDK.new(merged_opts)
361
381
  return {
362
382
  client: client,
@@ -24,9 +24,13 @@ import {
24
24
  buildIdNames,
25
25
  getMatchEntries,
26
26
  isAuthActive,
27
- entityDataIdField, envName, envToken
27
+ entityDataIdField, envName, envToken,
28
+ serverVarEnv,
29
+ serverVariables
28
30
  } from '@voxgig/sdkgen'
29
31
 
32
+ import { formatRubyValue } from './utility_rb'
33
+
30
34
 
31
35
  // See TestEntity_ts.ts for the GenCtx/OpGen contract.
32
36
  type GenCtx = {
@@ -62,12 +66,25 @@ const TestEntity = cmp(function TestEntity(props: any) {
62
66
 
63
67
  const authActive = isAuthActive(model)
64
68
  const apikeyEnvEntry = authActive
65
- ? `\n "${PROJUPPER}_APIKEY" => "NONE",`
69
+ ? `\n "${PROJUPPER}_APIKEY" => "",`
66
70
  : ''
67
71
  const apikeyLiveField = authActive
68
72
  ? `\n "apikey" => env["${PROJUPPER}_APIKEY"],`
69
73
  : ''
70
74
 
75
+ // A templated server URL (OpenAPI server variables) makes a LIVE client
76
+ // impossible to construct without values: makeOptions raises rather than
77
+ // request a URL with a literal `{account_id}` in it. So the live suite
78
+ // takes them from the environment the same way it takes the apikey.
79
+ const svars = serverVariables(model)
80
+ const serverEnvEntry = svars
81
+ .map((v: any) => `\n "${serverVarEnv(PROJUPPER, v.name)}" => ${formatRubyValue(v.dflt)},`).join('')
82
+ const serverLiveField = 0 === svars.length ? '' : `
83
+ "server" => {${svars
84
+ .map((v: any) => `
85
+ "${v.name}" => env["${serverVarEnv(PROJUPPER, v.name)}"],`).join('')}
86
+ },`
87
+
71
88
  const idnames = buildIdNames(entity, basicflow)
72
89
  const idnamesStr = idnames.map(n => `"${n}"`).join(', ')
73
90
 
@@ -227,7 +244,7 @@ end
227
244
  env = Runner.env_override({
228
245
  "${PROJUPPER}_TEST_${envToken(entity.name)}_ENTID" => idmap,
229
246
  "${PROJUPPER}_TEST_LIVE" => "FALSE",
230
- "${PROJUPPER}_TEST_EXPLAIN" => "FALSE",${apikeyEnvEntry}
247
+ "${PROJUPPER}_TEST_EXPLAIN" => "FALSE",${apikeyEnvEntry}${serverEnvEntry}
231
248
  })
232
249
 
233
250
  idmap_resolved = Helpers.to_map(
@@ -248,7 +265,10 @@ end
248
265
  Content(`
249
266
  if env["${PROJUPPER}_TEST_LIVE"] == "TRUE"
250
267
  merged_opts = Vs.merge([
251
- {${apikeyLiveField}
268
+ # FIRST, so the generated fields below win: sdk-test-control.json's
269
+ # test.client.options adds to the live client, it does not redirect it.
270
+ Runner.live_client_options,
271
+ {${apikeyLiveField}${serverLiveField}
252
272
  },
253
273
  extra || {},
254
274
  ])
@@ -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_rb'
@@ -17,6 +18,9 @@ const Test = cmp(function Test(props: any) {
17
18
 
18
19
  Folder({ name: 'test' }, () => {
19
20
 
21
+ // Write-once: a project's edited control file survives regeneration.
22
+ TestControl({ target, dir: 'test' })
23
+
20
24
  // Generate exists test
21
25
  File({ name: 'exists_test.' + target.ext }, () => {
22
26
  Content(`# ${model.const.Name} SDK exists test
@@ -48,7 +48,9 @@ function formatRubyValue(val: any, indent: number = 0): string {
48
48
  return 'nil'
49
49
  }
50
50
  if (typeof val === 'string') {
51
- return `"${val.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`
51
+ // `#` is escaped only where it OPENS an interpolation (#{, #$, #@), so
52
+ // this changes output solely for values that would otherwise interpolate.
53
+ return `"${val.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/#(?=[{$@])/g, '\\#')}"`
52
54
  }
53
55
  if (typeof val === 'number') {
54
56
  return String(val)
@@ -6,6 +6,7 @@ import {
6
6
  File, Content, Copy, Folder, Fragment,
7
7
  entityClassName,
8
8
  targetFeatures,
9
+ TEST_CONTROL_EXCLUDE
9
10
  } from '@voxgig/sdkgen'
10
11
 
11
12
 
@@ -54,7 +55,7 @@ const Main = cmp(async function Main(props: any) {
54
55
  // here exactly like the go target.
55
56
  Copy({
56
57
  from: 'tm/' + target.name,
57
- exclude: [/src\//],
58
+ exclude: [/src\//, TEST_CONTROL_EXCLUDE],
58
59
  replace: {
59
60
  ...props.ctx$.stdrep,
60
61
  RUSTCRATE: rustcrate,