@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.
- package/bin/voxgig-sdkgen +1 -1
- package/dist/cmp/Feature.js +5 -0
- package/dist/cmp/Feature.js.map +1 -1
- package/dist/cmp/TestControl.d.ts +4 -0
- package/dist/cmp/TestControl.js +51 -0
- package/dist/cmp/TestControl.js.map +1 -0
- package/dist/helpers/featureSource.d.ts +3 -1
- package/dist/helpers/featureSource.js +105 -0
- package/dist/helpers/featureSource.js.map +1 -1
- package/dist/helpers/opShape.js +8 -11
- package/dist/helpers/opShape.js.map +1 -1
- package/dist/helpers/pointPath.d.ts +10 -0
- package/dist/helpers/pointPath.js +54 -0
- package/dist/helpers/pointPath.js.map +1 -0
- package/dist/sdkgen.d.ts +6 -3
- package/dist/sdkgen.js +14 -4
- package/dist/sdkgen.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/utility.d.ts +2 -1
- package/dist/utility.js +78 -1
- package/dist/utility.js.map +1 -1
- package/model/sdkgen.aon +58 -0
- package/package.json +2 -2
- package/project/.sdk/model/feature/secrets.aon +82 -0
- package/project/.sdk/src/cmp/c/Config_c.ts +7 -8
- package/project/.sdk/src/cmp/c/TestDirect_c.ts +4 -3
- package/project/.sdk/src/cmp/cpp/Main_cpp.ts +2 -1
- package/project/.sdk/src/cmp/cpp/TestDirect_cpp.ts +4 -3
- package/project/.sdk/src/cmp/cpp/Test_cpp.ts +5 -1
- package/project/.sdk/src/cmp/csharp/Main_csharp.ts +2 -1
- package/project/.sdk/src/cmp/csharp/TestDirect_csharp.ts +34 -9
- package/project/.sdk/src/cmp/csharp/TestEntity_csharp.ts +29 -5
- package/project/.sdk/src/cmp/csharp/Test_csharp.ts +5 -1
- package/project/.sdk/src/cmp/dart/Config_dart.ts +7 -8
- package/project/.sdk/src/cmp/dart/Main_dart.ts +2 -1
- package/project/.sdk/src/cmp/dart/TestDirect_dart.ts +28 -10
- package/project/.sdk/src/cmp/dart/TestEntity_dart.ts +28 -7
- package/project/.sdk/src/cmp/dart/Test_dart.ts +5 -1
- package/project/.sdk/src/cmp/elixir/Config_elixir.ts +9 -8
- package/project/.sdk/src/cmp/go/Config_go.ts +1 -7
- package/project/.sdk/src/cmp/go/Main_go.ts +2 -1
- package/project/.sdk/src/cmp/go/TestDirect_go.ts +5 -4
- package/project/.sdk/src/cmp/go/Test_go.ts +5 -1
- package/project/.sdk/src/cmp/java/Config_java.ts +7 -7
- package/project/.sdk/src/cmp/java/Main_java.ts +2 -1
- package/project/.sdk/src/cmp/java/TestDirect_java.ts +5 -4
- package/project/.sdk/src/cmp/java/Test_java.ts +5 -1
- package/project/.sdk/src/cmp/js/Config_js.ts +7 -8
- package/project/.sdk/src/cmp/js/Main_js.ts +2 -1
- package/project/.sdk/src/cmp/js/TestDirect_js.ts +34 -8
- package/project/.sdk/src/cmp/js/TestEntity_js.ts +35 -5
- package/project/.sdk/src/cmp/js/Test_js.ts +5 -1
- package/project/.sdk/src/cmp/js/fragment/Direct.test.fragment.js +7 -1
- package/project/.sdk/src/cmp/js/fragment/Entity.test.fragment.js +7 -1
- package/project/.sdk/src/cmp/kotlin/Config_kotlin.ts +7 -7
- package/project/.sdk/src/cmp/kotlin/Main_kotlin.ts +2 -1
- package/project/.sdk/src/cmp/kotlin/TestDirect_kotlin.ts +5 -4
- package/project/.sdk/src/cmp/kotlin/Test_kotlin.ts +5 -1
- package/project/.sdk/src/cmp/lean/Config_lean.ts +9 -8
- package/project/.sdk/src/cmp/lua/Config_lua.ts +1 -7
- package/project/.sdk/src/cmp/lua/Main_lua.ts +2 -1
- package/project/.sdk/src/cmp/lua/TestDirect_lua.ts +32 -7
- package/project/.sdk/src/cmp/lua/TestEntity_lua.ts +24 -4
- package/project/.sdk/src/cmp/lua/Test_lua.ts +5 -1
- package/project/.sdk/src/cmp/ocaml/Gitignore_ocaml.ts +1 -0
- package/project/.sdk/src/cmp/perl/Main_perl.ts +2 -1
- package/project/.sdk/src/cmp/perl/TestDirect_perl.ts +29 -7
- package/project/.sdk/src/cmp/perl/TestEntity_perl.ts +24 -4
- package/project/.sdk/src/cmp/perl/Test_perl.ts +5 -1
- package/project/.sdk/src/cmp/perl/utility_perl.ts +10 -0
- package/project/.sdk/src/cmp/php/Config_php.ts +1 -7
- package/project/.sdk/src/cmp/php/Main_php.ts +2 -1
- package/project/.sdk/src/cmp/php/TestDirect_php.ts +28 -8
- package/project/.sdk/src/cmp/php/TestEntity_php.ts +28 -4
- package/project/.sdk/src/cmp/php/Test_php.ts +5 -1
- package/project/.sdk/src/cmp/py/Config_py.ts +1 -7
- package/project/.sdk/src/cmp/py/Main_py.ts +2 -1
- package/project/.sdk/src/cmp/py/TestDirect_py.ts +5 -4
- package/project/.sdk/src/cmp/py/Test_py.ts +5 -1
- package/project/.sdk/src/cmp/rb/Config_rb.ts +1 -7
- package/project/.sdk/src/cmp/rb/Main_rb.ts +2 -1
- package/project/.sdk/src/cmp/rb/TestDirect_rb.ts +28 -8
- package/project/.sdk/src/cmp/rb/TestEntity_rb.ts +24 -4
- package/project/.sdk/src/cmp/rb/Test_rb.ts +5 -1
- package/project/.sdk/src/cmp/rb/utility_rb.ts +3 -1
- package/project/.sdk/src/cmp/rust/Main_rust.ts +2 -1
- package/project/.sdk/src/cmp/rust/TestDirect_rust.ts +35 -7
- package/project/.sdk/src/cmp/rust/TestEntity_rust.ts +37 -4
- package/project/.sdk/src/cmp/rust/Test_rust.ts +5 -1
- package/project/.sdk/src/cmp/scala/Config_scala.ts +9 -7
- package/project/.sdk/src/cmp/scala/Main_scala.ts +2 -1
- package/project/.sdk/src/cmp/scala/TestDirect_scala.ts +4 -3
- package/project/.sdk/src/cmp/scala/Test_scala.ts +5 -1
- package/project/.sdk/src/cmp/seneca-provider/Extras_seneca-provider.ts +3 -1
- package/project/.sdk/src/cmp/seneca-provider/Main_seneca-provider.ts +9 -5
- package/project/.sdk/src/cmp/ts/Config_ts.ts +57 -16
- package/project/.sdk/src/cmp/ts/Main_ts.ts +10 -2
- package/project/.sdk/src/cmp/ts/TestDirect_ts.ts +15 -7
- package/project/.sdk/src/cmp/ts/TestEntity_ts.ts +11 -3
- package/project/.sdk/src/cmp/ts/Test_ts.ts +5 -1
- package/project/.sdk/tm/csharp/test/Runner.cs +45 -0
- package/project/.sdk/tm/dart/test/utility.dart +39 -0
- package/project/.sdk/tm/js/test/sdk-test-control.json +19 -0
- package/project/.sdk/tm/js/test/utility.js +91 -1
- package/project/.sdk/tm/lua/test/runner.lua +40 -0
- package/project/.sdk/tm/ocaml/test/harness.ml +14 -4
- package/project/.sdk/tm/perl/t/runner.pm +30 -0
- package/project/.sdk/tm/php/test/Runner.php +36 -0
- package/project/.sdk/tm/rb/test/runner.rb +25 -0
- package/project/.sdk/tm/rust/tests/common/mod.rs +32 -0
- package/project/.sdk/tm/rust/utility/make_options.rs +1 -1
- package/project/.sdk/tm/ts/src/feature/secrets/sekreto/Sekreto.ts +8 -2
- package/project/.sdk/tm/ts/src/feature/secrets/sekreto/Sigv4.ts +29 -5
- package/project/.sdk/tm/ts/src/feature/secrets/sekreto/index.ts +30 -21
- package/project/.sdk/tm/ts/src/feature/secrets/sekreto/provider/Registry.ts +90 -0
- package/project/.sdk/tm/ts/src/feature/secrets/sekreto/provider/addr.ts +48 -0
- package/project/.sdk/tm/ts/src/feature/secrets/sekreto/provider/aws.ts +219 -0
- package/project/.sdk/tm/ts/src/feature/secrets/sekreto/provider/azuresecrets.ts +144 -0
- package/project/.sdk/tm/ts/src/feature/secrets/sekreto/provider/boru.ts +113 -0
- package/project/.sdk/tm/ts/src/feature/secrets/sekreto/provider/doppler.ts +77 -0
- package/project/.sdk/tm/ts/src/feature/secrets/sekreto/provider/dotenv.ts +51 -0
- package/project/.sdk/tm/ts/src/feature/secrets/sekreto/provider/env.ts +31 -0
- package/project/.sdk/tm/ts/src/feature/secrets/sekreto/provider/file.ts +54 -0
- package/project/.sdk/tm/ts/src/feature/secrets/sekreto/provider/gcpsecrets.ts +118 -0
- package/project/.sdk/tm/ts/src/feature/secrets/sekreto/provider/hashicorp.ts +159 -0
- package/project/.sdk/tm/ts/src/feature/secrets/sekreto/provider/http.ts +66 -0
- package/project/.sdk/tm/ts/src/feature/secrets/sekreto/provider/infisical.ts +107 -0
- package/project/.sdk/tm/ts/src/feature/secrets/sekreto/provider/memory.ts +33 -0
- package/project/.sdk/tm/ts/src/feature/secrets/sekreto/provider/onepassword.ts +120 -0
- package/project/.sdk/tm/ts/src/feature/secrets/sekreto/provider/support.ts +176 -0
- package/project/.sdk/tm/ts/test/vendor/omni/Runner.ts +15 -13
- package/project/.sdk/tm/ts/test/vendor/omni/Util.ts +25 -19
- package/project/.sdk/tm/ts/test/vendor/omni/compat.ts +1 -1
- package/project/.sdk/tm/ts/test/vendor/omni/index.ts +1 -1
- package/project/.sdk/tm/zig/core/utility.zig +1 -1
- package/project/sdkgen-package.json +1 -1
- package/src/cmp/Feature.ts +5 -0
- package/src/cmp/TestControl.ts +64 -0
- package/src/helpers/featureSource.ts +130 -0
- package/src/helpers/opShape.ts +9 -14
- package/src/helpers/pointPath.ts +88 -0
- package/src/sdkgen.ts +18 -0
- package/src/utility.ts +92 -1
- 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("
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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("
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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': () =>
|
|
103
|
-
|
|
104
|
-
`
|
|
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': () =>
|
|
134
|
-
|
|
135
|
-
`
|
|
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
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
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,
|
|
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
|
-
|
|
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': '
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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': '
|
|
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
|
|
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
|
|
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
|
|