@voxgig/sdkgen 0.45.0 → 1.1.1
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/package.json +3 -3
- package/project/.sdk/src/cmp/go/Entity_go.ts +2 -2
- package/project/.sdk/src/cmp/go/Main_go.ts +8 -4
- package/project/.sdk/src/cmp/go/Package_go.ts +2 -2
- package/project/.sdk/src/cmp/go/ReadmeExplanation_go.ts +2 -2
- package/project/.sdk/src/cmp/go/ReadmeHowto_go.ts +2 -2
- package/project/.sdk/src/cmp/go/ReadmeInstall_go.ts +2 -2
- package/project/.sdk/src/cmp/go/ReadmeModel_go.ts +2 -2
- package/project/.sdk/src/cmp/go/ReadmeQuick_go.ts +2 -2
- package/project/.sdk/src/cmp/go/ReadmeTopQuick_go.ts +2 -2
- package/project/.sdk/src/cmp/go/ReadmeTopTest_go.ts +2 -2
- package/project/.sdk/src/cmp/go/TestDirect_go.ts +204 -33
- package/project/.sdk/src/cmp/go/TestEntity_go.ts +37 -6
- package/project/.sdk/src/cmp/go/Test_go.ts +2 -2
- package/project/.sdk/src/cmp/go/fragment/Main.fragment.go +21 -4
- package/project/.sdk/src/cmp/lua/Package_lua.ts +9 -2
- package/project/.sdk/src/cmp/lua/TestDirect_lua.ts +154 -21
- package/project/.sdk/src/cmp/lua/TestEntity_lua.ts +25 -1
- package/project/.sdk/src/cmp/lua/fragment/Main.fragment.lua +20 -4
- package/project/.sdk/src/cmp/php/Package_php.ts +7 -1
- package/project/.sdk/src/cmp/php/TestDirect_php.ts +153 -20
- package/project/.sdk/src/cmp/php/TestEntity_php.ts +25 -1
- package/project/.sdk/src/cmp/php/fragment/Main.fragment.php +17 -3
- package/project/.sdk/src/cmp/py/Package_py.ts +8 -1
- package/project/.sdk/src/cmp/py/TestDirect_py.ts +146 -19
- package/project/.sdk/src/cmp/py/TestEntity_py.ts +25 -1
- package/project/.sdk/src/cmp/py/fragment/Main.fragment.py +19 -4
- package/project/.sdk/src/cmp/rb/Package_rb.ts +9 -2
- package/project/.sdk/src/cmp/rb/TestDirect_rb.ts +154 -21
- package/project/.sdk/src/cmp/rb/TestEntity_rb.ts +25 -1
- package/project/.sdk/src/cmp/rb/fragment/Main.fragment.rb +19 -3
- package/project/.sdk/src/cmp/ts/Package_ts.ts +1 -1
- package/project/.sdk/src/cmp/ts/TestDirect_ts.ts +145 -22
- package/project/.sdk/src/cmp/ts/TestEntity_ts.ts +59 -1
- package/project/.sdk/src/cmp/ts/fragment/Direct.test.fragment.ts +8 -1
- package/project/.sdk/src/cmp/ts/fragment/Entity.test.fragment.ts +8 -2
- package/project/.sdk/src/cmp/ts/fragment/Main.fragment.ts +21 -1
- package/project/.sdk/tm/go/feature/test_feature.go +56 -3
- package/project/.sdk/tm/go/test/runner_test.go +106 -6
- package/project/.sdk/tm/go/test/sdk-test-control.json +19 -0
- package/project/.sdk/tm/go/utility/fetcher.go +10 -0
- package/project/.sdk/tm/go/utility/make_url.go +12 -0
- package/project/.sdk/tm/lua/feature/test_feature.lua +46 -3
- package/project/.sdk/tm/lua/test/runner.lua +74 -0
- package/project/.sdk/tm/lua/test/sdk-test-control.json +19 -0
- package/project/.sdk/tm/lua/utility/fetcher.lua +13 -0
- package/project/.sdk/tm/lua/utility/make_url.lua +16 -0
- package/project/.sdk/tm/php/feature/TestFeature.php +192 -43
- package/project/.sdk/tm/php/test/Runner.php +62 -0
- package/project/.sdk/tm/php/test/sdk-test-control.json +19 -0
- package/project/.sdk/tm/php/utility/Fetcher.php +132 -9
- package/project/.sdk/tm/php/utility/MakeUrl.php +16 -0
- package/project/.sdk/tm/py/feature/test_feature.py +39 -3
- package/project/.sdk/tm/py/test/runner.py +60 -0
- package/project/.sdk/tm/py/test/sdk-test-control.json +19 -0
- package/project/.sdk/tm/py/utility/fetcher.py +13 -0
- package/project/.sdk/tm/py/utility/make_url.py +13 -0
- package/project/.sdk/tm/rb/feature/test_feature.rb +37 -3
- package/project/.sdk/tm/rb/test/runner.rb +46 -0
- package/project/.sdk/tm/rb/test/sdk-test-control.json +19 -0
- package/project/.sdk/tm/rb/utility/fetcher.rb +49 -28
- package/project/.sdk/tm/rb/utility/make_url.rb +16 -0
- package/project/.sdk/tm/ts/test/sdk-test-control.json +19 -0
- package/project/.sdk/tm/ts/test/utility.ts +120 -2
package/bin/voxgig-sdkgen
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@voxgig/sdkgen",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "1.1.1",
|
|
4
4
|
"main": "dist/sdkgen.js",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"types": "dist/sdkgen.d.ts",
|
|
@@ -41,13 +41,13 @@
|
|
|
41
41
|
],
|
|
42
42
|
"devDependencies": {
|
|
43
43
|
"@types/js-yaml": "4.0.9",
|
|
44
|
-
"@types/node": "25.
|
|
44
|
+
"@types/node": "25.7.0",
|
|
45
45
|
"json-schema-to-ts": "3.1.1",
|
|
46
46
|
"memfs": "4.57.2",
|
|
47
47
|
"typescript": "6.0.3"
|
|
48
48
|
},
|
|
49
49
|
"peerDependencies": {
|
|
50
|
-
"@voxgig/apidef": ">=
|
|
50
|
+
"@voxgig/apidef": ">=6",
|
|
51
51
|
"@voxgig/struct": ">=0",
|
|
52
52
|
"@voxgig/util": ">=0",
|
|
53
53
|
"aontu": ">=0",
|
|
@@ -14,8 +14,8 @@ const Entity = cmp(function Entity(props: any) {
|
|
|
14
14
|
const { target, entity } = props
|
|
15
15
|
|
|
16
16
|
// Module name: concatenated lowercase
|
|
17
|
-
|
|
18
|
-
const gomodule =
|
|
17
|
+
// Go module path == repo path on GitHub (org from model.origin).
|
|
18
|
+
const gomodule = `github.com/${model.origin || 'voxgig-sdk'}/${model.name}-sdk`
|
|
19
19
|
|
|
20
20
|
const entrep = {
|
|
21
21
|
...stdrep,
|
|
@@ -32,9 +32,13 @@ const Main = cmp(async function Main(props: any) {
|
|
|
32
32
|
const entity: ModelEntity = getModelPath(model, `main.${KIT}.entity`)
|
|
33
33
|
const feature = getModelPath(model, `main.${KIT}.feature`)
|
|
34
34
|
|
|
35
|
-
//
|
|
36
|
-
|
|
37
|
-
const gomodule =
|
|
35
|
+
// Go module path == the repo path on GitHub (org from model.origin),
|
|
36
|
+
// e.g. github.com/voxgig-sdk/<slug>-sdk. Used in go.mod and every import.
|
|
37
|
+
const gomodule = `github.com/${model.origin || 'voxgig-sdk'}/${model.name}-sdk`
|
|
38
|
+
// The root package name must be a plain Go identifier (can't be a path),
|
|
39
|
+
// so it stays as the concatenated-lowercase form (e.g. voxgigdogsdk).
|
|
40
|
+
const gopackage = (model.origin || 'voxgig-sdk').replace(/-sdk$/, '').replace(/[^a-z0-9]/gi, '') +
|
|
41
|
+
model.name.replace(/[^a-z0-9]/gi, '').toLowerCase() + 'sdk'
|
|
38
42
|
|
|
39
43
|
Package({ target })
|
|
40
44
|
|
|
@@ -121,7 +125,7 @@ var NewBaseFeatureFunc func() Feature
|
|
|
121
125
|
const hasEntities = Object.keys(entity || {}).length > 0
|
|
122
126
|
const entityImport = hasEntities ? `\n\t"${gomodule}/entity"` : ''
|
|
123
127
|
File({ name: model.name + '.' + target.ext }, () => {
|
|
124
|
-
Content(`package ${
|
|
128
|
+
Content(`package ${gopackage}
|
|
125
129
|
|
|
126
130
|
import (
|
|
127
131
|
"${gomodule}/core"${entityImport}
|
|
@@ -19,8 +19,8 @@ const Package = cmp(async function Package(props: any) {
|
|
|
19
19
|
const model: Model = ctx$.model
|
|
20
20
|
|
|
21
21
|
// Module name: concatenated lowercase (e.g., voxgigsolardemosdk)
|
|
22
|
-
|
|
23
|
-
const gomodule =
|
|
22
|
+
// Go module path == repo path on GitHub (org from model.origin).
|
|
23
|
+
const gomodule = `github.com/${model.origin || 'voxgig-sdk'}/${model.name}-sdk`
|
|
24
24
|
|
|
25
25
|
File({ name: 'go.mod' }, () => {
|
|
26
26
|
Content(`module ${gomodule}
|
|
@@ -5,8 +5,8 @@ import { cmp, Content } from '@voxgig/sdkgen'
|
|
|
5
5
|
const ReadmeExplanation = cmp(function ReadmeExplanation(props: any) {
|
|
6
6
|
const { target, ctx$: { model } } = props
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
const gomodule =
|
|
8
|
+
// Go module path == repo path on GitHub (org from model.origin).
|
|
9
|
+
const gomodule = `github.com/${model.origin || 'voxgig-sdk'}/${model.name}-sdk`
|
|
10
10
|
|
|
11
11
|
Content(`### Data as maps
|
|
12
12
|
|
|
@@ -10,8 +10,8 @@ import {
|
|
|
10
10
|
const ReadmeHowto = cmp(function ReadmeHowto(props: any) {
|
|
11
11
|
const { target, ctx$: { model } } = props
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
const gomodule =
|
|
13
|
+
// Go module path == repo path on GitHub (org from model.origin).
|
|
14
|
+
const gomodule = `github.com/${model.origin || 'voxgig-sdk'}/${model.name}-sdk`
|
|
15
15
|
|
|
16
16
|
const apikeyEnvLine = isAuthActive(model)
|
|
17
17
|
? `\n${model.NAME}_APIKEY=<your-key>`
|
|
@@ -6,8 +6,8 @@ const ReadmeInstall = cmp(function ReadmeInstall(props: any) {
|
|
|
6
6
|
const { target, ctx$ } = props
|
|
7
7
|
const { model } = ctx$
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
const gomodule =
|
|
9
|
+
// Go module path == repo path on GitHub (org from model.origin).
|
|
10
|
+
const gomodule = `github.com/${model.origin || 'voxgig-sdk'}/${model.name}-sdk`
|
|
11
11
|
|
|
12
12
|
Content(`\`\`\`bash
|
|
13
13
|
go get ${gomodule}
|
|
@@ -13,8 +13,8 @@ const ReadmeModel = cmp(function ReadmeModel(props: any) {
|
|
|
13
13
|
const entity = getModelPath(model, `main.${KIT}.entity`)
|
|
14
14
|
const entityList = each(entity).filter((e: any) => e.active !== false)
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
const gomodule =
|
|
16
|
+
// Go module path == repo path on GitHub (org from model.origin).
|
|
17
|
+
const gomodule = `github.com/${model.origin || 'voxgig-sdk'}/${model.name}-sdk`
|
|
18
18
|
|
|
19
19
|
const apikeyOptionRow = isAuthActive(model)
|
|
20
20
|
? '| `"apikey"` | `string` | API key for authentication. |\n'
|
|
@@ -12,8 +12,8 @@ const ReadmeQuick = cmp(function ReadmeQuick(props: any) {
|
|
|
12
12
|
const { target, ctx$: { model } } = props
|
|
13
13
|
|
|
14
14
|
const entity = getModelPath(model, `main.${KIT}.entity`)
|
|
15
|
-
|
|
16
|
-
const gomodule =
|
|
15
|
+
// Go module path == repo path on GitHub (org from model.origin).
|
|
16
|
+
const gomodule = `github.com/${model.origin || 'voxgig-sdk'}/${model.name}-sdk`
|
|
17
17
|
|
|
18
18
|
// Find the first published entity for examples
|
|
19
19
|
const exampleEntity = Object.values(entity).find((e: any) => e.active !== false) as any
|
|
@@ -12,8 +12,8 @@ const ReadmeTopQuick = cmp(function ReadmeTopQuick(props: any) {
|
|
|
12
12
|
const { target, ctx$: { model } } = props
|
|
13
13
|
|
|
14
14
|
const entity = getModelPath(model, `main.${KIT}.entity`)
|
|
15
|
-
|
|
16
|
-
const gomodule =
|
|
15
|
+
// Go module path == repo path on GitHub (org from model.origin).
|
|
16
|
+
const gomodule = `github.com/${model.origin || 'voxgig-sdk'}/${model.name}-sdk`
|
|
17
17
|
|
|
18
18
|
const exampleEntity = Object.values(entity).find((e: any) => e.active !== false) as any
|
|
19
19
|
|
|
@@ -12,8 +12,8 @@ const ReadmeTopTest = cmp(function ReadmeTopTest(props: any) {
|
|
|
12
12
|
const { target, ctx$: { model } } = props
|
|
13
13
|
|
|
14
14
|
const entity = getModelPath(model, `main.${KIT}.entity`)
|
|
15
|
-
|
|
16
|
-
const gomodule =
|
|
15
|
+
// Go module path == repo path on GitHub (org from model.origin).
|
|
16
|
+
const gomodule = `github.com/${model.origin || 'voxgig-sdk'}/${model.name}-sdk`
|
|
17
17
|
|
|
18
18
|
const exampleEntity = Object.values(entity).find((e: any) => e.active !== false) as any
|
|
19
19
|
|
|
@@ -89,13 +89,62 @@ const TestDirect = cmp(function TestDirect(props: any) {
|
|
|
89
89
|
// Get load point info
|
|
90
90
|
const loadPoint = loadOp?.points?.[0]
|
|
91
91
|
const loadPath = loadPoint ? normalizePathParams(loadPoint.parts || [], loadPoint?.args?.params || [], loadPoint?.rename?.param) : ''
|
|
92
|
-
const
|
|
92
|
+
const allLoadParams = loadPoint?.args?.params || []
|
|
93
|
+
// Some upstream OpenAPI specs declare a parameter as `in: path` even when
|
|
94
|
+
// that path has no `{name}` placeholder for it. Only path params that
|
|
95
|
+
// actually appear in the URL template should drive direct-test path-param
|
|
96
|
+
// setup and URL-substitution asserts; otherwise the SDK silently drops
|
|
97
|
+
// them and the URL-includes assert fails.
|
|
98
|
+
const _pathPlaceholders = new Set<string>()
|
|
99
|
+
for (const part of (loadPoint?.parts || [])) {
|
|
100
|
+
if (typeof part === 'string' && part.startsWith('{') && part.endsWith('}')) {
|
|
101
|
+
_pathPlaceholders.add(part.slice(1, -1))
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
const _renameMap = (loadPoint?.rename?.param || {}) as Record<string, string>
|
|
105
|
+
const _renamedPlaceholders = new Set<string>()
|
|
106
|
+
for (const ph of _pathPlaceholders) {
|
|
107
|
+
_renamedPlaceholders.add(ph)
|
|
108
|
+
for (const [orig, renamed] of Object.entries(_renameMap)) {
|
|
109
|
+
if (renamed === ph) _renamedPlaceholders.add(orig)
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
const loadParams = allLoadParams.filter((p: any) =>
|
|
113
|
+
_renamedPlaceholders.has(p.name) || _renamedPlaceholders.has(p.orig))
|
|
93
114
|
|
|
94
115
|
// Get list point info
|
|
95
116
|
const listPoint = listOp?.points?.[0]
|
|
96
117
|
const listPath = listPoint ? normalizePathParams(listPoint.parts || [], listPoint?.args?.params || [], listPoint?.rename?.param) : ''
|
|
97
118
|
const listParams = listPoint?.args?.params || []
|
|
98
119
|
|
|
120
|
+
// Required query params with spec-provided examples — needed in live mode
|
|
121
|
+
// to satisfy API contracts (e.g. /v2018/history requires city/start/end).
|
|
122
|
+
const loadQuery = loadPoint?.args?.query || []
|
|
123
|
+
const loadLiveQueryEntries = loadQuery
|
|
124
|
+
.filter((q: any) => q.reqd && undefined !== q.example && null !== q.example)
|
|
125
|
+
const loadLiveQueryLines = loadLiveQueryEntries
|
|
126
|
+
.map((q: any) => `\t\t\tquery["${q.name}"] = ${JSON.stringify(q.example)}`)
|
|
127
|
+
.join('\n')
|
|
128
|
+
|
|
129
|
+
const listQuery = listPoint?.args?.query || []
|
|
130
|
+
const listLiveQueryEntries = listQuery
|
|
131
|
+
.filter((q: any) => q.reqd && undefined !== q.example && null !== q.example)
|
|
132
|
+
const listLiveQueryLines = listLiveQueryEntries
|
|
133
|
+
.map((q: any) => `\t\t\tquery["${q.name}"] = ${JSON.stringify(q.example)}`)
|
|
134
|
+
.join('\n')
|
|
135
|
+
|
|
136
|
+
// Path params with spec-provided examples — when ALL load params have
|
|
137
|
+
// spec examples, prefer them over list-bootstrap. Spec example values are
|
|
138
|
+
// by definition real identifiers the API accepts (e.g. casa: "blue",
|
|
139
|
+
// fecha: "2024/01/01"), avoiding the brittle list-bootstrap path-param
|
|
140
|
+
// semantic mismatch.
|
|
141
|
+
const loadAllHaveExamples =
|
|
142
|
+
loadParams.length > 0 &&
|
|
143
|
+
loadParams.every((p: any) => undefined !== p.example && null !== p.example)
|
|
144
|
+
const loadExampleLines = loadAllHaveExamples
|
|
145
|
+
? loadParams.map((p: any) => `\t\t\tparams["${p.name}"] = ${JSON.stringify(p.example)}`).join('\n')
|
|
146
|
+
: ''
|
|
147
|
+
|
|
99
148
|
// Build the ENTID env var name for this entity
|
|
100
149
|
const entidEnvVar = `${PROJECTNAME}_TEST_${nom(entity, 'NAME').replace(/[^A-Z_]/g, '_')}_ENTID`
|
|
101
150
|
|
|
@@ -131,12 +180,44 @@ func Test${entity.Name}Direct(t *testing.T) {
|
|
|
131
180
|
return { name: p.name, key }
|
|
132
181
|
})
|
|
133
182
|
|
|
183
|
+
// Track idmap keys this test consumes in live mode. If any are
|
|
184
|
+
// missing (no ENTID override), skip — the request would 4xx on
|
|
185
|
+
// undefined path params.
|
|
186
|
+
const listLiveIdKeys = listParams.length > 0
|
|
187
|
+
? listLiveParams.map((lp: any) => lp.key)
|
|
188
|
+
: []
|
|
189
|
+
const listLiveIdKeysGoLiteral = listLiveIdKeys.length > 0
|
|
190
|
+
? `[]string{${listLiveIdKeys.map((k: string) => `"${k}"`).join(', ')}}`
|
|
191
|
+
: ''
|
|
192
|
+
const listSkipBlock = listLiveIdKeys.length > 0
|
|
193
|
+
? ` if setup.live {
|
|
194
|
+
for _, _liveKey := range ${listLiveIdKeysGoLiteral} {
|
|
195
|
+
if v := setup.idmap[_liveKey]; v == nil {
|
|
196
|
+
t.Skipf("live test needs %s via *_ENTID env var (synthetic IDs only)", _liveKey)
|
|
197
|
+
return
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
`
|
|
202
|
+
: ''
|
|
203
|
+
|
|
134
204
|
Content(` t.Run("direct-list-${entity.name}", func(t *testing.T) {
|
|
135
205
|
setup := ${entity.name}DirectSetup([]any{
|
|
136
206
|
map[string]any{"id": "direct01"},
|
|
137
207
|
map[string]any{"id": "direct02"},
|
|
138
208
|
})
|
|
139
|
-
|
|
209
|
+
_mode := "unit"
|
|
210
|
+
if setup.live {
|
|
211
|
+
_mode = "live"
|
|
212
|
+
}
|
|
213
|
+
if _shouldSkip, _reason := isControlSkipped("direct", "direct-list-${entity.name}", _mode); _shouldSkip {
|
|
214
|
+
if _reason == "" {
|
|
215
|
+
_reason = "skipped via sdk-test-control.json"
|
|
216
|
+
}
|
|
217
|
+
t.Skip(_reason)
|
|
218
|
+
return
|
|
219
|
+
}
|
|
220
|
+
${listSkipBlock} client := setup.client
|
|
140
221
|
|
|
141
222
|
`)
|
|
142
223
|
|
|
@@ -172,15 +253,30 @@ func Test${entity.Name}Direct(t *testing.T) {
|
|
|
172
253
|
`)
|
|
173
254
|
}
|
|
174
255
|
|
|
175
|
-
Content(` if
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
256
|
+
Content(` if setup.live {
|
|
257
|
+
// Live mode is lenient: synthetic IDs frequently 4xx and the
|
|
258
|
+
// list-response shape varies wildly across public APIs. Skip
|
|
259
|
+
// rather than fail when the call doesn't return a usable list.
|
|
260
|
+
if err != nil {
|
|
261
|
+
t.Skipf("list call failed (likely synthetic IDs against live API): %v", err)
|
|
262
|
+
}
|
|
263
|
+
if result["ok"] != true {
|
|
264
|
+
t.Skipf("list call not ok (likely synthetic IDs against live API): %v", result)
|
|
265
|
+
}
|
|
266
|
+
status := core.ToInt(result["status"])
|
|
267
|
+
if status < 200 || status >= 300 {
|
|
268
|
+
t.Skipf("expected 2xx status, got %v", result["status"])
|
|
269
|
+
}
|
|
270
|
+
} else {
|
|
271
|
+
if err != nil {
|
|
272
|
+
t.Fatalf("direct failed: %v", err)
|
|
273
|
+
}
|
|
274
|
+
if result["ok"] != true {
|
|
275
|
+
t.Fatalf("expected ok to be true, got %v", result["ok"])
|
|
276
|
+
}
|
|
277
|
+
if core.ToInt(result["status"]) != 200 {
|
|
278
|
+
t.Fatalf("expected status 200, got %v", result["status"])
|
|
279
|
+
}
|
|
184
280
|
}
|
|
185
281
|
|
|
186
282
|
if !setup.live {
|
|
@@ -232,22 +328,75 @@ func Test${entity.Name}Direct(t *testing.T) {
|
|
|
232
328
|
// Identify ancestor params (not 'id') for live mode
|
|
233
329
|
const ancestorParams = loadParams.filter((p: any) => p.name !== 'id')
|
|
234
330
|
|
|
331
|
+
// Determine which idmap keys this load test will consume in live mode.
|
|
332
|
+
// - allHaveExamples: spec provides example values for every load
|
|
333
|
+
// path-param, so live mode uses them — no idmap needed.
|
|
334
|
+
// - hasList: we list-bootstrap, so we need the keys for the list call's
|
|
335
|
+
// path params (ancestors of the list path).
|
|
336
|
+
// - synthetic-only: we'd use idmap for load path-params; without an
|
|
337
|
+
// override they're undefined and the live request 4xx's.
|
|
338
|
+
let loadLiveIdKeys: string[] = []
|
|
339
|
+
if (loadParams.length > 0 && !loadAllHaveExamples) {
|
|
340
|
+
if (hasList) {
|
|
341
|
+
loadLiveIdKeys = listParams.map((p: any) => {
|
|
342
|
+
return p.name === 'id'
|
|
343
|
+
? entity.name + '01'
|
|
344
|
+
: p.name.replace(/_id$/, '') + '01'
|
|
345
|
+
})
|
|
346
|
+
} else {
|
|
347
|
+
loadLiveIdKeys = loadParams.map((p: any) => p.name + '01')
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
const loadSkipBlock = loadLiveIdKeys.length > 0
|
|
351
|
+
? ` if setup.live {
|
|
352
|
+
for _, _liveKey := range []string{${loadLiveIdKeys.map(k => `"${k}"`).join(', ')}} {
|
|
353
|
+
if v := setup.idmap[_liveKey]; v == nil {
|
|
354
|
+
t.Skipf("live test needs %s via *_ENTID env var (synthetic IDs only)", _liveKey)
|
|
355
|
+
return
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
`
|
|
360
|
+
: ''
|
|
361
|
+
|
|
235
362
|
Content(` t.Run("direct-load-${entity.name}", func(t *testing.T) {
|
|
236
363
|
setup := ${entity.name}DirectSetup(map[string]any{"id": "direct01"})
|
|
237
|
-
|
|
364
|
+
_mode := "unit"
|
|
365
|
+
if setup.live {
|
|
366
|
+
_mode = "live"
|
|
367
|
+
}
|
|
368
|
+
if _shouldSkip, _reason := isControlSkipped("direct", "direct-load-${entity.name}", _mode); _shouldSkip {
|
|
369
|
+
if _reason == "" {
|
|
370
|
+
_reason = "skipped via sdk-test-control.json"
|
|
371
|
+
}
|
|
372
|
+
t.Skip(_reason)
|
|
373
|
+
return
|
|
374
|
+
}
|
|
375
|
+
${loadSkipBlock} client := setup.client
|
|
238
376
|
|
|
239
377
|
`)
|
|
240
378
|
|
|
241
|
-
|
|
379
|
+
// Always emit a query map so the test can pass it to Direct(); only
|
|
380
|
+
// set values in live mode.
|
|
381
|
+
const needsQuery = loadParams.length > 0 || loadLiveQueryLines !== ''
|
|
382
|
+
if (needsQuery) {
|
|
242
383
|
Content(` params := map[string]any{}
|
|
384
|
+
query := map[string]any{}
|
|
243
385
|
`)
|
|
244
386
|
|
|
245
387
|
Content(` if setup.live {
|
|
246
388
|
`)
|
|
247
389
|
|
|
248
|
-
//
|
|
249
|
-
if (
|
|
250
|
-
|
|
390
|
+
// Required-query setup (e.g. /v2018/history needs city/start/end).
|
|
391
|
+
if (loadLiveQueryLines) {
|
|
392
|
+
Content(loadLiveQueryLines + '\n')
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
if (loadAllHaveExamples) {
|
|
396
|
+
// Use spec-provided path-param examples — no list bootstrap needed.
|
|
397
|
+
Content(loadExampleLines + '\n')
|
|
398
|
+
} else if (hasList && loadParams.length > 0) {
|
|
399
|
+
// List-bootstrap: first call list, take id from response.
|
|
251
400
|
Content(` listParams := map[string]any{}
|
|
252
401
|
`)
|
|
253
402
|
for (const p of listParams) {
|
|
@@ -264,10 +413,10 @@ func Test${entity.Name}Direct(t *testing.T) {
|
|
|
264
413
|
"params": listParams,
|
|
265
414
|
})
|
|
266
415
|
if listErr != nil {
|
|
267
|
-
t.
|
|
416
|
+
t.Skipf("list call failed (likely synthetic IDs against live API): %v", listErr)
|
|
268
417
|
}
|
|
269
418
|
if listResult["ok"] != true {
|
|
270
|
-
t.
|
|
419
|
+
t.Skipf("list call not ok (likely synthetic IDs against live API): %v", listResult)
|
|
271
420
|
}
|
|
272
421
|
|
|
273
422
|
// Get first entity ID from list
|
|
@@ -286,11 +435,13 @@ func Test${entity.Name}Direct(t *testing.T) {
|
|
|
286
435
|
}
|
|
287
436
|
}
|
|
288
437
|
|
|
289
|
-
|
|
438
|
+
if (loadParams.length > 0) {
|
|
439
|
+
Content(` } else {
|
|
290
440
|
`)
|
|
291
|
-
|
|
292
|
-
|
|
441
|
+
for (let i = 0; i < loadParams.length; i++) {
|
|
442
|
+
Content(` params["${loadParams[i].name}"] = "direct0${i + 1}"
|
|
293
443
|
`)
|
|
444
|
+
}
|
|
294
445
|
}
|
|
295
446
|
Content(` }
|
|
296
447
|
`)
|
|
@@ -303,24 +454,44 @@ func Test${entity.Name}Direct(t *testing.T) {
|
|
|
303
454
|
`)
|
|
304
455
|
if (loadParams.length > 0) {
|
|
305
456
|
Content(` "params": params,
|
|
457
|
+
"query": query,
|
|
458
|
+
`)
|
|
459
|
+
} else if (loadLiveQueryLines) {
|
|
460
|
+
Content(` "params": params,
|
|
461
|
+
"query": query,
|
|
306
462
|
`)
|
|
307
463
|
} else {
|
|
308
464
|
Content(` "params": map[string]any{},
|
|
309
465
|
`)
|
|
310
466
|
}
|
|
311
467
|
Content(` })
|
|
312
|
-
if
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
468
|
+
if setup.live {
|
|
469
|
+
// Live mode is lenient: synthetic IDs frequently 4xx. Skip
|
|
470
|
+
// rather than fail when the load endpoint isn't reachable with
|
|
471
|
+
// the IDs we can construct from setup.idmap.
|
|
472
|
+
if err != nil {
|
|
473
|
+
t.Skipf("load call failed (likely synthetic IDs against live API): %v", err)
|
|
474
|
+
}
|
|
475
|
+
if result["ok"] != true {
|
|
476
|
+
t.Skipf("load call not ok (likely synthetic IDs against live API): %v", result)
|
|
477
|
+
}
|
|
478
|
+
status := core.ToInt(result["status"])
|
|
479
|
+
if status < 200 || status >= 300 {
|
|
480
|
+
t.Skipf("expected 2xx status, got %v", result["status"])
|
|
481
|
+
}
|
|
482
|
+
} else {
|
|
483
|
+
if err != nil {
|
|
484
|
+
t.Fatalf("direct failed: %v", err)
|
|
485
|
+
}
|
|
486
|
+
if result["ok"] != true {
|
|
487
|
+
t.Fatalf("expected ok to be true, got %v", result["ok"])
|
|
488
|
+
}
|
|
489
|
+
if core.ToInt(result["status"]) != 200 {
|
|
490
|
+
t.Fatalf("expected status 200, got %v", result["status"])
|
|
491
|
+
}
|
|
492
|
+
if result["data"] == nil {
|
|
493
|
+
t.Fatal("expected data to be non-nil")
|
|
494
|
+
}
|
|
324
495
|
}
|
|
325
496
|
|
|
326
497
|
if !setup.live {
|
|
@@ -100,6 +100,7 @@ import (
|
|
|
100
100
|
"os"
|
|
101
101
|
"path/filepath"
|
|
102
102
|
"runtime"
|
|
103
|
+
"strings"
|
|
103
104
|
"testing"
|
|
104
105
|
"time"
|
|
105
106
|
|
|
@@ -120,6 +121,27 @@ func Test${entity.Name}Entity(t *testing.T) {
|
|
|
120
121
|
|
|
121
122
|
t.Run("basic", func(t *testing.T) {
|
|
122
123
|
setup := ${entity.name}BasicSetup(nil)
|
|
124
|
+
// Per-op sdk-test-control.json skip — basic test exercises a flow
|
|
125
|
+
// with multiple ops; skipping any op skips the whole flow.
|
|
126
|
+
_mode := "unit"
|
|
127
|
+
if setup.live {
|
|
128
|
+
_mode = "live"
|
|
129
|
+
}
|
|
130
|
+
for _, _op := range []string{${(Array.from(new Set((allSteps as any[]).map((s: any) => s.op).filter(Boolean)))).map(o => `"${o}"`).join(', ')}} {
|
|
131
|
+
if _shouldSkip, _reason := isControlSkipped("entityOp", "${entity.name}." + _op, _mode); _shouldSkip {
|
|
132
|
+
if _reason == "" {
|
|
133
|
+
_reason = "skipped via sdk-test-control.json"
|
|
134
|
+
}
|
|
135
|
+
t.Skip(_reason)
|
|
136
|
+
return
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
// The basic flow consumes synthetic IDs from the fixture. In live mode
|
|
140
|
+
// without an *_ENTID env override, those IDs hit the live API and 4xx.
|
|
141
|
+
if setup.syntheticOnly {
|
|
142
|
+
t.Skip("live entity test uses synthetic IDs from fixture — set ${PROJUPPER}_TEST_${entity.name.toUpperCase().replace(/[^A-Z_]/g, '_')}_ENTID JSON to run live")
|
|
143
|
+
return
|
|
144
|
+
}
|
|
123
145
|
${allSteps.length > 0 ? '\t\tclient := setup.client\n\n' : ''}`)
|
|
124
146
|
|
|
125
147
|
// Check if the flow has a create step; if not, bootstrap entity data
|
|
@@ -193,6 +215,12 @@ ${allSteps.length > 0 ? '\t\tclient := setup.client\n\n' : ''}`)
|
|
|
193
215
|
Content('\t)\n')
|
|
194
216
|
|
|
195
217
|
Content(`
|
|
218
|
+
// Detect ENTID env override before envOverride consumes it. When live
|
|
219
|
+
// mode is on without a real override, the basic test runs against synthetic
|
|
220
|
+
// IDs from the fixture and 4xx's. Surface this so the test can skip.
|
|
221
|
+
entidEnvRaw := os.Getenv("${PROJUPPER}_TEST_${entity.name.toUpperCase().replace(/[^A-Z_]/g, '_')}_ENTID")
|
|
222
|
+
idmapOverridden := entidEnvRaw != "" && strings.HasPrefix(strings.TrimSpace(entidEnvRaw), "{")
|
|
223
|
+
|
|
196
224
|
env := envOverride(map[string]any{
|
|
197
225
|
"${PROJUPPER}_TEST_${entity.name.toUpperCase().replace(/[^A-Z_]/g, '_')}_ENTID": idmap,
|
|
198
226
|
"${PROJUPPER}_TEST_LIVE": "FALSE",
|
|
@@ -224,13 +252,16 @@ ${allSteps.length > 0 ? '\t\tclient := setup.client\n\n' : ''}`)
|
|
|
224
252
|
client = sdk.New${model.const.Name}SDK(core.ToMapAny(mergedOpts))
|
|
225
253
|
}
|
|
226
254
|
|
|
255
|
+
live := env["${PROJUPPER}_TEST_LIVE"] == "TRUE"
|
|
227
256
|
return &entityTestSetup{
|
|
228
|
-
client:
|
|
229
|
-
data:
|
|
230
|
-
idmap:
|
|
231
|
-
env:
|
|
232
|
-
explain:
|
|
233
|
-
|
|
257
|
+
client: client,
|
|
258
|
+
data: entityData,
|
|
259
|
+
idmap: idmapResolved,
|
|
260
|
+
env: env,
|
|
261
|
+
explain: env["${PROJUPPER}_TEST_EXPLAIN"] == "TRUE",
|
|
262
|
+
live: live,
|
|
263
|
+
syntheticOnly: live && !idmapOverridden,
|
|
264
|
+
now: time.Now().UnixMilli(),
|
|
234
265
|
}
|
|
235
266
|
}
|
|
236
267
|
`)
|
|
@@ -20,8 +20,8 @@ const Test = cmp(function Test(props: any) {
|
|
|
20
20
|
const { target } = props
|
|
21
21
|
|
|
22
22
|
// Module name: concatenated lowercase
|
|
23
|
-
|
|
24
|
-
const gomodule =
|
|
23
|
+
// Go module path == repo path on GitHub (org from model.origin).
|
|
24
|
+
const gomodule = `github.com/${model.origin || 'voxgig-sdk'}/${model.name}-sdk`
|
|
25
25
|
|
|
26
26
|
Folder({ name: 'test' }, () => {
|
|
27
27
|
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
package core
|
|
2
2
|
|
|
3
3
|
import (
|
|
4
|
+
"fmt"
|
|
5
|
+
|
|
4
6
|
vs "github.com/voxgig/struct"
|
|
5
7
|
)
|
|
6
8
|
|
|
@@ -209,17 +211,32 @@ func (sdk *ProjectNameSDK) Direct(fetchargs map[string]any) (map[string]any, err
|
|
|
209
211
|
|
|
210
212
|
if fm, ok := fetched.(map[string]any); ok {
|
|
211
213
|
status := ToInt(vs.GetProp(fm, "status"))
|
|
214
|
+
headers := vs.GetProp(fm, "headers")
|
|
215
|
+
|
|
216
|
+
// No-body responses (204, 304) and explicit zero content-length
|
|
217
|
+
// must skip JSON parsing — calling json() on an empty body errors.
|
|
218
|
+
var contentLength string
|
|
219
|
+
if hm, ok := headers.(map[string]any); ok {
|
|
220
|
+
if cl, ok := hm["content-length"]; ok {
|
|
221
|
+
contentLength = fmt.Sprintf("%v", cl)
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
noBody := status == 204 || status == 304 || contentLength == "0"
|
|
225
|
+
|
|
212
226
|
var jsonData any
|
|
213
|
-
if
|
|
214
|
-
if
|
|
215
|
-
|
|
227
|
+
if !noBody {
|
|
228
|
+
if jf := vs.GetProp(fm, "json"); jf != nil {
|
|
229
|
+
if f, ok := jf.(func() any); ok {
|
|
230
|
+
// f() returns nil on parse error in our fetcher.
|
|
231
|
+
jsonData = f()
|
|
232
|
+
}
|
|
216
233
|
}
|
|
217
234
|
}
|
|
218
235
|
|
|
219
236
|
return map[string]any{
|
|
220
237
|
"ok": status >= 200 && status < 300,
|
|
221
238
|
"status": status,
|
|
222
|
-
"headers":
|
|
239
|
+
"headers": headers,
|
|
223
240
|
"data": jsonData,
|
|
224
241
|
}, nil
|
|
225
242
|
}
|
|
@@ -18,11 +18,18 @@ const Package = cmp(async function Package(props: any) {
|
|
|
18
18
|
|
|
19
19
|
const model: Model = ctx$.model
|
|
20
20
|
|
|
21
|
+
// Rock name is namespaced to model.origin (e.g. "voxgig-sdk"). LuaRocks has
|
|
22
|
+
// no real namespaces, so the parts are hyphen-joined. The Lua module name
|
|
23
|
+
// (`${model.name}_sdk`) used by `require` is unchanged.
|
|
24
|
+
const ns = model.origin || 'voxgig-sdk'
|
|
25
|
+
const pkgBase = ns.endsWith('-sdk') ? model.name : `${model.name}-sdk`
|
|
26
|
+
const rockName = `${ns}-${pkgBase}`
|
|
27
|
+
|
|
21
28
|
File({ name: model.name + '.rockspec' }, () => {
|
|
22
|
-
Content(`package = "${
|
|
29
|
+
Content(`package = "${rockName}"
|
|
23
30
|
version = "0.0-1"
|
|
24
31
|
source = {
|
|
25
|
-
url = "git://github.com
|
|
32
|
+
url = "git://github.com/${ns}/${model.name}-sdk.git"
|
|
26
33
|
}
|
|
27
34
|
description = {
|
|
28
35
|
summary = "${model.const.Name} SDK for Lua",
|