@voxgig/sdkgen 4.5.3 → 4.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (174) hide show
  1. package/bin/voxgig-sdkgen +1 -1
  2. package/dist/action/check.js +15 -0
  3. package/dist/action/check.js.map +1 -1
  4. package/dist/cmp/AgentGuide.js +1 -1
  5. package/dist/cmp/AgentGuide.js.map +1 -1
  6. package/dist/cmp/AgentGuideContent.d.ts +1 -1
  7. package/dist/cmp/AgentGuideContent.js +8 -2
  8. package/dist/cmp/AgentGuideContent.js.map +1 -1
  9. package/dist/cmp/Feature.js +17 -0
  10. package/dist/cmp/Feature.js.map +1 -1
  11. package/dist/cmp/FeatureDocs.d.ts +1 -1
  12. package/dist/cmp/FeatureDocs.js +6 -1
  13. package/dist/cmp/FeatureDocs.js.map +1 -1
  14. package/dist/cmp/ReadmeExplanation.js +3 -1
  15. package/dist/cmp/ReadmeExplanation.js.map +1 -1
  16. package/dist/cmp/ReadmeFeatures.js +1 -1
  17. package/dist/cmp/ReadmeFeatures.js.map +1 -1
  18. package/dist/cmp/ReadmeRefFeatures.js +1 -1
  19. package/dist/cmp/ReadmeRefFeatures.js.map +1 -1
  20. package/dist/helpers/applicability.d.ts +6 -0
  21. package/dist/helpers/applicability.js +103 -0
  22. package/dist/helpers/applicability.js.map +1 -0
  23. package/dist/helpers/collectDeps.js +4 -2
  24. package/dist/helpers/collectDeps.js.map +1 -1
  25. package/dist/helpers/serverVars.d.ts +2 -1
  26. package/dist/helpers/serverVars.js +17 -0
  27. package/dist/helpers/serverVars.js.map +1 -1
  28. package/dist/sdkgen.d.ts +3 -2
  29. package/dist/sdkgen.js +9 -2
  30. package/dist/sdkgen.js.map +1 -1
  31. package/dist/tsconfig.tsbuildinfo +1 -1
  32. package/dist/utility.js +7 -1
  33. package/dist/utility.js.map +1 -1
  34. package/model/sdkgen.aon +39 -0
  35. package/package.json +1 -1
  36. package/project/.sdk/model/feature/feature-index.aon +1 -0
  37. package/project/.sdk/model/feature/secrets.aon +118 -0
  38. package/project/.sdk/model/target/ts.aon +8 -1
  39. package/project/.sdk/src/cmp/c/Config_c.ts +5 -1
  40. package/project/.sdk/src/cmp/cpp/Config_cpp.ts +5 -1
  41. package/project/.sdk/src/cmp/csharp/Config_csharp.ts +5 -1
  42. package/project/.sdk/src/cmp/dart/Config_dart.ts +5 -1
  43. package/project/.sdk/src/cmp/elixir/Config_elixir.ts +5 -1
  44. package/project/.sdk/src/cmp/elixir/Main_elixir.ts +5 -1
  45. package/project/.sdk/src/cmp/go/Config_go.ts +5 -1
  46. package/project/.sdk/src/cmp/go/Main_go.ts +6 -2
  47. package/project/.sdk/src/cmp/go/ReadmeQuick_go.ts +16 -3
  48. package/project/.sdk/src/cmp/go/ReadmeTopQuick_go.ts +16 -3
  49. package/project/.sdk/src/cmp/go/TestDirect_go.ts +28 -4
  50. package/project/.sdk/src/cmp/go/TestEntity_go.ts +43 -10
  51. package/project/.sdk/src/cmp/go/fragment/Main.fragment.go +5 -5
  52. package/project/.sdk/src/cmp/java/Config_java.ts +22 -1
  53. package/project/.sdk/src/cmp/java/Main_java.ts +5 -1
  54. package/project/.sdk/src/cmp/java/ReadmeQuick_java.ts +17 -3
  55. package/project/.sdk/src/cmp/java/ReadmeTopQuick_java.ts +17 -3
  56. package/project/.sdk/src/cmp/java/TestDirect_java.ts +23 -4
  57. package/project/.sdk/src/cmp/java/TestEntity_java.ts +29 -4
  58. package/project/.sdk/src/cmp/js/Config_js.ts +5 -1
  59. package/project/.sdk/src/cmp/js/Main_js.ts +5 -1
  60. package/project/.sdk/src/cmp/js/Package_js.ts +4 -1
  61. package/project/.sdk/src/cmp/kotlin/Config_kotlin.ts +5 -1
  62. package/project/.sdk/src/cmp/lean/Config_lean.ts +5 -1
  63. package/project/.sdk/src/cmp/lua/Config_lua.ts +5 -1
  64. package/project/.sdk/src/cmp/lua/Main_lua.ts +5 -1
  65. package/project/.sdk/src/cmp/lua/Package_lua.ts +4 -1
  66. package/project/.sdk/src/cmp/ocaml/Config_ocaml.ts +5 -1
  67. package/project/.sdk/src/cmp/perl/Main_perl.ts +5 -1
  68. package/project/.sdk/src/cmp/php/Config_php.ts +5 -1
  69. package/project/.sdk/src/cmp/php/Main_php.ts +5 -1
  70. package/project/.sdk/src/cmp/py/Config_py.ts +5 -1
  71. package/project/.sdk/src/cmp/py/Main_py.ts +5 -1
  72. package/project/.sdk/src/cmp/py/ReadmeQuick_py.ts +16 -3
  73. package/project/.sdk/src/cmp/py/ReadmeTopQuick_py.ts +16 -3
  74. package/project/.sdk/src/cmp/py/TestDirect_py.ts +24 -5
  75. package/project/.sdk/src/cmp/py/TestEntity_py.ts +23 -3
  76. package/project/.sdk/src/cmp/rb/Config_rb.ts +5 -1
  77. package/project/.sdk/src/cmp/rb/Main_rb.ts +5 -1
  78. package/project/.sdk/src/cmp/rust/Config_rust.ts +5 -1
  79. package/project/.sdk/src/cmp/rust/Main_rust.ts +5 -1
  80. package/project/.sdk/src/cmp/scala/Config_scala.ts +5 -1
  81. package/project/.sdk/src/cmp/scala/Main_scala.ts +5 -1
  82. package/project/.sdk/src/cmp/swift/Config_swift.ts +5 -1
  83. package/project/.sdk/src/cmp/ts/Config_ts.ts +5 -1
  84. package/project/.sdk/src/cmp/ts/Main_ts.ts +57 -1
  85. package/project/.sdk/src/cmp/ts/Package_ts.ts +4 -1
  86. package/project/.sdk/src/cmp/ts/ReadmeModel_ts.ts +15 -3
  87. package/project/.sdk/src/cmp/ts/ReadmeQuick_ts.ts +18 -5
  88. package/project/.sdk/src/cmp/ts/ReadmeTopQuick_ts.ts +19 -5
  89. package/project/.sdk/src/cmp/ts/TestDirect_ts.ts +67 -7
  90. package/project/.sdk/src/cmp/ts/TestEntity_ts.ts +27 -4
  91. package/project/.sdk/src/cmp/ts/fragment/Direct.test.fragment.ts +9 -2
  92. package/project/.sdk/src/cmp/ts/fragment/Entity.test.fragment.ts +9 -2
  93. package/project/.sdk/src/cmp/ts/fragment/Main.fragment.ts +7 -0
  94. package/project/.sdk/src/cmp/zig/Config_zig.ts +5 -1
  95. package/project/.sdk/tm/c/utility/make_options.c +22 -0
  96. package/project/.sdk/tm/clojure/src/sdk/core.clj +18 -1
  97. package/project/.sdk/tm/cpp/utility/pipeline.hpp +23 -0
  98. package/project/.sdk/tm/csharp/test/CustomUtilityTest.cs +15 -0
  99. package/project/.sdk/tm/csharp/utility/MakeOptions.cs +23 -0
  100. package/project/.sdk/tm/dart/lib/utility/MakeOptionsUtility.dart +22 -0
  101. package/project/.sdk/tm/elixir/lib/projectname/utility.ex +19 -0
  102. package/project/.sdk/tm/go/core/context.go +1 -1
  103. package/project/.sdk/tm/go/feature/cost_feature.go +1 -1
  104. package/project/.sdk/tm/go/feature/paging_feature.go +5 -5
  105. package/project/.sdk/tm/go/feature/test_feature.go +12 -5
  106. package/project/.sdk/tm/go/test/custom_utility_test.go +15 -0
  107. package/project/.sdk/tm/go/test/feature_corpus_test.go +7 -0
  108. package/project/.sdk/tm/go/test/runner_test.go +46 -0
  109. package/project/.sdk/tm/go/test/struct_runner_test.go +7 -7
  110. package/project/.sdk/tm/go/test/struct_utility_test.go +63 -27
  111. package/project/.sdk/tm/go/utility/fetcher.go +2 -2
  112. package/project/.sdk/tm/go/utility/make_options.go +24 -5
  113. package/project/.sdk/tm/go/utility/make_point.go +1 -1
  114. package/project/.sdk/tm/go/utility/make_spec.go +17 -1
  115. package/project/.sdk/tm/go/utility/prepare_auth.go +1 -1
  116. package/project/.sdk/tm/go/utility/struct/voxgigstruct.go +370 -307
  117. package/project/.sdk/tm/go/utility/transform_request.go +29 -1
  118. package/project/.sdk/tm/go/utility/transform_response.go +20 -1
  119. package/project/.sdk/tm/java/test/CustomUtilityTest.java +10 -0
  120. package/project/.sdk/tm/java/test/FeatureCorpusTest.java +7 -0
  121. package/project/.sdk/tm/java/test/PrimaryUtilityTest.java +6 -0
  122. package/project/.sdk/tm/java/test/RunnerSupport.java +43 -0
  123. package/project/.sdk/tm/java/utility/MakeOptions.java +84 -1
  124. package/project/.sdk/tm/js/src/utility/MakeOptionsUtility.js +24 -0
  125. package/project/.sdk/tm/js/src/utility/StructUtility.js +2076 -1837
  126. package/project/.sdk/tm/js/test/feature/Corpus.test.js +9 -0
  127. package/project/.sdk/tm/kotlin/utility/MakeOptions.kt +21 -0
  128. package/project/.sdk/tm/lean/src/SdkUtility.lean +12 -1
  129. package/project/.sdk/tm/ocaml/sdk_runtime.ml +11 -0
  130. package/project/.sdk/tm/perl/utility/make_options.pm +18 -0
  131. package/project/.sdk/tm/php/test/FeatureCorpusTest.php +10 -1
  132. package/project/.sdk/tm/php/utility/MakeOptions.php +24 -0
  133. package/project/.sdk/tm/py/pkg/utility/make_options.py +20 -0
  134. package/project/.sdk/tm/py/test/runner.py +32 -0
  135. package/project/.sdk/tm/py/test/test_feature_corpus.py +10 -1
  136. package/project/.sdk/tm/rb/test/feature_corpus_test.rb +9 -1
  137. package/project/.sdk/tm/rb/utility/make_options.rb +17 -0
  138. package/project/.sdk/tm/rust/utility/make_options.rs +25 -0
  139. package/project/.sdk/tm/scala/utility/Make.scala +17 -0
  140. package/project/.sdk/tm/swift/Sources/ProjectNameSDK/utility/MakeOptions.swift +21 -0
  141. package/project/.sdk/tm/ts/src/feature/secrets/SecretsFeature.ts +419 -0
  142. package/project/.sdk/tm/ts/src/feature/secrets/sekreto/Providers.ts +1221 -0
  143. package/project/.sdk/tm/ts/src/feature/secrets/sekreto/Sekreto.ts +381 -0
  144. package/project/.sdk/tm/ts/src/feature/secrets/sekreto/Sigv4.ts +151 -0
  145. package/project/.sdk/tm/ts/src/feature/secrets/sekreto/index.ts +42 -0
  146. package/project/.sdk/tm/ts/src/utility/MakeOptionsUtility.ts +18 -0
  147. package/project/.sdk/tm/ts/src/utility/StructUtility.ts +1321 -710
  148. package/project/.sdk/tm/ts/test/feature/Corpus.test.ts +9 -0
  149. package/project/.sdk/tm/ts/test/feature/secrets/Secrets.test.ts +764 -0
  150. package/project/.sdk/tm/ts/test/omni.test.ts +84 -0
  151. package/project/.sdk/tm/ts/test/omni.ts +86 -0
  152. package/project/.sdk/tm/ts/test/utility/PrimaryUtility.test.ts +6 -2
  153. package/project/.sdk/tm/ts/test/utility/StructUtility.test.ts +2 -2
  154. package/project/.sdk/tm/ts/test/utility.ts +122 -0
  155. package/project/.sdk/tm/ts/test/vendor/omni/Runner.ts +692 -0
  156. package/project/.sdk/tm/ts/test/vendor/omni/Util.ts +238 -0
  157. package/project/.sdk/tm/ts/test/vendor/omni/compat.ts +188 -0
  158. package/project/.sdk/tm/ts/test/vendor/omni/index.ts +38 -0
  159. package/project/.sdk/tm/zig/core/utility.zig +23 -0
  160. package/project/sdkgen-package.json +2 -1
  161. package/src/action/check.ts +18 -0
  162. package/src/cmp/AgentGuide.ts +1 -1
  163. package/src/cmp/AgentGuideContent.ts +9 -2
  164. package/src/cmp/Feature.ts +19 -0
  165. package/src/cmp/FeatureDocs.ts +7 -1
  166. package/src/cmp/ReadmeExplanation.ts +4 -1
  167. package/src/cmp/ReadmeFeatures.ts +1 -1
  168. package/src/cmp/ReadmeRefFeatures.ts +1 -1
  169. package/src/helpers/applicability.ts +123 -0
  170. package/src/helpers/collectDeps.ts +5 -1
  171. package/src/helpers/serverVars.ts +19 -0
  172. package/src/sdkgen.ts +9 -1
  173. package/src/utility.ts +10 -1
  174. package/project/.sdk/tm/ts/test/runner.ts +0 -425
