@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
|
@@ -18,13 +18,20 @@ const Package = cmp(async function Package(props: any) {
|
|
|
18
18
|
|
|
19
19
|
const model: Model = ctx$.model
|
|
20
20
|
|
|
21
|
+
// PyPI distribution name is namespaced to model.origin (e.g. "voxgig-sdk").
|
|
22
|
+
// PyPI names can't contain "/", so the parts are hyphen-joined. The import
|
|
23
|
+
// package (the `${model.name}_sdk/` dir) is unchanged.
|
|
24
|
+
const ns = model.origin || 'voxgig-sdk'
|
|
25
|
+
const pkgBase = ns.endsWith('-sdk') ? model.name : `${model.name}-sdk`
|
|
26
|
+
const distName = `${ns}-${pkgBase}`
|
|
27
|
+
|
|
21
28
|
File({ name: 'pyproject.toml' }, () => {
|
|
22
29
|
Content(`[build-system]
|
|
23
30
|
requires = ["setuptools>=61.0"]
|
|
24
31
|
build-backend = "setuptools.build_meta"
|
|
25
32
|
|
|
26
33
|
[project]
|
|
27
|
-
name = "${
|
|
34
|
+
name = "${distName}"
|
|
28
35
|
version = "0.0.1"
|
|
29
36
|
description = "${model.const.Name} SDK for Python"
|
|
30
37
|
license = "MIT"
|
|
@@ -83,12 +83,51 @@ const TestDirect = cmp(function TestDirect(props: any) {
|
|
|
83
83
|
|
|
84
84
|
const loadPoint = loadOp?.points?.[0]
|
|
85
85
|
const loadPath = loadPoint ? normalizePathParams(loadPoint.parts || [], loadPoint?.args?.params || [], loadPoint?.rename?.param) : ''
|
|
86
|
-
const
|
|
86
|
+
const allLoadParams = loadPoint?.args?.params || []
|
|
87
|
+
// Some upstream OpenAPI specs declare a parameter as `in: path` even when
|
|
88
|
+
// that path has no `{name}` placeholder for it. Only path params that
|
|
89
|
+
// actually appear in the URL template should drive direct-test path-param
|
|
90
|
+
// setup and URL-substitution asserts; otherwise the SDK silently drops
|
|
91
|
+
// them and the URL-includes assert fails.
|
|
92
|
+
const _pathPlaceholders = new Set<string>()
|
|
93
|
+
for (const part of (loadPoint?.parts || [])) {
|
|
94
|
+
if (typeof part === 'string' && part.startsWith('{') && part.endsWith('}')) {
|
|
95
|
+
_pathPlaceholders.add(part.slice(1, -1))
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
const _renameMap = (loadPoint?.rename?.param || {}) as Record<string, string>
|
|
99
|
+
const _renamedPlaceholders = new Set<string>()
|
|
100
|
+
for (const ph of _pathPlaceholders) {
|
|
101
|
+
_renamedPlaceholders.add(ph)
|
|
102
|
+
for (const [orig, renamed] of Object.entries(_renameMap)) {
|
|
103
|
+
if (renamed === ph) _renamedPlaceholders.add(orig)
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
const loadParams = allLoadParams.filter((p: any) =>
|
|
107
|
+
_renamedPlaceholders.has(p.name) || _renamedPlaceholders.has(p.orig))
|
|
87
108
|
|
|
88
109
|
const listPoint = listOp?.points?.[0]
|
|
89
110
|
const listPath = listPoint ? normalizePathParams(listPoint.parts || [], listPoint?.args?.params || [], listPoint?.rename?.param) : ''
|
|
90
111
|
const listParams = listPoint?.args?.params || []
|
|
91
112
|
|
|
113
|
+
// Required query params with spec-provided examples — needed in live mode
|
|
114
|
+
// to satisfy API contracts (e.g. /v2018/history requires city/start/end).
|
|
115
|
+
const loadQuery = loadPoint?.args?.query || []
|
|
116
|
+
const loadLiveQueryEntries = loadQuery
|
|
117
|
+
.filter((q: any) => q.reqd && undefined !== q.example && null !== q.example)
|
|
118
|
+
const loadLiveQueryLines = loadLiveQueryEntries
|
|
119
|
+
.map((q: any) => ` query["${q.name}"] = ${JSON.stringify(q.example)}`)
|
|
120
|
+
.join('\n')
|
|
121
|
+
|
|
122
|
+
// Path params with spec-provided examples — when ALL load params have
|
|
123
|
+
// spec examples, prefer them over list-bootstrap.
|
|
124
|
+
const loadAllHaveExamples =
|
|
125
|
+
loadParams.length > 0 &&
|
|
126
|
+
loadParams.every((p: any) => undefined !== p.example && null !== p.example)
|
|
127
|
+
const loadExampleLines = loadAllHaveExamples
|
|
128
|
+
? loadParams.map((p: any) => ` params["${p.name}"] = ${JSON.stringify(p.example)}`).join('\n')
|
|
129
|
+
: ''
|
|
130
|
+
|
|
92
131
|
const entidEnvVar = `${PROJECTNAME}_TEST_${nom(entity, 'NAME').replace(/[^A-Z_]/g, '_')}_ENTID`
|
|
93
132
|
|
|
94
133
|
File({ name: 'test_' + entity.name + '_direct.' + target.ext }, () => {
|
|
@@ -109,12 +148,33 @@ class Test${entity.Name}Direct:
|
|
|
109
148
|
`)
|
|
110
149
|
|
|
111
150
|
if (hasList && listPoint) {
|
|
151
|
+
// Track idmap keys this list test consumes in live mode.
|
|
152
|
+
const listLiveIdKeys: string[] = listParams.map((lp: any) => {
|
|
153
|
+
return lp.name === 'id'
|
|
154
|
+
? entity.name + '01'
|
|
155
|
+
: lp.name.replace(/_id$/, '') + '01'
|
|
156
|
+
})
|
|
157
|
+
const listSkipBlock = listLiveIdKeys.length > 0
|
|
158
|
+
? ` if setup["live"]:
|
|
159
|
+
for _live_key in [${listLiveIdKeys.map(k => `"${k}"`).join(', ')}]:
|
|
160
|
+
if setup["idmap"].get(_live_key) is None:
|
|
161
|
+
# pytest already imported at module scope
|
|
162
|
+
pytest.skip(f"live test needs {_live_key} via *_ENTID env var (synthetic IDs only)")
|
|
163
|
+
return
|
|
164
|
+
|
|
165
|
+
`
|
|
166
|
+
: ''
|
|
112
167
|
Content(` def test_should_direct_list_${entity.name}(self):
|
|
113
168
|
setup = _${entity.name}_direct_setup([
|
|
114
169
|
{"id": "direct01"},
|
|
115
170
|
{"id": "direct02"},
|
|
116
171
|
])
|
|
117
|
-
|
|
172
|
+
_skip, _reason = runner.is_control_skipped("direct", "direct-list-${entity.name}", "live" if setup["live"] else "unit")
|
|
173
|
+
if _skip:
|
|
174
|
+
# pytest already imported at module scope
|
|
175
|
+
pytest.skip(_reason or "skipped via sdk-test-control.json")
|
|
176
|
+
return
|
|
177
|
+
${listSkipBlock} client = setup["client"]
|
|
118
178
|
|
|
119
179
|
`)
|
|
120
180
|
|
|
@@ -148,11 +208,24 @@ class Test${entity.Name}Direct:
|
|
|
148
208
|
`)
|
|
149
209
|
}
|
|
150
210
|
|
|
151
|
-
Content(`
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
211
|
+
Content(` if setup["live"]:
|
|
212
|
+
# Live mode is lenient: synthetic IDs frequently 4xx and the
|
|
213
|
+
# list-response shape varies wildly across public APIs. Skip
|
|
214
|
+
# rather than fail when the call doesn't return a usable list.
|
|
215
|
+
if err is not None:
|
|
216
|
+
pytest.skip(f"list call failed (likely synthetic IDs against live API): {err}")
|
|
217
|
+
return
|
|
218
|
+
if not result.get("ok"):
|
|
219
|
+
pytest.skip("list call not ok (likely synthetic IDs against live API)")
|
|
220
|
+
return
|
|
221
|
+
status = helpers.to_int(result["status"])
|
|
222
|
+
if status < 200 or status >= 300:
|
|
223
|
+
pytest.skip(f"expected 2xx status, got {status}")
|
|
224
|
+
return
|
|
225
|
+
else:
|
|
226
|
+
assert err is None
|
|
227
|
+
assert result["ok"] is True
|
|
228
|
+
assert helpers.to_int(result["status"]) == 200
|
|
156
229
|
assert isinstance(result["data"], list)
|
|
157
230
|
assert len(result["data"]) == 2
|
|
158
231
|
assert len(setup["calls"]) == 1
|
|
@@ -161,18 +234,58 @@ class Test${entity.Name}Direct:
|
|
|
161
234
|
}
|
|
162
235
|
|
|
163
236
|
if (hasLoad && loadPoint) {
|
|
237
|
+
// Python's direct-load test has no list-bootstrap, so when load path
|
|
238
|
+
// params can't be filled (no spec examples + no override), skip cleanly.
|
|
239
|
+
const loadSkipBlock = (loadParams.length > 0 && !loadAllHaveExamples)
|
|
240
|
+
? ` if setup["live"]:
|
|
241
|
+
# pytest already imported at module scope
|
|
242
|
+
pytest.skip("live direct-load needs real ID — set *_ENTID env var with real IDs to run")
|
|
243
|
+
return
|
|
244
|
+
|
|
245
|
+
`
|
|
246
|
+
: ''
|
|
164
247
|
Content(` def test_should_direct_load_${entity.name}(self):
|
|
165
248
|
setup = _${entity.name}_direct_setup({"id": "direct01"})
|
|
166
|
-
|
|
249
|
+
_skip, _reason = runner.is_control_skipped("direct", "direct-load-${entity.name}", "live" if setup["live"] else "unit")
|
|
250
|
+
if _skip:
|
|
251
|
+
# pytest already imported at module scope
|
|
252
|
+
pytest.skip(_reason or "skipped via sdk-test-control.json")
|
|
253
|
+
return
|
|
254
|
+
${loadSkipBlock} client = setup["client"]
|
|
167
255
|
|
|
168
256
|
`)
|
|
169
257
|
|
|
170
|
-
|
|
258
|
+
const needsQuery = loadParams.length > 0 || loadLiveQueryLines !== ''
|
|
259
|
+
if (needsQuery) {
|
|
171
260
|
Content(` params = {}
|
|
172
|
-
|
|
261
|
+
query = {}
|
|
262
|
+
`)
|
|
263
|
+
if (loadAllHaveExamples) {
|
|
264
|
+
// Use spec-provided path-param examples in live mode.
|
|
265
|
+
Content(` if setup["live"]:
|
|
266
|
+
${loadLiveQueryLines ? loadLiveQueryLines + '\n' : ''}${loadExampleLines}
|
|
267
|
+
else:
|
|
173
268
|
`)
|
|
174
|
-
|
|
175
|
-
|
|
269
|
+
for (let i = 0; i < loadParams.length; i++) {
|
|
270
|
+
Content(` params["${loadParams[i].name}"] = "direct0${i + 1}"
|
|
271
|
+
`)
|
|
272
|
+
}
|
|
273
|
+
} else if (loadParams.length > 0) {
|
|
274
|
+
if (loadLiveQueryLines) {
|
|
275
|
+
Content(` if setup["live"]:
|
|
276
|
+
${loadLiveQueryLines}
|
|
277
|
+
`)
|
|
278
|
+
}
|
|
279
|
+
Content(` if not setup["live"]:
|
|
280
|
+
`)
|
|
281
|
+
for (let i = 0; i < loadParams.length; i++) {
|
|
282
|
+
Content(` params["${loadParams[i].name}"] = "direct0${i + 1}"
|
|
283
|
+
`)
|
|
284
|
+
}
|
|
285
|
+
} else if (loadLiveQueryLines) {
|
|
286
|
+
// Required-query only, no path params.
|
|
287
|
+
Content(` if setup["live"]:
|
|
288
|
+
${loadLiveQueryLines}
|
|
176
289
|
`)
|
|
177
290
|
}
|
|
178
291
|
}
|
|
@@ -182,20 +295,34 @@ class Test${entity.Name}Direct:
|
|
|
182
295
|
"path": "${loadPath}",
|
|
183
296
|
"method": "GET",
|
|
184
297
|
`)
|
|
185
|
-
if (
|
|
298
|
+
if (needsQuery) {
|
|
186
299
|
Content(` "params": params,
|
|
300
|
+
"query": query,
|
|
187
301
|
`)
|
|
188
302
|
} else {
|
|
189
303
|
Content(` "params": {},
|
|
190
304
|
`)
|
|
191
305
|
}
|
|
192
306
|
Content(` })
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
307
|
+
if setup["live"]:
|
|
308
|
+
# Live mode is lenient: synthetic IDs frequently 4xx. Skip
|
|
309
|
+
# rather than fail when the load endpoint isn't reachable
|
|
310
|
+
# with the IDs we can construct from setup.idmap.
|
|
311
|
+
if err is not None:
|
|
312
|
+
pytest.skip(f"load call failed (likely synthetic IDs against live API): {err}")
|
|
313
|
+
return
|
|
314
|
+
if not result.get("ok"):
|
|
315
|
+
pytest.skip("load call not ok (likely synthetic IDs against live API)")
|
|
316
|
+
return
|
|
317
|
+
status = helpers.to_int(result["status"])
|
|
318
|
+
if status < 200 or status >= 300:
|
|
319
|
+
pytest.skip(f"expected 2xx status, got {status}")
|
|
320
|
+
return
|
|
321
|
+
else:
|
|
322
|
+
assert err is None
|
|
323
|
+
assert result["ok"] is True
|
|
324
|
+
assert helpers.to_int(result["status"]) == 200
|
|
325
|
+
assert result["data"] is not None
|
|
199
326
|
if isinstance(result["data"], dict):
|
|
200
327
|
assert result["data"]["id"] == "direct01"
|
|
201
328
|
assert len(setup["calls"]) == 1
|
|
@@ -104,6 +104,20 @@ class Test${entity.Name}Entity:
|
|
|
104
104
|
|
|
105
105
|
def test_should_run_basic_flow(self):
|
|
106
106
|
setup = _${entity.name}_basic_setup(None)
|
|
107
|
+
# Per-op sdk-test-control.json skip — basic test exercises a flow with
|
|
108
|
+
# multiple ops; skipping any one skips the whole flow (steps depend
|
|
109
|
+
# on each other).
|
|
110
|
+
_live = setup.get("live", False)
|
|
111
|
+
for _op in [${(Array.from(new Set((basicflow.step as any[]).map((s: any) => s.op).filter(Boolean)))).map(o => `"${o}"`).join(', ')}]:
|
|
112
|
+
_skip, _reason = runner.is_control_skipped("entityOp", "${entity.name}." + _op, "live" if _live else "unit")
|
|
113
|
+
if _skip:
|
|
114
|
+
pytest.skip(_reason or "skipped via sdk-test-control.json")
|
|
115
|
+
return
|
|
116
|
+
# The basic flow consumes synthetic IDs from the fixture. In live mode
|
|
117
|
+
# without an *_ENTID env override, those IDs hit the live API and 4xx.
|
|
118
|
+
if setup.get("synthetic_only"):
|
|
119
|
+
pytest.skip("live entity test uses synthetic IDs from fixture — "
|
|
120
|
+
"set ${PROJUPPER}_TEST_${entity.name.toUpperCase().replace(/[^A-Z_]/g, '_')}_ENTID JSON to run live")
|
|
107
121
|
client = setup["client"]
|
|
108
122
|
|
|
109
123
|
`)
|
|
@@ -162,7 +176,14 @@ def _${entity.name}_basic_setup(extra):
|
|
|
162
176
|
|
|
163
177
|
`)
|
|
164
178
|
|
|
165
|
-
Content(` env
|
|
179
|
+
Content(` # Detect ENTID env override before envOverride consumes it. When live
|
|
180
|
+
# mode is on without a real override, the basic test runs against synthetic
|
|
181
|
+
# IDs from the fixture and 4xx's. We surface this so the test can skip.
|
|
182
|
+
_entid_env_raw = os.environ.get(
|
|
183
|
+
"${PROJUPPER}_TEST_${entity.name.toUpperCase().replace(/[^A-Z_]/g, '_')}_ENTID")
|
|
184
|
+
_idmap_overridden = _entid_env_raw is not None and _entid_env_raw.strip().startswith("{")
|
|
185
|
+
|
|
186
|
+
env = runner.env_override({
|
|
166
187
|
"${PROJUPPER}_TEST_${entity.name.toUpperCase().replace(/[^A-Z_]/g, '_')}_ENTID": idmap,
|
|
167
188
|
"${PROJUPPER}_TEST_LIVE": "FALSE",
|
|
168
189
|
"${PROJUPPER}_TEST_EXPLAIN": "FALSE",${apikeyEnvEntry}
|
|
@@ -190,12 +211,15 @@ def _${entity.name}_basic_setup(extra):
|
|
|
190
211
|
])
|
|
191
212
|
client = ${model.const.Name}SDK(helpers.to_map(merged_opts))
|
|
192
213
|
|
|
214
|
+
_live = env.get("${PROJUPPER}_TEST_LIVE") == "TRUE"
|
|
193
215
|
return {
|
|
194
216
|
"client": client,
|
|
195
217
|
"data": entity_data,
|
|
196
218
|
"idmap": idmap_resolved,
|
|
197
219
|
"env": env,
|
|
198
220
|
"explain": env.get("${PROJUPPER}_TEST_EXPLAIN") == "TRUE",
|
|
221
|
+
"live": _live,
|
|
222
|
+
"synthetic_only": _live and not _idmap_overridden,
|
|
199
223
|
"now": int(time.time() * 1000),
|
|
200
224
|
}
|
|
201
225
|
`)
|
|
@@ -180,15 +180,30 @@ class ProjectNameSDK:
|
|
|
180
180
|
|
|
181
181
|
if isinstance(fetched, dict):
|
|
182
182
|
status = helpers.to_int(vs.getprop(fetched, "status"))
|
|
183
|
+
headers = vs.getprop(fetched, "headers") or {}
|
|
184
|
+
|
|
185
|
+
# No-body responses (204, 304) and explicit zero content-length
|
|
186
|
+
# must skip JSON parsing — calling json() on an empty body raises.
|
|
187
|
+
content_length = None
|
|
188
|
+
if isinstance(headers, dict):
|
|
189
|
+
content_length = headers.get("content-length")
|
|
190
|
+
no_body = status in (204, 304) or str(content_length) == "0"
|
|
191
|
+
|
|
183
192
|
json_data = None
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
193
|
+
if not no_body:
|
|
194
|
+
jf = vs.getprop(fetched, "json")
|
|
195
|
+
if callable(jf):
|
|
196
|
+
try:
|
|
197
|
+
json_data = jf()
|
|
198
|
+
except Exception:
|
|
199
|
+
# Non-JSON body (e.g. text/plain, text/html). Surface
|
|
200
|
+
# status + headers but leave data as None.
|
|
201
|
+
json_data = None
|
|
187
202
|
|
|
188
203
|
return {
|
|
189
204
|
"ok": status >= 200 and status < 300,
|
|
190
205
|
"status": status,
|
|
191
|
-
"headers":
|
|
206
|
+
"headers": headers,
|
|
192
207
|
"data": json_data,
|
|
193
208
|
}, None
|
|
194
209
|
|
|
@@ -18,6 +18,13 @@ const Package = cmp(async function Package(props: any) {
|
|
|
18
18
|
|
|
19
19
|
const model: Model = ctx$.model
|
|
20
20
|
|
|
21
|
+
// Gem name is namespaced to model.origin (e.g. "voxgig-sdk"). RubyGems
|
|
22
|
+
// names can't contain "/", so the parts are hyphen-joined. The require
|
|
23
|
+
// path (`${model.name}_sdk`) is unchanged.
|
|
24
|
+
const ns = model.origin || 'voxgig-sdk'
|
|
25
|
+
const pkgBase = ns.endsWith('-sdk') ? model.name : `${model.name}-sdk`
|
|
26
|
+
const gemName = `${ns}-${pkgBase}`
|
|
27
|
+
|
|
21
28
|
const versionOf = (d: { version: string; source: 'feature' | 'target' }) =>
|
|
22
29
|
d.source === 'target' ? (d.version || '0.0') : d.version
|
|
23
30
|
|
|
@@ -38,12 +45,12 @@ gemspec
|
|
|
38
45
|
// Generate gemspec
|
|
39
46
|
File({ name: model.const.Name + '_sdk.gemspec' }, () => {
|
|
40
47
|
Content(`Gem::Specification.new do |spec|
|
|
41
|
-
spec.name = "${
|
|
48
|
+
spec.name = "${gemName}"
|
|
42
49
|
spec.version = "0.0.1"
|
|
43
50
|
spec.authors = ["Voxgig"]
|
|
44
51
|
spec.summary = "${model.const.Name} SDK for Ruby"
|
|
45
52
|
spec.license = "MIT"
|
|
46
|
-
spec.homepage = "https://github.com
|
|
53
|
+
spec.homepage = "https://github.com/${ns}/${model.name}-sdk"
|
|
47
54
|
|
|
48
55
|
spec.files = Dir["lib/**/*.rb", "*.rb"]
|
|
49
56
|
spec.require_paths = ["."]
|
|
@@ -83,12 +83,48 @@ const TestDirect = cmp(function TestDirect(props: any) {
|
|
|
83
83
|
|
|
84
84
|
const loadPoint = loadOp?.points?.[0]
|
|
85
85
|
const loadPath = loadPoint ? normalizePathParams(loadPoint.parts || [], loadPoint?.args?.params || [], loadPoint?.rename?.param) : ''
|
|
86
|
-
const
|
|
86
|
+
const allLoadParams = loadPoint?.args?.params || []
|
|
87
|
+
// Some upstream OpenAPI specs declare a parameter as `in: path` even when
|
|
88
|
+
// that path has no `{name}` placeholder for it. Only path params that
|
|
89
|
+
// actually appear in the URL template should drive direct-test path-param
|
|
90
|
+
// setup and URL-substitution asserts; otherwise the SDK silently drops
|
|
91
|
+
// them and the URL-includes assert fails.
|
|
92
|
+
const _pathPlaceholders = new Set<string>()
|
|
93
|
+
for (const part of (loadPoint?.parts || [])) {
|
|
94
|
+
if (typeof part === 'string' && part.startsWith('{') && part.endsWith('}')) {
|
|
95
|
+
_pathPlaceholders.add(part.slice(1, -1))
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
const _renameMap = (loadPoint?.rename?.param || {}) as Record<string, string>
|
|
99
|
+
const _renamedPlaceholders = new Set<string>()
|
|
100
|
+
for (const ph of _pathPlaceholders) {
|
|
101
|
+
_renamedPlaceholders.add(ph)
|
|
102
|
+
for (const [orig, renamed] of Object.entries(_renameMap)) {
|
|
103
|
+
if (renamed === ph) _renamedPlaceholders.add(orig)
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
const loadParams = allLoadParams.filter((p: any) =>
|
|
107
|
+
_renamedPlaceholders.has(p.name) || _renamedPlaceholders.has(p.orig))
|
|
87
108
|
|
|
88
109
|
const listPoint = listOp?.points?.[0]
|
|
89
110
|
const listPath = listPoint ? normalizePathParams(listPoint.parts || [], listPoint?.args?.params || [], listPoint?.rename?.param) : ''
|
|
90
111
|
const listParams = listPoint?.args?.params || []
|
|
91
112
|
|
|
113
|
+
// Required query params with spec-provided examples — needed in live mode.
|
|
114
|
+
const loadQuery = loadPoint?.args?.query || []
|
|
115
|
+
const loadLiveQueryEntries = loadQuery
|
|
116
|
+
.filter((q: any) => q.reqd && undefined !== q.example && null !== q.example)
|
|
117
|
+
const loadLiveQueryLines = loadLiveQueryEntries
|
|
118
|
+
.map((q: any) => ` query["${q.name}"] = ${JSON.stringify(q.example)}`)
|
|
119
|
+
.join('\n')
|
|
120
|
+
|
|
121
|
+
const loadAllHaveExamples =
|
|
122
|
+
loadParams.length > 0 &&
|
|
123
|
+
loadParams.every((p: any) => undefined !== p.example && null !== p.example)
|
|
124
|
+
const loadExampleLines = loadAllHaveExamples
|
|
125
|
+
? loadParams.map((p: any) => ` params["${p.name}"] = ${JSON.stringify(p.example)}`).join('\n')
|
|
126
|
+
: ''
|
|
127
|
+
|
|
92
128
|
const entidEnvVar = `${PROJECTNAME}_TEST_${nom(entity, 'NAME').replace(/[^A-Z_]/g, '_')}_ENTID`
|
|
93
129
|
|
|
94
130
|
File({ name: entity.name + '_direct_test.' + target.ext }, () => {
|
|
@@ -104,12 +140,33 @@ class ${entity.Name}DirectTest < Minitest::Test
|
|
|
104
140
|
`)
|
|
105
141
|
|
|
106
142
|
if (hasList && listPoint) {
|
|
143
|
+
const listLiveIdKeys: string[] = listParams.map((lp: any) => {
|
|
144
|
+
return lp.name === 'id'
|
|
145
|
+
? entity.name + '01'
|
|
146
|
+
: lp.name.replace(/_id$/, '') + '01'
|
|
147
|
+
})
|
|
148
|
+
const listSkipBlock = listLiveIdKeys.length > 0
|
|
149
|
+
? ` if setup[:live]
|
|
150
|
+
[${listLiveIdKeys.map(k => `"${k}"`).join(', ')}].each do |_live_key|
|
|
151
|
+
if setup[:idmap][_live_key].nil?
|
|
152
|
+
skip "live test needs #{_live_key} via *_ENTID env var (synthetic IDs only)"
|
|
153
|
+
return
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
`
|
|
158
|
+
: ''
|
|
107
159
|
Content(` def test_direct_list_${entity.name}
|
|
108
160
|
setup = ${entity.name}_direct_setup([
|
|
109
161
|
{ "id" => "direct01" },
|
|
110
162
|
{ "id" => "direct02" },
|
|
111
163
|
])
|
|
112
|
-
|
|
164
|
+
_should_skip, _reason = Runner.is_control_skipped("direct", "direct-list-${entity.name}", setup[:live] ? "live" : "unit")
|
|
165
|
+
if _should_skip
|
|
166
|
+
skip(_reason || "skipped via sdk-test-control.json")
|
|
167
|
+
return
|
|
168
|
+
end
|
|
169
|
+
${listSkipBlock} client = setup[:client]
|
|
113
170
|
|
|
114
171
|
`)
|
|
115
172
|
|
|
@@ -144,11 +201,27 @@ class ${entity.Name}DirectTest < Minitest::Test
|
|
|
144
201
|
`)
|
|
145
202
|
}
|
|
146
203
|
|
|
147
|
-
Content(`
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
204
|
+
Content(` if setup[:live]
|
|
205
|
+
# Live mode is lenient: synthetic IDs frequently 4xx and the list-
|
|
206
|
+
# response shape varies wildly across public APIs. Skip rather than
|
|
207
|
+
# fail when the call doesn't return a usable list.
|
|
208
|
+
if !err.nil?
|
|
209
|
+
skip("list call failed (likely synthetic IDs against live API): #{err}")
|
|
210
|
+
return
|
|
211
|
+
end
|
|
212
|
+
unless result["ok"]
|
|
213
|
+
skip("list call not ok (likely synthetic IDs against live API)")
|
|
214
|
+
return
|
|
215
|
+
end
|
|
216
|
+
status = Helpers.to_int(result["status"])
|
|
217
|
+
if status < 200 || status >= 300
|
|
218
|
+
skip("expected 2xx status, got #{status}")
|
|
219
|
+
return
|
|
220
|
+
end
|
|
221
|
+
else
|
|
222
|
+
assert_nil err
|
|
223
|
+
assert result["ok"]
|
|
224
|
+
assert_equal 200, Helpers.to_int(result["status"])
|
|
152
225
|
assert result["data"].is_a?(Array)
|
|
153
226
|
assert_equal 2, result["data"].length
|
|
154
227
|
assert_equal 1, setup[:calls].length
|
|
@@ -159,22 +232,65 @@ class ${entity.Name}DirectTest < Minitest::Test
|
|
|
159
232
|
}
|
|
160
233
|
|
|
161
234
|
if (hasLoad && loadPoint) {
|
|
235
|
+
// Skip live direct-load only when we can't fill path params:
|
|
236
|
+
// no spec examples and no list-bootstrap. Spec examples win first.
|
|
237
|
+
const loadSkipBlock = (loadParams.length > 0 && !loadAllHaveExamples)
|
|
238
|
+
? ` if setup[:live]
|
|
239
|
+
skip "live direct-load needs real ID — set *_ENTID env var with real IDs to run"
|
|
240
|
+
return
|
|
241
|
+
end
|
|
242
|
+
`
|
|
243
|
+
: ''
|
|
162
244
|
Content(` def test_direct_load_${entity.name}
|
|
163
245
|
setup = ${entity.name}_direct_setup({ "id" => "direct01" })
|
|
164
|
-
|
|
246
|
+
_should_skip, _reason = Runner.is_control_skipped("direct", "direct-load-${entity.name}", setup[:live] ? "live" : "unit")
|
|
247
|
+
if _should_skip
|
|
248
|
+
skip(_reason || "skipped via sdk-test-control.json")
|
|
249
|
+
return
|
|
250
|
+
end
|
|
251
|
+
${loadSkipBlock} client = setup[:client]
|
|
165
252
|
|
|
166
253
|
`)
|
|
167
254
|
|
|
168
|
-
|
|
255
|
+
const needsQuery = loadParams.length > 0 || loadLiveQueryLines !== ''
|
|
256
|
+
if (needsQuery) {
|
|
169
257
|
Content(` params = {}
|
|
170
|
-
|
|
258
|
+
query = {}
|
|
171
259
|
`)
|
|
172
|
-
|
|
173
|
-
Content(`
|
|
260
|
+
if (loadAllHaveExamples) {
|
|
261
|
+
Content(` if setup[:live]
|
|
174
262
|
`)
|
|
175
|
-
|
|
176
|
-
|
|
263
|
+
if (loadLiveQueryLines) Content(loadLiveQueryLines + '\n')
|
|
264
|
+
Content(loadExampleLines + '\n')
|
|
265
|
+
Content(` else
|
|
177
266
|
`)
|
|
267
|
+
for (let i = 0; i < loadParams.length; i++) {
|
|
268
|
+
Content(` params["${loadParams[i].name}"] = "direct0${i + 1}"
|
|
269
|
+
`)
|
|
270
|
+
}
|
|
271
|
+
Content(` end
|
|
272
|
+
`)
|
|
273
|
+
} else if (loadParams.length > 0) {
|
|
274
|
+
if (loadLiveQueryLines) {
|
|
275
|
+
Content(` if setup[:live]
|
|
276
|
+
${loadLiveQueryLines}
|
|
277
|
+
end
|
|
278
|
+
`)
|
|
279
|
+
}
|
|
280
|
+
Content(` unless setup[:live]
|
|
281
|
+
`)
|
|
282
|
+
for (let i = 0; i < loadParams.length; i++) {
|
|
283
|
+
Content(` params["${loadParams[i].name}"] = "direct0${i + 1}"
|
|
284
|
+
`)
|
|
285
|
+
}
|
|
286
|
+
Content(` end
|
|
287
|
+
`)
|
|
288
|
+
} else if (loadLiveQueryLines) {
|
|
289
|
+
Content(` if setup[:live]
|
|
290
|
+
${loadLiveQueryLines}
|
|
291
|
+
end
|
|
292
|
+
`)
|
|
293
|
+
}
|
|
178
294
|
}
|
|
179
295
|
|
|
180
296
|
Content(`
|
|
@@ -182,20 +298,37 @@ class ${entity.Name}DirectTest < Minitest::Test
|
|
|
182
298
|
"path" => "${loadPath}",
|
|
183
299
|
"method" => "GET",
|
|
184
300
|
`)
|
|
185
|
-
if (
|
|
301
|
+
if (needsQuery) {
|
|
186
302
|
Content(` "params" => params,
|
|
303
|
+
"query" => query,
|
|
187
304
|
`)
|
|
188
305
|
} else {
|
|
189
306
|
Content(` "params" => {},
|
|
190
307
|
`)
|
|
191
308
|
}
|
|
192
309
|
Content(` })
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
310
|
+
if setup[:live]
|
|
311
|
+
# Live mode is lenient: synthetic IDs frequently 4xx. Skip rather
|
|
312
|
+
# than fail when the load endpoint isn't reachable with the IDs
|
|
313
|
+
# we can construct from setup.idmap.
|
|
314
|
+
if !err.nil?
|
|
315
|
+
skip("load call failed (likely synthetic IDs against live API): #{err}")
|
|
316
|
+
return
|
|
317
|
+
end
|
|
318
|
+
unless result["ok"]
|
|
319
|
+
skip("load call not ok (likely synthetic IDs against live API)")
|
|
320
|
+
return
|
|
321
|
+
end
|
|
322
|
+
status = Helpers.to_int(result["status"])
|
|
323
|
+
if status < 200 || status >= 300
|
|
324
|
+
skip("expected 2xx status, got #{status}")
|
|
325
|
+
return
|
|
326
|
+
end
|
|
327
|
+
else
|
|
328
|
+
assert_nil err
|
|
329
|
+
assert result["ok"]
|
|
330
|
+
assert_equal 200, Helpers.to_int(result["status"])
|
|
331
|
+
assert !result["data"].nil?
|
|
199
332
|
if result["data"].is_a?(Hash)
|
|
200
333
|
assert_equal "direct01", result["data"]["id"]
|
|
201
334
|
end
|
|
@@ -91,6 +91,21 @@ class ${entity.Name}EntityTest < Minitest::Test
|
|
|
91
91
|
|
|
92
92
|
def test_basic_flow
|
|
93
93
|
setup = ${entity.name}_basic_setup(nil)
|
|
94
|
+
# Per-op sdk-test-control.json skip.
|
|
95
|
+
_live = setup[:live] || false
|
|
96
|
+
[${(Array.from(new Set((allSteps as any[]).map((s: any) => s.op).filter(Boolean)))).map(o => `"${o}"`).join(', ')}].each do |_op|
|
|
97
|
+
_should_skip, _reason = Runner.is_control_skipped("entityOp", "${entity.name}." + _op, _live ? "live" : "unit")
|
|
98
|
+
if _should_skip
|
|
99
|
+
skip(_reason || "skipped via sdk-test-control.json")
|
|
100
|
+
return
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
# The basic flow consumes synthetic IDs from the fixture. In live mode
|
|
104
|
+
# without an *_ENTID env override, those IDs hit the live API and 4xx.
|
|
105
|
+
if setup[:synthetic_only]
|
|
106
|
+
skip "live entity test uses synthetic IDs from fixture — set ${PROJUPPER}_TEST_${entity.name.toUpperCase().replace(/[^A-Z_]/g, '_')}_ENTID JSON to run live"
|
|
107
|
+
return
|
|
108
|
+
end
|
|
94
109
|
client = setup[:client]
|
|
95
110
|
|
|
96
111
|
`)
|
|
@@ -152,7 +167,13 @@ end
|
|
|
152
167
|
|
|
153
168
|
`)
|
|
154
169
|
|
|
155
|
-
Content(` env
|
|
170
|
+
Content(` # Detect ENTID env override before envOverride consumes it. When live
|
|
171
|
+
# mode is on without a real override, the basic test runs against synthetic
|
|
172
|
+
# IDs from the fixture and 4xx's. Surface this so the test can skip.
|
|
173
|
+
entid_env_raw = ENV["${PROJUPPER}_TEST_${entity.name.toUpperCase().replace(/[^A-Z_]/g, '_')}_ENTID"]
|
|
174
|
+
idmap_overridden = !entid_env_raw.nil? && entid_env_raw.strip.start_with?("{")
|
|
175
|
+
|
|
176
|
+
env = Runner.env_override({
|
|
156
177
|
"${PROJUPPER}_TEST_${entity.name.toUpperCase().replace(/[^A-Z_]/g, '_')}_ENTID" => idmap,
|
|
157
178
|
"${PROJUPPER}_TEST_LIVE" => "FALSE",
|
|
158
179
|
"${PROJUPPER}_TEST_EXPLAIN" => "FALSE",${apikeyEnvEntry}
|
|
@@ -183,12 +204,15 @@ end
|
|
|
183
204
|
client = ${model.const.Name}SDK.new(Helpers.to_map(merged_opts))
|
|
184
205
|
end
|
|
185
206
|
|
|
207
|
+
live = env["${PROJUPPER}_TEST_LIVE"] == "TRUE"
|
|
186
208
|
{
|
|
187
209
|
client: client,
|
|
188
210
|
data: entity_data,
|
|
189
211
|
idmap: idmap_resolved,
|
|
190
212
|
env: env,
|
|
191
213
|
explain: env["${PROJUPPER}_TEST_EXPLAIN"] == "TRUE",
|
|
214
|
+
live: live,
|
|
215
|
+
synthetic_only: live && !idmap_overridden,
|
|
192
216
|
now: (Time.now.to_f * 1000).to_i,
|
|
193
217
|
}
|
|
194
218
|
end
|