@@ -30,6 +30,8 @@ import {
30
30
  cmp,
31
31
  each,
32
32
  isAuthActive,
33
+ serverVarEnv,
34
+ serverVariables,
33
35
  isHttpBasicAuth,
34
36
  entityDataIdField, envName, envToken
35
37
  } from '@voxgig/sdkgen'
@@ -67,7 +69,7 @@ const TestEntity = cmp(function TestEntity(props: any) {
67
69
  const authActive = isAuthActive(model)
68
70
  const authBasic = authActive && isHttpBasicAuth(model)
69
71
  const apikeyEnvEntry = authActive
70
- ? `\n '${PROJENVNAME}_APIKEY': 'NONE',${authBasic ? `\n '${PROJENVNAME}_SECRET': 'NONE',` : ''}`
72
+ ? `\n '${PROJENVNAME}_APIKEY': '',${authBasic ? `\n '${PROJENVNAME}_SECRET': 'NONE',` : ''}`
71
73
  : ''
72
74
  const apikeyLiveField = authActive
73
75
  ? `
@@ -75,6 +77,19 @@ const TestEntity = cmp(function TestEntity(props: any) {
75
77
  secret: env.${PROJENVNAME}_SECRET,` : ''}`
76
78
  : ''
77
79
 
80
+ // A templated server URL (OpenAPI server variables) makes a LIVE client
81
+ // impossible to construct without values: makeOptions raises rather than
82
+ // request a URL with a literal `{account_id}` in it. So the live suite
83
+ // takes them from the environment the same way it takes the apikey.
84
+ const svars = serverVariables(model)
85
+ const serverEnvEntry = svars
86
+ .map((v: any) => `\n '${serverVarEnv(PROJENVNAME, v.name)}': ${JSON.stringify(v.dflt)},`).join('')
87
+ const serverLiveField = 0 === svars.length ? '' : `
88
+ server: {${svars
89
+ .map((v: any) => `
90
+ ${v.name}: env.${serverVarEnv(PROJENVNAME, v.name)},`).join('')}
91
+ },`
92
+
78
93
  // TODO: should be a utility function
79
94
  const ff = projectPath('src/cmp/ts/fragment/')
80
95
 
@@ -156,7 +171,7 @@ function basicSetup(extra?: any) {
156
171
  const env = envOverride({
157
172
  '${PROJENVNAME}_TEST_${ENTENVNAME}_ENTID': idmap,
158
173
  '${PROJENVNAME}_TEST_LIVE': 'FALSE',
159
- '${PROJENVNAME}_TEST_EXPLAIN': 'FALSE',${apikeyEnvEntry}
174
+ '${PROJENVNAME}_TEST_EXPLAIN': 'FALSE',${apikeyEnvEntry}${serverEnvEntry}
160
175
  })
161
176
 
162
177
  idmap = env['${PROJENVNAME}_TEST_${ENTENVNAME}_ENTID']
@@ -165,9 +180,17 @@ function basicSetup(extra?: any) {
165
180
 
166
181
  if (live) {
167
182
  client = new ${model.Name}SDK(merge([
168
- {${apikeyLiveField}
183
+ // FIRST, so the generated fields below win: sdk-test-control.json's
184
+ // test.client.options adds to the live client, it does not redirect it.
185
+ liveClientOptions(),
186
+ {${apikeyLiveField}${serverLiveField}
169
187
  },
170
- extra
188
+ // 'extra || {}', not a bare 'extra': struct.merge returns UNDEFINED when the
189
+ // last entry is undefined, and basicSetup is normally called with no
190
+ // argument at all - so a bare 'extra' silently discarded the apikey
191
+ // and server values above and handed the SDK undefined. Harmless
192
+ // while there was nothing in that object; not harmless now.
193
+ extra || {}
171
194
  ]))
172
195
  }
173
196
 
@@ -1,6 +1,4 @@
1
1
 
2
- const envlocal = __dirname + '/../../../.env.local'
3
- require('dotenv').config({ quiet: true, path: [envlocal] })
4
2
 
5
3
  import { test, describe, afterEach } from 'node:test'
6
4
  import assert from 'node:assert'
@@ -10,12 +8,21 @@ import { ProjectNameSDK } from '../../..'
10
8
 
11
9
  import {
12
10
  envOverride,
11
+ liveClientOptions,
13
12
  liveDelay,
13
+ loadEnvLocal,
14
14
  maybeSkipControl,
15
15
  skipIfMissingIds,
16
16
  } from '../../utility'
17
17
 
18
18
 
19
+ // AFTER the imports on purpose: TypeScript hoists `import` above any
20
+ // statement in the emitted CommonJS, so a loader placed above them would
21
+ // run only after every imported module had already been evaluated - and
22
+ // anything reading process.env at module scope would miss these values.
23
+ loadEnvLocal(__dirname + '/../../../.env.local')
24
+
25
+
19
26
  describe('EntityNameDirect', async () => {
20
27
 
21
28
  // Per-test live pacing. Delay is read from sdk-test-control.json's
@@ -1,6 +1,4 @@
1
1
 
2
- const envlocal = __dirname + '/../../../.env.local'
3
- require('dotenv').config({ quiet: true, path: [envlocal] })
4
2
 
5
3
  import Path from 'node:path'
6
4
  import * as Fs from 'node:fs'
@@ -13,7 +11,9 @@ import { ProjectNameSDK, BaseFeature, stdutil } from '../../..'
13
11
 
14
12
  import {
15
13
  envOverride,
14
+ liveClientOptions,
16
15
  liveDelay,
16
+ loadEnvLocal,
17
17
  makeCtrl,
18
18
  makeMatch,
19
19
  makeReqdata,
@@ -23,6 +23,13 @@ import {
23
23
  } from '../../utility'
24
24
 
25
25
 
26
+ // AFTER the imports on purpose: TypeScript hoists `import` above any
27
+ // statement in the emitted CommonJS, so a loader placed above them would
28
+ // run only after every imported module had already been evaluated - and
29
+ // anything reading process.env at module scope would miss these values.
30
+ loadEnvLocal(__dirname + '/../../../.env.local')
31
+
32
+
26
33
  describe('EntityNameEntity', async () => {
27
34
 
28
35
  // Per-test live pacing. Delay is read from sdk-test-control.json's
@@ -9,6 +9,7 @@ import { Utility } from './utility/Utility'
9
9
 
10
10
 
11
11
  import { BaseFeature } from './feature/base/BaseFeature'
12
+ // #SecretsImport
12
13
 
13
14
 
14
15
  const stdutil = new Utility()
@@ -20,6 +21,7 @@ class ProjectNameSDK {
20
21
  _utility = new Utility()
21
22
  _features: Feature[]
22
23
  _rootctx: Context
24
+ // #SecretsField
23
25
 
24
26
  constructor(options?: any) {
25
27
 
@@ -92,6 +94,8 @@ class ProjectNameSDK {
92
94
  return this._utility.struct.clone(this._utility)
93
95
  }
94
96
 
97
+ // #SecretsAccessor
98
+
95
99
 
96
100
  async prepare(fetchargs?: any) {
97
101
  const utility = this._utility
@@ -138,6 +142,8 @@ class ProjectNameSDK {
138
142
  }
139
143
  }
140
144
 
145
+ // #SecretsResolve
146
+
141
147
  // Apply SDK auth (apikey, auth prefix, etc.)
142
148
  const authResult = prepareAuth(ctx)
143
149
  if (authResult instanceof Error) {
@@ -334,6 +340,7 @@ const SDK = ProjectNameSDK
334
340
  export {
335
341
  stdutil,
336
342
  config,
343
+ // #SecretsExport
337
344
 
338
345
  BaseFeature,
339
346
  ProjectNameEntityBase,
@@ -8,6 +8,7 @@ import {
8
8
  each,
9
9
  isAuthActive,
10
10
  isConfigData,
11
+ targetFeatures,
11
12
  } from '@voxgig/sdkgen'
12
13
 
13
14
 
@@ -32,7 +33,10 @@ const Config = cmp(async function Config(props: any) {
32
33
 
33
34
  const model: Model = ctx$.model
34
35
 
35
- const feature = getModelPath(model, `main.${KIT}.feature`)
36
+ // Gated by the applicability tags, so this target never imports or
37
+ // registers a feature it has no source for. One rule, one place:
38
+ // helpers/applicability.
39
+ const feature = targetFeatures(model, target)
36
40
 
37
41
  // The canonical config OBJECT and its JSON, from the shared helper. Both
38
42
  // representations render from the same `def`, so they cannot describe
@@ -22,6 +22,23 @@ voxgig_value* make_options_util(Context* ctx) {
22
22
  }
23
23
  }
24
24
 
25
+ /* `auth: null` is the documented way to disable auth outright, and
26
+ * prepare_auth honours it before it ever reads the apikey. validate would
27
+ * erase it: this port follows the Group A rule, so a stored null reads as
28
+ * "no value" and the optspec's `auth` default fires instead - transmitting
29
+ * the credential the caller withheld. Put the null back afterwards.
30
+ *
31
+ * Unlike js, no delete-before-validate is needed here: Group A means
32
+ * validate DEFAULTS rather than rejecting, so the key can travel through.
33
+ *
34
+ * Read the map DIRECTLY rather than through getp, which applies that same
35
+ * rule and so cannot tell an absent auth from a suppressed one. */
36
+ bool auth_suppressed = false;
37
+ if (voxgig_is_map(options)) {
38
+ voxgig_value* authval = voxgig_map_get(voxgig_as_map(options), "auth");
39
+ auth_suppressed = (NULL != authval && voxgig_is_null(authval));
40
+ }
41
+
25
42
  voxgig_value* opts = voxgig_clone(options);
26
43
 
27
44
  // Feature add-order. options.feature may be an ordered list of
@@ -93,6 +110,11 @@ voxgig_value* make_options_util(Context* ctx) {
93
110
  opts = validated;
94
111
  }
95
112
 
113
+ /* Restore the suppression the optspec default would otherwise erase. */
114
+ if (auth_suppressed) {
115
+ setp(opts, "auth", voxgig_new_null());
116
+ }
117
+
96
118
  // Restore system.fetch.
97
119
  if (!v_is_noval(sys_fetch)) {
98
120
  voxgig_value* sys = getp(opts, "system");
@@ -988,7 +988,22 @@
988
988
  (if (and member (not= member :custom) (contains? members member))
989
989
  (oset! util member v)
990
990
  (.put ^java.util.Map (oget util :custom) k v))))))
991
- (let [opts0 (let [c (vs/clone options)] (if (vs/ismap c) c (vs/jm)))
991
+ (let [;; `auth` nil is the documented way to disable auth outright, and
992
+ ;; u-prepare-auth honours it before it ever reads the apikey. It
993
+ ;; cannot survive validate: a stored null reads as "no value", so the
994
+ ;; optspec's `auth` default fires and the suppression silently
995
+ ;; becomes "use default auth" - transmitting the credential the
996
+ ;; caller withheld. Withhold the key for validate, then put the nil
997
+ ;; back. Same fix as ts/js/go makeOptions.
998
+ ;;
999
+ ;; `.containsKey` rather than a nil check on the value: the latter
1000
+ ;; cannot tell an ABSENT auth from a suppressed one, and only the
1001
+ ;; second is a suppression.
1002
+ auth-suppressed (and (vs/ismap options)
1003
+ (.containsKey ^java.util.Map options "auth")
1004
+ (nil? (.get ^java.util.Map options "auth")))
1005
+ opts0 (let [c (vs/clone options)] (if (vs/ismap c) c (vs/jm)))
1006
+ _ (when auth-suppressed (.remove ^java.util.Map opts0 "auth"))
992
1007
  ;; Feature add-order. options.feature may be given as an ordered
993
1008
  ;; ARRAY of {name active ...opts} entries (array position = add
994
1009
  ;; order) or a {name {opts}} map. Normalize an array to a map (so
@@ -1037,6 +1052,8 @@
1037
1052
  merged (vs/merge (vs/jt (vs/jm) cfgopts opts0))
1038
1053
  validated (vs/validate merged optspec)
1039
1054
  opts (if (vs/ismap validated) validated (vs/jm))
1055
+ ;; Restore the suppression the optspec default would otherwise erase.
1056
+ _ (when auth-suppressed (.put ^java.util.Map opts "auth" nil))
1040
1057
  ;; Resolve a templated base URL (e.g. https://{tenant_id}.hanko.io).
1041
1058
  ;; Every placeholder must resolve to a non-empty value: from
1042
1059
  ;; options["server"] (user), else the config default. A placeholder
@@ -1107,8 +1107,26 @@ inline Value makeOptions(CtxPtr ctx) {
1107
1107
  }
1108
1108
  }
1109
1109
 
1110
+ // `auth: null` is the documented way to disable auth outright, and
1111
+ // prepareAuth honours it before it ever reads the apikey. It cannot survive
1112
+ // validate: this port follows the Group A rule, so a stored null reads as
1113
+ // "no value" and the optspec's `auth` default fires instead - transmitting
1114
+ // the credential the caller withheld. Withhold the key for validate, then
1115
+ // put the null back. Same fix as ts/js/go makeOptions.
1116
+ //
1117
+ // mapget rather than getp: getp applies Group A and collapses a stored null
1118
+ // to undef, so it cannot tell an ABSENT auth from a suppressed one. And the
1119
+ // test is is_null, NOT is_nullish - a present-but-UNDEF auth is a missing
1120
+ // value, which the ts/js reference lets the optspec default fill in. Only an
1121
+ // explicit null is a suppression.
1122
+ bool authSuppressed = mapget(options, "auth").is_null();
1123
+
1110
1124
  Value opts = Struct::clone(options);
1111
1125
 
1126
+ if (authSuppressed) {
1127
+ map_remove(opts, "auth");
1128
+ }
1129
+
1112
1130
  // Feature add-order. options.feature may be an ordered list of
1113
1131
  // { name, active, ...opts } entries (the list position IS the order in which
1114
1132
  // features are added), or a { name: {opts} } map. Normalize a list to a map
@@ -1159,6 +1177,11 @@ inline Value makeOptions(CtxPtr ctx) {
1159
1177
  Value validated = Struct::validate(merged, optspec, vopts);
1160
1178
  opts = validated;
1161
1179
 
1180
+ // Restore the suppression the optspec default would otherwise erase.
1181
+ if (authSuppressed) {
1182
+ map_put(opts, "auth", Value(nullptr));
1183
+ }
1184
+
1162
1185
  if (!is_nullish(sysFetch)) {
1163
1186
  Value sys = Helpers::toMapAny(getp(opts, "system"));
1164
1187
  if (sys.is_map()) {
@@ -85,8 +85,13 @@ public class CustomUtilityTest
85
85
  // transport: 'base' - it REPLACES the transport by design - so a client
86
86
  // in test mode would shadow the scripted fetcher and this would assert
87
87
  // nothing.
88
+ // ["test"] is the OPTION, not the `test` FEATURE: it says "this
89
+ // client is not live", so a REQUIRED OpenAPI server variable resolves
90
+ // to a deterministic test-<name> instead of failing construction. It
91
+ // installs no transport, so the override under test still stands.
88
92
  var client = new ProjectNameSDK(new Dictionary<string, object?>
89
93
  {
94
+ ["test"] = new Dictionary<string, object?> { ["active"] = true },
90
95
  ["utility"] = new Dictionary<string, object?> { ["fetcher"] = scripted },
91
96
  });
92
97
 
@@ -122,8 +127,13 @@ public class CustomUtilityTest
122
127
  };
123
128
  };
124
129
 
130
+ // ["test"] is the OPTION, not the `test` FEATURE: it says "this
131
+ // client is not live", so a REQUIRED OpenAPI server variable resolves
132
+ // to a deterministic test-<name> instead of failing construction. It
133
+ // installs no transport, so the override under test still stands.
125
134
  var client = new ProjectNameSDK(new Dictionary<string, object?>
126
135
  {
136
+ ["test"] = new Dictionary<string, object?> { ["active"] = true },
127
137
  ["utility"] = new Dictionary<string, object?> { ["fetcher"] = scripted },
128
138
  });
129
139
 
@@ -141,8 +151,13 @@ public class CustomUtilityTest
141
151
  [Fact]
142
152
  public void AnUnknownKeyIsStillAttached()
143
153
  {
154
+ // ["test"] is the OPTION, not the `test` FEATURE: it says "this
155
+ // client is not live", so a REQUIRED OpenAPI server variable resolves
156
+ // to a deterministic test-<name> instead of failing construction. It
157
+ // installs no transport, so the override under test still stands.
144
158
  var client = new ProjectNameSDK(new Dictionary<string, object?>
145
159
  {
160
+ ["test"] = new Dictionary<string, object?> { ["active"] = true },
146
161
  ["utility"] = new Dictionary<string, object?>
147
162
  {
148
163
  ["notAUtilityMember"] = Util("EXTRA"),
@@ -114,9 +114,26 @@ public static partial class SdkUtility
114
114
  }
115
115
  }
116
116
 
117
+ // `auth: null` is the documented way to disable auth outright, and
118
+ // PrepareAuth honours it before it ever reads the apikey. It cannot
119
+ // survive validate: depending on the struct port a stored null is
120
+ // either REPLACED by the optspec default - transmitting the credential
121
+ // the caller withheld - or REJECTED outright. Withhold the key for
122
+ // validate, then put the null back. Same fix as ts/js/go makeOptions.
123
+ //
124
+ // Suppliedness cannot be recovered after validate, hence here, and it
125
+ // must tell an ABSENT auth from a present null: TryGetValue rather
126
+ // than an indexer null check, which cannot distinguish them.
127
+ var authSuppressed = options.TryGetValue("auth", out var authval) && null == authval;
128
+
117
129
  var opts = StructUtils.Clone(options) as Dictionary<string, object?>
118
130
  ?? new Dictionary<string, object?>();
119
131
 
132
+ if (authSuppressed)
133
+ {
134
+ opts.Remove("auth");
135
+ }
136
+
120
137
  // Feature add-order. options.feature may be given as an ordered LIST of
121
138
  // { name, active, ...opts } entries (the list position IS the order in
122
139
  // which features are added), or as a { name: {opts} } map. Normalize a
@@ -230,6 +247,12 @@ public static partial class SdkUtility
230
247
  var validated = StructUtils.Validate(merged, optspec);
231
248
  opts = validated as Dictionary<string, object?> ?? new Dictionary<string, object?>();
232
249
 
250
+ // Restore the suppression the optspec default would otherwise erase.
251
+ if (authSuppressed)
252
+ {
253
+ opts["auth"] = null;
254
+ }
255
+
233
256
  // Resolve a templated base URL (e.g. https://{tenant_id}.hanko.io).
234
257
  // Every placeholder must resolve to a non-empty value: from
235
258
  // options.server (user), else the Config default. A placeholder that
@@ -106,12 +106,34 @@ dynamic makeOptions(dynamic ctx) {
106
106
  mergeOptions['feature'] = fmap;
107
107
  }
108
108
 
109
+ // `auth: null` is the documented way to disable auth outright, and
110
+ // prepareAuth honours it before it ever reads the apikey. It cannot survive
111
+ // validate: depending on the struct port a stored null is either REPLACED
112
+ // by the optspec default — transmitting the credential the caller withheld
113
+ // — or REJECTED outright. Withhold the key for validate, then put the null
114
+ // back. Same fix as ts/js/go makeOptions.
115
+ //
116
+ // Suppliedness cannot be recovered after validate, hence here, and it must
117
+ // tell an ABSENT auth from a present null: containsKey rather than a null
118
+ // check on the value, which cannot distinguish them.
119
+ final authSuppressed =
120
+ options is Map && options.containsKey('auth') && null == options['auth'];
121
+
109
122
  // User option maps are cloned first — their (possibly narrow) literal
110
123
  // types must not constrain the merged structures.
111
124
  dynamic opts = vs.merge([{}, cfgopts, vs.clone(mergeOptions)]);
112
125
 
126
+ if (authSuppressed && opts is Map) {
127
+ opts.remove('auth');
128
+ }
129
+
113
130
  opts = vs.validate(opts, optspec);
114
131
 
132
+ // Restore the suppression the optspec default would otherwise erase.
133
+ if (authSuppressed && opts is Map) {
134
+ opts['auth'] = null;
135
+ }
136
+
115
137
  // Resolve a templated base URL (e.g. https://{tenant_id}.hanko.io).
116
138
  // Every placeholder must resolve to a non-empty value: from options.server
117
139
  // (user), else the Config default. A placeholder that resolves to '' is a
@@ -243,8 +243,24 @@ defmodule ProjectName.Utility do
243
243
  end
244
244
  end
245
245
 
246
+ # `auth` nil is the documented way to disable auth outright, and
247
+ # prepare_auth honours it before it ever reads the apikey. It cannot
248
+ # survive validate: a stored null reads as "no value", so the optspec's
249
+ # `auth` default fires and the suppression silently becomes "use default
250
+ # auth" - transmitting the credential the caller withheld. Withhold the
251
+ # key for validate, then put the nil back. Same fix as ts/js/go
252
+ # makeOptions.
253
+ #
254
+ # keysof rather than S.haskey: haskey is `getprop(...) != nil`, which
255
+ # collapses a stored null and so cannot tell an ABSENT auth from a
256
+ # suppressed one. keysof lists the key either way.
257
+ auth_suppressed =
258
+ S.ismap(options) and "auth" in S.keysof(options) and
259
+ S.getprop(options, "auth") == nil
260
+
246
261
  opts0 = S.clone(options)
247
262
  opts0 = if S.ismap(opts0), do: opts0, else: S.jm([])
263
+ if auth_suppressed, do: S.delprop(opts0, "auth")
248
264
 
249
265
  # Feature add-order. options.feature may be given as an ordered LIST of
250
266
  # {name, active, ...opts} entries (list position = add order) or a
@@ -329,6 +345,9 @@ defmodule ProjectName.Utility do
329
345
  validated = S.validate(merged, optspec)
330
346
  opts = if S.ismap(validated), do: validated, else: S.jm([])
331
347
 
348
+ # Restore the suppression the optspec default would otherwise erase.
349
+ if auth_suppressed, do: S.setprop(opts, "auth", nil)
350
+
332
351
  # Resolve a templated base URL (e.g. https://{tenant_id}.hanko.io).
333
352
  # Every placeholder must resolve to a non-empty value: from options.server
334
353
  # (user), else the Config default. A placeholder that resolves to "" is a
@@ -245,7 +245,7 @@ func (ctx *Context) resolveOp(opname string) *Operation {
245
245
  return NewOperation(map[string]any{})
246
246
  }
247
247
 
248
- opcfg := vs.GetPath([]any{"entity", entname, "op", opname}, ctx.Config)
248
+ opcfg := vs.GetPath(ctx.Config, []any{"entity", entname, "op", opname})
249
249
 
250
250
  input := "match"
251
251
  if opname == "update" || opname == "create" {
@@ -367,7 +367,7 @@ func (f *CostFeature) body(ctx *core.Context) (float64, bool) {
367
367
  return 0, false
368
368
  }
369
369
 
370
- switch n := vs.GetPath(path, ctx.Result.Body).(type) {
370
+ switch n := vs.GetPath(ctx.Result.Body, path).(type) {
371
371
  case int:
372
372
  return float64(n) * f.perUnit(), true
373
373
  case int64:
@@ -109,7 +109,7 @@ func (f *PagingFeature) graphqlPreRequest(ctx *core.Context, paging map[string]a
109
109
  // Only bind variables the operation actually declares, or the server
110
110
  // rejects the document.
111
111
  declared := map[string]bool{}
112
- if varlist, ok := vs.GetPath([]any{"graphql", "vars"}, ctx.Point).([]any); ok {
112
+ if varlist, ok := vs.GetPath(ctx.Point, []any{"graphql", "vars"}).([]any); ok {
113
113
  for _, v := range varlist {
114
114
  if name, ok := vs.GetProp(v, "name").(string); ok {
115
115
  declared[name] = true
@@ -160,7 +160,7 @@ func (f *PagingFeature) PreResult(ctx *core.Context) {
160
160
 
161
161
  // Relay connections carry the cursor in pageInfo, at the path the model
162
162
  // recorded for this op.
163
- page := vs.GetPath([]any{"graphql", "page"}, ctx.Point)
163
+ page := vs.GetPath(ctx.Point, []any{"graphql", "page"})
164
164
  if pm, ok := page.(map[string]any); ok {
165
165
  if _, isMap := body.(map[string]any); isMap {
166
166
  // `connpath` locates the connection object inside the response
@@ -168,18 +168,18 @@ func (f *PagingFeature) PreResult(ctx *core.Context) {
168
168
  // to it.
169
169
  conn := body
170
170
  if cp, ok := pm["connpath"].(string); ok && cp != "" {
171
- if sub := vs.GetPath(cp, body); sub != nil {
171
+ if sub := vs.GetPath(body, cp); sub != nil {
172
172
  conn = sub
173
173
  }
174
174
  }
175
175
 
176
176
  if cp, ok := pm["cursor"].(string); ok && cp != "" {
177
- if cursor := vs.GetPath(cp, conn); cursor != nil {
177
+ if cursor := vs.GetPath(conn, cp); cursor != nil {
178
178
  paging["cursor"] = cursor
179
179
  }
180
180
  }
181
181
  if mp, ok := pm["more"].(string); ok && mp != "" {
182
- if more, ok := vs.GetPath(mp, conn).(bool); ok {
182
+ if more, ok := vs.GetPath(conn, mp).(bool); ok {
183
183
  paging["hasMore"] = more
184
184
  explicitMore = true
185
185
  }
@@ -323,7 +323,7 @@ func (f *TestFeature) buildArgs(ctx *core.Context, op *core.Operation, args map[
323
323
  // back to, so the seed-data query is built from the endpoint the request
324
324
  // will actually be sent to: a terminal `{id}` marks a record route, and
325
325
  // failing that the shallower path wins.
326
- points := vs.GetPath([]any{"entity", ctx.Entity.GetName(), "op", opname, "points"}, ctx.Config)
326
+ points := vs.GetPath(ctx.Config, []any{"entity", ctx.Entity.GetName(), "op", opname, "points"})
327
327
  point := vs.GetElem(points, 0)
328
328
  if plist, ok := points.([]any); ok {
329
329
  partsLen := func(p any) int {
@@ -355,12 +355,19 @@ func (f *TestFeature) buildArgs(ctx *core.Context, op *core.Operation, args map[
355
355
 
356
356
  // Path AND query: a path-only read misses a query-addressed record
357
357
  // (e.g. GET /result?trace_id=), which has no path param at all.
358
- paramsPath := vs.GetPath([]any{"args", "params"}, point)
358
+ paramsPath := vs.GetPath(point, []any{"args", "params"})
359
359
  reqdParams := vs.Select(paramsPath, map[string]any{"reqd": true})
360
- reqdFromParams := vs.Transform(reqdParams, []any{"`$EACH`", "", "`$KEY.name`"})
361
- queryPath := vs.GetPath([]any{"args", "query"}, point)
360
+ // The error return Transform gained in struct go 0.1.3 is discarded
361
+ // DELIBERATELY here, unlike in transform_request/response: these two
362
+ // transform a spec the SDK itself built from the point definition, not
363
+ // user data, so an error means a generator bug rather than something a
364
+ // caller can act on — and buildArgs has no error seam to route it
365
+ // through. An empty result is the conservative outcome: no required
366
+ // fields, which is what an absent spec already produces.
367
+ reqdFromParams, _ := vs.Transform(reqdParams, []any{"`$EACH`", "", "`$KEY.name`"})
368
+ queryPath := vs.GetPath(point, []any{"args", "query"})
362
369
  reqdQuery := vs.Select(queryPath, map[string]any{"reqd": true})
363
- reqdFromQuery := vs.Transform(reqdQuery, []any{"`$EACH`", "", "`$KEY.name`"})
370
+ reqdFromQuery, _ := vs.Transform(reqdQuery, []any{"`$EACH`", "", "`$KEY.name`"})
364
371
 
365
372
  qand := []any{}
366
373
  q := map[string]any{"`$AND`": &qand}
@@ -109,7 +109,12 @@ func TestCustomUtility(t *testing.T) {
109
109
  // that was broken: a plain function literal in a map[string]any has the
110
110
  // unnamed signature, so an override that only accepted the named type
111
111
  // shelved it in Custom. Converting here would have hidden that.
112
+ // "test" is the OPTION, not the `test` FEATURE: it says "this client
113
+ // is not live", so a REQUIRED OpenAPI server variable resolves to a
114
+ // deterministic test-<name> instead of panicking at construction. It
115
+ // installs no transport, so the override under test still stands.
112
116
  client := sdk.NewProjectNameSDK(map[string]any{
117
+ "test": map[string]any{"active": true},
113
118
  "utility": map[string]any{"fetcher": scripted},
114
119
  })
115
120
 
@@ -138,7 +143,12 @@ func TestCustomUtility(t *testing.T) {
138
143
  // the subtest above, and broken by any fix that swaps one for the other.
139
144
  t.Run("a converted fetcher is accepted too", func(t *testing.T) {
140
145
  reached := 0
146
+ // "test" is the OPTION, not the `test` FEATURE: it says "this client
147
+ // is not live", so a REQUIRED OpenAPI server variable resolves to a
148
+ // deterministic test-<name> instead of panicking at construction. It
149
+ // installs no transport, so the override under test still stands.
141
150
  client := sdk.NewProjectNameSDK(map[string]any{
151
+ "test": map[string]any{"active": true},
142
152
  "utility": map[string]any{
143
153
  "fetcher": sdk.FetcherFunc(func(
144
154
  ctx *sdk.Context, fullurl string, fetchdef map[string]any,
@@ -171,7 +181,12 @@ func TestCustomUtility(t *testing.T) {
171
181
  // An unknown key must still be attached rather than dropped, so the two
172
182
  // halves cannot be satisfied by a switch that also swallows extras.
173
183
  t.Run("an unknown key is still attached", func(t *testing.T) {
184
+ // "test" is the OPTION, not the `test` FEATURE: it says "this client
185
+ // is not live", so a REQUIRED OpenAPI server variable resolves to a
186
+ // deterministic test-<name> instead of panicking at construction. It
187
+ // installs no transport, so the override under test still stands.
174
188
  client := sdk.NewProjectNameSDK(map[string]any{
189
+ "test": map[string]any{"active": true},
175
190
  "utility": map[string]any{
176
191
  "notAUtilityMember": func() string { return "EXTRA" },
177
192
  },
@@ -114,7 +114,14 @@ func fcFetcher(res []any) sdk.FetcherFunc {
114
114
  // REPLACES the transport, so a client in test mode would shadow the script.
115
115
  func fcClient(kase map[string]any) *sdk.ProjectNameSDK {
116
116
  res, _ := kase["res"].([]any)
117
+ // "test" here is the OPTION, not the `test` FEATURE. It says "this
118
+ // client is not live", which is what makes a REQUIRED OpenAPI server
119
+ // variable resolve to a deterministic test-<name> rather than panic at
120
+ // construction (see makeOptions). It installs no transport, so the
121
+ // scripted fetcher still stands — the FEATURE is transport: 'base' and
122
+ // would shadow it, which is why this cannot just turn the feature on.
117
123
  opts := map[string]any{
124
+ "test": map[string]any{"active": true},
118
125
  "utility": map[string]any{"fetcher": fcFetcher(res)},
119
126
  }
120
127
  if f, ok := kase["feature"]; ok {
@@ -156,6 +156,52 @@ func isControlSkipped(kind, name, mode string) (bool, string) {
156
156
  }
157
157
 
158
158
  // liveDelayMs returns the configured per-test live delay in ms; default 500.
159
+ // liveClientOptions returns the extra SDK options every LIVE client is
160
+ // constructed with, from sdk-test-control.json `test.client.options`.
161
+ //
162
+ // The generated live client knows two things: the base URL (from the spec)
163
+ // and the credential (from the environment). Everything else about how a
164
+ // particular API wants to be talked to - which features to switch on, and
165
+ // with what settings - is a property of THAT API, known to the project and
166
+ // to nothing in the toolchain.
167
+ //
168
+ // Merged UNDER the generated fields, so the suite's own base/apikey/server
169
+ // values win: this ADDS to the live client, it does not redirect it.
170
+ //
171
+ // That contract is enforced HERE rather than left to each merge site: the
172
+ // generated map only names a field when the model calls for one, so a
173
+ // "base" in this block would face no competing value and would silently
174
+ // redirect the whole suite - credential included - to another host.
175
+ var liveReserved = map[string]bool{
176
+ "base": true, "prefix": true, "suffix": true,
177
+ "server": true, "apikey": true, "secret": true,
178
+ }
179
+
180
+ func liveClientOptions() map[string]any {
181
+ ctrl := loadTestControl()
182
+ test, _ := ctrl["test"].(map[string]any)
183
+ if test == nil {
184
+ return map[string]any{}
185
+ }
186
+ client, _ := test["client"].(map[string]any)
187
+ if client == nil {
188
+ return map[string]any{}
189
+ }
190
+ opts, _ := client["options"].(map[string]any)
191
+ if opts == nil {
192
+ return map[string]any{}
193
+ }
194
+
195
+ out := map[string]any{}
196
+ for k, v := range opts {
197
+ if !liveReserved[k] {
198
+ out[k] = v
199
+ }
200
+ }
201
+ return out
202
+ }
203
+
204
+
159
205
  func liveDelayMs() int {
160
206
  ctrl := loadTestControl()
161
207
  test, _ := ctrl["test"].(map[string]any)