@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
|
@@ -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 }, () => {
|
|
@@ -105,12 +141,33 @@ describe("${entity.Name}Direct", function()
|
|
|
105
141
|
`)
|
|
106
142
|
|
|
107
143
|
if (hasList && listPoint) {
|
|
144
|
+
const listLiveIdKeys: string[] = listParams.map((lp: any) => {
|
|
145
|
+
return lp.name === 'id'
|
|
146
|
+
? entity.name + '01'
|
|
147
|
+
: lp.name.replace(/_id$/, '') + '01'
|
|
148
|
+
})
|
|
149
|
+
const listSkipBlock = listLiveIdKeys.length > 0
|
|
150
|
+
? ` if setup.live then
|
|
151
|
+
for _, _live_key in ipairs({${listLiveIdKeys.map(k => `"${k}"`).join(', ')}}) do
|
|
152
|
+
if setup.idmap[_live_key] == nil then
|
|
153
|
+
pending("live test needs " .. _live_key .. " via *_ENTID env var (synthetic IDs only)")
|
|
154
|
+
return
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
`
|
|
159
|
+
: ''
|
|
108
160
|
Content(` it("should direct-list-${entity.name}", function()
|
|
109
161
|
local setup = ${entity.name}_direct_setup({
|
|
110
162
|
{ id = "direct01" },
|
|
111
163
|
{ id = "direct02" },
|
|
112
164
|
})
|
|
113
|
-
local
|
|
165
|
+
local _should_skip, _reason = runner.is_control_skipped("direct", "direct-list-${entity.name}", setup.live and "live" or "unit")
|
|
166
|
+
if _should_skip then
|
|
167
|
+
pending(_reason or "skipped via sdk-test-control.json")
|
|
168
|
+
return
|
|
169
|
+
end
|
|
170
|
+
${listSkipBlock} local client = setup.client
|
|
114
171
|
|
|
115
172
|
`)
|
|
116
173
|
|
|
@@ -145,11 +202,27 @@ describe("${entity.Name}Direct", function()
|
|
|
145
202
|
`)
|
|
146
203
|
}
|
|
147
204
|
|
|
148
|
-
Content(`
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
205
|
+
Content(` if setup.live then
|
|
206
|
+
-- Live mode is lenient: synthetic IDs frequently 4xx and the list-
|
|
207
|
+
-- response shape varies wildly across public APIs. Skip rather than
|
|
208
|
+
-- fail when the call doesn't return a usable list.
|
|
209
|
+
if err ~= nil then
|
|
210
|
+
pending("list call failed (likely synthetic IDs against live API): " .. tostring(err))
|
|
211
|
+
return
|
|
212
|
+
end
|
|
213
|
+
if not result["ok"] then
|
|
214
|
+
pending("list call not ok (likely synthetic IDs against live API)")
|
|
215
|
+
return
|
|
216
|
+
end
|
|
217
|
+
local status = helpers.to_int(result["status"])
|
|
218
|
+
if status < 200 or status >= 300 then
|
|
219
|
+
pending("expected 2xx status, got " .. tostring(status))
|
|
220
|
+
return
|
|
221
|
+
end
|
|
222
|
+
else
|
|
223
|
+
assert.is_nil(err)
|
|
224
|
+
assert.is_true(result["ok"])
|
|
225
|
+
assert.are.equal(200, helpers.to_int(result["status"]))
|
|
153
226
|
assert.is_table(result["data"])
|
|
154
227
|
assert.are.equal(2, #result["data"])
|
|
155
228
|
assert.are.equal(1, #setup.calls)
|
|
@@ -160,22 +233,65 @@ describe("${entity.Name}Direct", function()
|
|
|
160
233
|
}
|
|
161
234
|
|
|
162
235
|
if (hasLoad && loadPoint) {
|
|
236
|
+
// Skip live direct-load only when we can't fill path params:
|
|
237
|
+
// no spec examples and no list-bootstrap. Spec examples win first.
|
|
238
|
+
const loadSkipBlock = (loadParams.length > 0 && !loadAllHaveExamples)
|
|
239
|
+
? ` if setup.live then
|
|
240
|
+
pending("live direct-load needs real ID — set *_ENTID env var with real IDs to run")
|
|
241
|
+
return
|
|
242
|
+
end
|
|
243
|
+
`
|
|
244
|
+
: ''
|
|
163
245
|
Content(` it("should direct-load-${entity.name}", function()
|
|
164
246
|
local setup = ${entity.name}_direct_setup({ id = "direct01" })
|
|
165
|
-
local
|
|
247
|
+
local _should_skip, _reason = runner.is_control_skipped("direct", "direct-load-${entity.name}", setup.live and "live" or "unit")
|
|
248
|
+
if _should_skip then
|
|
249
|
+
pending(_reason or "skipped via sdk-test-control.json")
|
|
250
|
+
return
|
|
251
|
+
end
|
|
252
|
+
${loadSkipBlock} local client = setup.client
|
|
166
253
|
|
|
167
254
|
`)
|
|
168
255
|
|
|
169
|
-
|
|
256
|
+
const needsQuery = loadParams.length > 0 || loadLiveQueryLines !== ''
|
|
257
|
+
if (needsQuery) {
|
|
170
258
|
Content(` local params = {}
|
|
171
|
-
|
|
259
|
+
local query = {}
|
|
172
260
|
`)
|
|
173
|
-
|
|
174
|
-
Content(`
|
|
261
|
+
if (loadAllHaveExamples) {
|
|
262
|
+
Content(` if setup.live then
|
|
175
263
|
`)
|
|
176
|
-
|
|
177
|
-
|
|
264
|
+
if (loadLiveQueryLines) Content(loadLiveQueryLines + '\n')
|
|
265
|
+
Content(loadExampleLines + '\n')
|
|
266
|
+
Content(` else
|
|
178
267
|
`)
|
|
268
|
+
for (let i = 0; i < loadParams.length; i++) {
|
|
269
|
+
Content(` params["${loadParams[i].name}"] = "direct0${i + 1}"
|
|
270
|
+
`)
|
|
271
|
+
}
|
|
272
|
+
Content(` end
|
|
273
|
+
`)
|
|
274
|
+
} else if (loadParams.length > 0) {
|
|
275
|
+
if (loadLiveQueryLines) {
|
|
276
|
+
Content(` if setup.live then
|
|
277
|
+
${loadLiveQueryLines}
|
|
278
|
+
end
|
|
279
|
+
`)
|
|
280
|
+
}
|
|
281
|
+
Content(` if not setup.live then
|
|
282
|
+
`)
|
|
283
|
+
for (let i = 0; i < loadParams.length; i++) {
|
|
284
|
+
Content(` params["${loadParams[i].name}"] = "direct0${i + 1}"
|
|
285
|
+
`)
|
|
286
|
+
}
|
|
287
|
+
Content(` end
|
|
288
|
+
`)
|
|
289
|
+
} else if (loadLiveQueryLines) {
|
|
290
|
+
Content(` if setup.live then
|
|
291
|
+
${loadLiveQueryLines}
|
|
292
|
+
end
|
|
293
|
+
`)
|
|
294
|
+
}
|
|
179
295
|
}
|
|
180
296
|
|
|
181
297
|
Content(`
|
|
@@ -183,20 +299,37 @@ describe("${entity.Name}Direct", function()
|
|
|
183
299
|
path = "${loadPath}",
|
|
184
300
|
method = "GET",
|
|
185
301
|
`)
|
|
186
|
-
if (
|
|
302
|
+
if (needsQuery) {
|
|
187
303
|
Content(` params = params,
|
|
304
|
+
query = query,
|
|
188
305
|
`)
|
|
189
306
|
} else {
|
|
190
307
|
Content(` params = {},
|
|
191
308
|
`)
|
|
192
309
|
}
|
|
193
310
|
Content(` })
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
311
|
+
if setup.live then
|
|
312
|
+
-- Live mode is lenient: synthetic IDs frequently 4xx. Skip rather
|
|
313
|
+
-- than fail when the load endpoint isn't reachable with the IDs we
|
|
314
|
+
-- can construct from setup.idmap.
|
|
315
|
+
if err ~= nil then
|
|
316
|
+
pending("load call failed (likely synthetic IDs against live API): " .. tostring(err))
|
|
317
|
+
return
|
|
318
|
+
end
|
|
319
|
+
if not result["ok"] then
|
|
320
|
+
pending("load call not ok (likely synthetic IDs against live API)")
|
|
321
|
+
return
|
|
322
|
+
end
|
|
323
|
+
local status = helpers.to_int(result["status"])
|
|
324
|
+
if status < 200 or status >= 300 then
|
|
325
|
+
pending("expected 2xx status, got " .. tostring(status))
|
|
326
|
+
return
|
|
327
|
+
end
|
|
328
|
+
else
|
|
329
|
+
assert.is_nil(err)
|
|
330
|
+
assert.is_true(result["ok"])
|
|
331
|
+
assert.are.equal(200, helpers.to_int(result["status"]))
|
|
332
|
+
assert.is_not_nil(result["data"])
|
|
200
333
|
if type(result["data"]) == "table" then
|
|
201
334
|
assert.are.equal("direct01", result["data"]["id"])
|
|
202
335
|
end
|
|
@@ -94,6 +94,21 @@ describe("${entity.Name}Entity", function()
|
|
|
94
94
|
|
|
95
95
|
it("should run basic flow", function()
|
|
96
96
|
local setup = ${entity.name}_basic_setup(nil)
|
|
97
|
+
-- Per-op sdk-test-control.json skip.
|
|
98
|
+
local _live = setup.live or false
|
|
99
|
+
for _, _op in ipairs({${(Array.from(new Set((allSteps as any[]).map((s: any) => s.op).filter(Boolean)))).map(o => `"${o}"`).join(', ')}}) do
|
|
100
|
+
local _should_skip, _reason = runner.is_control_skipped("entityOp", "${entity.name}." .. _op, _live and "live" or "unit")
|
|
101
|
+
if _should_skip then
|
|
102
|
+
pending(_reason or "skipped via sdk-test-control.json")
|
|
103
|
+
return
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
-- The basic flow consumes synthetic IDs from the fixture. In live mode
|
|
107
|
+
-- without an *_ENTID env override, those IDs hit the live API and 4xx.
|
|
108
|
+
if setup.synthetic_only then
|
|
109
|
+
pending("live entity test uses synthetic IDs from fixture — set ${PROJUPPER}_TEST_${entity.name.toUpperCase().replace(/[^A-Z_]/g, '_')}_ENTID JSON to run live")
|
|
110
|
+
return
|
|
111
|
+
end
|
|
97
112
|
local client = setup.client
|
|
98
113
|
|
|
99
114
|
`)
|
|
@@ -161,7 +176,13 @@ end)
|
|
|
161
176
|
|
|
162
177
|
`)
|
|
163
178
|
|
|
164
|
-
Content(`
|
|
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. Surface this so the test can skip.
|
|
182
|
+
local entid_env_raw = os.getenv("${PROJUPPER}_TEST_${entity.name.toUpperCase().replace(/[^A-Z_]/g, '_')}_ENTID")
|
|
183
|
+
local idmap_overridden = entid_env_raw ~= nil and entid_env_raw:match("^%s*{") ~= nil
|
|
184
|
+
|
|
185
|
+
local env = runner.env_override({
|
|
165
186
|
["${PROJUPPER}_TEST_${entity.name.toUpperCase().replace(/[^A-Z_]/g, '_')}_ENTID"] = idmap,
|
|
166
187
|
["${PROJUPPER}_TEST_LIVE"] = "FALSE",
|
|
167
188
|
["${PROJUPPER}_TEST_EXPLAIN"] = "FALSE",${apikeyEnvEntry}
|
|
@@ -192,12 +213,15 @@ end)
|
|
|
192
213
|
client = sdk.new(helpers.to_map(merged_opts))
|
|
193
214
|
end
|
|
194
215
|
|
|
216
|
+
local live = env["${PROJUPPER}_TEST_LIVE"] == "TRUE"
|
|
195
217
|
return {
|
|
196
218
|
client = client,
|
|
197
219
|
data = entity_data,
|
|
198
220
|
idmap = idmap_resolved,
|
|
199
221
|
env = env,
|
|
200
222
|
explain = env["${PROJUPPER}_TEST_EXPLAIN"] == "TRUE",
|
|
223
|
+
live = live,
|
|
224
|
+
synthetic_only = live and not idmap_overridden,
|
|
201
225
|
now = os.time() * 1000,
|
|
202
226
|
}
|
|
203
227
|
end
|
|
@@ -206,16 +206,32 @@ function ProjectNameSDK:direct(fetchargs)
|
|
|
206
206
|
|
|
207
207
|
if type(fetched) == "table" then
|
|
208
208
|
local status = helpers.to_int(vs.getprop(fetched, "status"))
|
|
209
|
+
local headers = vs.getprop(fetched, "headers") or {}
|
|
210
|
+
|
|
211
|
+
-- No-body responses (204, 304) and explicit zero content-length
|
|
212
|
+
-- must skip JSON parsing — calling json() on an empty body errors.
|
|
213
|
+
local content_length = nil
|
|
214
|
+
if type(headers) == "table" then
|
|
215
|
+
content_length = headers["content-length"]
|
|
216
|
+
end
|
|
217
|
+
local no_body = status == 204 or status == 304 or tostring(content_length) == "0"
|
|
218
|
+
|
|
209
219
|
local json_data = nil
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
220
|
+
if not no_body then
|
|
221
|
+
local jf = vs.getprop(fetched, "json")
|
|
222
|
+
if type(jf) == "function" then
|
|
223
|
+
local ok, result = pcall(jf)
|
|
224
|
+
if ok then
|
|
225
|
+
json_data = result
|
|
226
|
+
end
|
|
227
|
+
-- Non-JSON body: json_data stays nil, status/headers preserved.
|
|
228
|
+
end
|
|
213
229
|
end
|
|
214
230
|
|
|
215
231
|
return {
|
|
216
232
|
ok = status >= 200 and status < 300,
|
|
217
233
|
status = status,
|
|
218
|
-
headers =
|
|
234
|
+
headers = headers,
|
|
219
235
|
data = json_data,
|
|
220
236
|
}, nil
|
|
221
237
|
end
|
|
@@ -18,10 +18,16 @@ const Package = cmp(async function Package(props: any) {
|
|
|
18
18
|
|
|
19
19
|
const model: Model = ctx$.model
|
|
20
20
|
|
|
21
|
+
// Package namespace mirrors the npm scope (model.origin, e.g. "voxgig-sdk").
|
|
22
|
+
// If origin already ends in "-sdk" the slug stands alone; otherwise append
|
|
23
|
+
// "-sdk" (matches the TS Package generator).
|
|
24
|
+
const ns = model.origin || 'voxgig-sdk'
|
|
25
|
+
const pkgBase = ns.endsWith('-sdk') ? model.name : `${model.name}-sdk`
|
|
26
|
+
|
|
21
27
|
// Generate composer.json
|
|
22
28
|
File({ name: 'composer.json' }, () => {
|
|
23
29
|
Content(`{
|
|
24
|
-
"name": "
|
|
30
|
+
"name": "${ns}/${pkgBase}",
|
|
25
31
|
"description": "${model.const.Name} SDK for PHP",
|
|
26
32
|
"type": "library",
|
|
27
33
|
"license": "MIT",
|
|
@@ -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 + 'DirectTest.' + target.ext }, () => {
|
|
@@ -108,13 +144,34 @@ class ${entity.Name}DirectTest extends TestCase
|
|
|
108
144
|
`)
|
|
109
145
|
|
|
110
146
|
if (hasList && listPoint) {
|
|
147
|
+
const listLiveIdKeys: string[] = listParams.map((lp: any) => {
|
|
148
|
+
return lp.name === 'id'
|
|
149
|
+
? entity.name + '01'
|
|
150
|
+
: lp.name.replace(/_id$/, '') + '01'
|
|
151
|
+
})
|
|
152
|
+
const listSkipBlock = listLiveIdKeys.length > 0
|
|
153
|
+
? ` if ($setup["live"]) {
|
|
154
|
+
foreach ([${listLiveIdKeys.map(k => `"${k}"`).join(', ')}] as $_liveKey) {
|
|
155
|
+
if (!isset($setup["idmap"][$_liveKey]) || $setup["idmap"][$_liveKey] === null) {
|
|
156
|
+
$this->markTestSkipped("live test needs $_liveKey via *_ENTID env var (synthetic IDs only)");
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
`
|
|
162
|
+
: ''
|
|
111
163
|
Content(` public function test_direct_list_${entity.name}(): void
|
|
112
164
|
{
|
|
113
165
|
$setup = ${entity.name}_direct_setup([
|
|
114
166
|
["id" => "direct01"],
|
|
115
167
|
["id" => "direct02"],
|
|
116
168
|
]);
|
|
117
|
-
$
|
|
169
|
+
[$_shouldSkip, $_reason] = Runner::is_control_skipped("direct", "direct-list-${entity.name}", $setup["live"] ? "live" : "unit");
|
|
170
|
+
if ($_shouldSkip) {
|
|
171
|
+
$this->markTestSkipped($_reason ?? "skipped via sdk-test-control.json");
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
${listSkipBlock} $client = $setup["client"];
|
|
118
175
|
|
|
119
176
|
`)
|
|
120
177
|
|
|
@@ -149,11 +206,27 @@ class ${entity.Name}DirectTest extends TestCase
|
|
|
149
206
|
`)
|
|
150
207
|
}
|
|
151
208
|
|
|
152
|
-
Content(`
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
209
|
+
Content(` if ($setup["live"]) {
|
|
210
|
+
// Live mode is lenient: synthetic IDs frequently 4xx and the
|
|
211
|
+
// list-response shape varies wildly across public APIs. Skip
|
|
212
|
+
// rather than fail when the call doesn't return a usable list.
|
|
213
|
+
if ($err !== null) {
|
|
214
|
+
$this->markTestSkipped("list call failed (likely synthetic IDs against live API): " . (string)$err);
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
if (empty($result["ok"])) {
|
|
218
|
+
$this->markTestSkipped("list call not ok (likely synthetic IDs against live API)");
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
$status = Helpers::to_int($result["status"]);
|
|
222
|
+
if ($status < 200 || $status >= 300) {
|
|
223
|
+
$this->markTestSkipped("expected 2xx status, got " . $status);
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
} else {
|
|
227
|
+
$this->assertNull($err);
|
|
228
|
+
$this->assertTrue($result["ok"]);
|
|
229
|
+
$this->assertEquals(200, Helpers::to_int($result["status"]));
|
|
157
230
|
$this->assertIsArray($result["data"]);
|
|
158
231
|
$this->assertCount(2, $result["data"]);
|
|
159
232
|
$this->assertCount(1, $setup["calls"]);
|
|
@@ -164,23 +237,66 @@ class ${entity.Name}DirectTest extends TestCase
|
|
|
164
237
|
}
|
|
165
238
|
|
|
166
239
|
if (hasLoad && loadPoint) {
|
|
240
|
+
// Skip live direct-load only when there's no way to fill path params:
|
|
241
|
+
// no spec examples and no list-bootstrap. Spec examples win first.
|
|
242
|
+
const loadSkipBlock = (loadParams.length > 0 && !loadAllHaveExamples)
|
|
243
|
+
? ` if ($setup["live"]) {
|
|
244
|
+
$this->markTestSkipped("live direct-load needs real ID — set *_ENTID env var with real IDs to run");
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
`
|
|
248
|
+
: ''
|
|
167
249
|
Content(` public function test_direct_load_${entity.name}(): void
|
|
168
250
|
{
|
|
169
251
|
$setup = ${entity.name}_direct_setup(["id" => "direct01"]);
|
|
170
|
-
$
|
|
252
|
+
[$_shouldSkip, $_reason] = Runner::is_control_skipped("direct", "direct-load-${entity.name}", $setup["live"] ? "live" : "unit");
|
|
253
|
+
if ($_shouldSkip) {
|
|
254
|
+
$this->markTestSkipped($_reason ?? "skipped via sdk-test-control.json");
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
${loadSkipBlock} $client = $setup["client"];
|
|
171
258
|
|
|
172
259
|
`)
|
|
173
260
|
|
|
174
|
-
|
|
261
|
+
const needsQuery = loadParams.length > 0 || loadLiveQueryLines !== ''
|
|
262
|
+
if (needsQuery) {
|
|
175
263
|
Content(` $params = [];
|
|
176
|
-
|
|
264
|
+
$query = [];
|
|
265
|
+
`)
|
|
266
|
+
if (loadAllHaveExamples) {
|
|
267
|
+
Content(` if ($setup["live"]) {
|
|
177
268
|
`)
|
|
178
|
-
|
|
179
|
-
Content(
|
|
269
|
+
if (loadLiveQueryLines) Content(loadLiveQueryLines + '\n')
|
|
270
|
+
Content(loadExampleLines + '\n')
|
|
271
|
+
Content(` } else {
|
|
180
272
|
`)
|
|
273
|
+
for (let i = 0; i < loadParams.length; i++) {
|
|
274
|
+
Content(` $params["${loadParams[i].name}"] = "direct0${i + 1}";
|
|
275
|
+
`)
|
|
276
|
+
}
|
|
277
|
+
Content(` }
|
|
278
|
+
`)
|
|
279
|
+
} else if (loadParams.length > 0) {
|
|
280
|
+
if (loadLiveQueryLines) {
|
|
281
|
+
Content(` if ($setup["live"]) {
|
|
282
|
+
${loadLiveQueryLines}
|
|
181
283
|
}
|
|
182
|
-
Content(` }
|
|
183
284
|
`)
|
|
285
|
+
}
|
|
286
|
+
Content(` if (!$setup["live"]) {
|
|
287
|
+
`)
|
|
288
|
+
for (let i = 0; i < loadParams.length; i++) {
|
|
289
|
+
Content(` $params["${loadParams[i].name}"] = "direct0${i + 1}";
|
|
290
|
+
`)
|
|
291
|
+
}
|
|
292
|
+
Content(` }
|
|
293
|
+
`)
|
|
294
|
+
} else if (loadLiveQueryLines) {
|
|
295
|
+
Content(` if ($setup["live"]) {
|
|
296
|
+
${loadLiveQueryLines}
|
|
297
|
+
}
|
|
298
|
+
`)
|
|
299
|
+
}
|
|
184
300
|
}
|
|
185
301
|
|
|
186
302
|
Content(`
|
|
@@ -188,20 +304,37 @@ class ${entity.Name}DirectTest extends TestCase
|
|
|
188
304
|
"path" => "${loadPath}",
|
|
189
305
|
"method" => "GET",
|
|
190
306
|
`)
|
|
191
|
-
if (
|
|
307
|
+
if (needsQuery) {
|
|
192
308
|
Content(` "params" => $params,
|
|
309
|
+
"query" => $query,
|
|
193
310
|
`)
|
|
194
311
|
} else {
|
|
195
312
|
Content(` "params" => [],
|
|
196
313
|
`)
|
|
197
314
|
}
|
|
198
315
|
Content(` ]);
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
316
|
+
if ($setup["live"]) {
|
|
317
|
+
// Live mode is lenient: synthetic IDs frequently 4xx. Skip
|
|
318
|
+
// rather than fail when the load endpoint isn't reachable
|
|
319
|
+
// with the IDs we can construct from setup.idmap.
|
|
320
|
+
if ($err !== null) {
|
|
321
|
+
$this->markTestSkipped("load call failed (likely synthetic IDs against live API): " . (string)$err);
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
if (empty($result["ok"])) {
|
|
325
|
+
$this->markTestSkipped("load call not ok (likely synthetic IDs against live API)");
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
$status = Helpers::to_int($result["status"]);
|
|
329
|
+
if ($status < 200 || $status >= 300) {
|
|
330
|
+
$this->markTestSkipped("expected 2xx status, got " . $status);
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
} else {
|
|
334
|
+
$this->assertNull($err);
|
|
335
|
+
$this->assertTrue($result["ok"]);
|
|
336
|
+
$this->assertEquals(200, Helpers::to_int($result["status"]));
|
|
337
|
+
$this->assertNotNull($result["data"]);
|
|
205
338
|
if (is_array($result["data"]) && isset($result["data"]["id"])) {
|
|
206
339
|
$this->assertEquals("direct01", $result["data"]["id"]);
|
|
207
340
|
}
|
|
@@ -109,6 +109,21 @@ class ${entity.Name}EntityTest extends TestCase
|
|
|
109
109
|
public function test_basic_flow(): void
|
|
110
110
|
{
|
|
111
111
|
$setup = ${entity.name}_basic_setup(null);
|
|
112
|
+
// Per-op sdk-test-control.json skip.
|
|
113
|
+
$_live = !empty($setup["live"]);
|
|
114
|
+
foreach ([${(Array.from(new Set((allSteps as any[]).map((s: any) => s.op).filter(Boolean)))).map(o => `"${o}"`).join(', ')}] as $_op) {
|
|
115
|
+
[$_shouldSkip, $_reason] = Runner::is_control_skipped("entityOp", "${entity.name}." . $_op, $_live ? "live" : "unit");
|
|
116
|
+
if ($_shouldSkip) {
|
|
117
|
+
$this->markTestSkipped($_reason ?? "skipped via sdk-test-control.json");
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
// The basic flow consumes synthetic IDs from the fixture. In live mode
|
|
122
|
+
// without an *_ENTID env override, those IDs hit the live API and 4xx.
|
|
123
|
+
if (!empty($setup["synthetic_only"])) {
|
|
124
|
+
$this->markTestSkipped("live entity test uses synthetic IDs from fixture — set ${PROJUPPER}_TEST_${entity.name.toUpperCase().replace(/[^A-Z_]/g, '_')}_ENTID JSON to run live");
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
112
127
|
$client = $setup["client"];
|
|
113
128
|
|
|
114
129
|
`)
|
|
@@ -166,7 +181,13 @@ class ${entity.Name}EntityTest extends TestCase
|
|
|
166
181
|
|
|
167
182
|
`)
|
|
168
183
|
|
|
169
|
-
Content(`
|
|
184
|
+
Content(` // Detect ENTID env override before envOverride consumes it. When live
|
|
185
|
+
// mode is on without a real override, the basic test runs against synthetic
|
|
186
|
+
// IDs from the fixture and 4xx's. Surface this so the test can skip.
|
|
187
|
+
$entid_env_raw = getenv("${PROJUPPER}_TEST_${entity.name.toUpperCase().replace(/[^A-Z_]/g, '_')}_ENTID");
|
|
188
|
+
$idmap_overridden = $entid_env_raw !== false && str_starts_with(trim($entid_env_raw), "{");
|
|
189
|
+
|
|
190
|
+
$env = Runner::env_override([
|
|
170
191
|
"${PROJUPPER}_TEST_${entity.name.toUpperCase().replace(/[^A-Z_]/g, '_')}_ENTID" => $idmap,
|
|
171
192
|
"${PROJUPPER}_TEST_LIVE" => "FALSE",
|
|
172
193
|
"${PROJUPPER}_TEST_EXPLAIN" => "FALSE",${apikeyEnvEntry}
|
|
@@ -197,12 +218,15 @@ class ${entity.Name}EntityTest extends TestCase
|
|
|
197
218
|
$client = new ${model.const.Name}SDK(Helpers::to_map($merged_opts));
|
|
198
219
|
}
|
|
199
220
|
|
|
221
|
+
$live = $env["${PROJUPPER}_TEST_LIVE"] === "TRUE";
|
|
200
222
|
return [
|
|
201
223
|
"client" => $client,
|
|
202
224
|
"data" => $entity_data,
|
|
203
225
|
"idmap" => $idmap_resolved,
|
|
204
226
|
"env" => $env,
|
|
205
227
|
"explain" => $env["${PROJUPPER}_TEST_EXPLAIN"] === "TRUE",
|
|
228
|
+
"live" => $live,
|
|
229
|
+
"synthetic_only" => $live && !$idmap_overridden,
|
|
206
230
|
"now" => (int)(microtime(true) * 1000),
|
|
207
231
|
];
|
|
208
232
|
}
|
|
@@ -188,10 +188,24 @@ class ProjectNameSDK
|
|
|
188
188
|
|
|
189
189
|
if (is_array($fetched)) {
|
|
190
190
|
$status = ProjectNameHelpers::to_int(Struct::getprop($fetched, "status"));
|
|
191
|
+
$headers = Struct::getprop($fetched, "headers") ?? [];
|
|
192
|
+
|
|
193
|
+
// No-body responses (204, 304) and explicit zero content-length
|
|
194
|
+
// must skip JSON parsing — calling json() on an empty body errors.
|
|
195
|
+
$content_length = is_array($headers) ? ($headers["content-length"] ?? null) : null;
|
|
196
|
+
$no_body = $status === 204 || $status === 304 || (string)$content_length === "0";
|
|
197
|
+
|
|
191
198
|
$json_data = null;
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
199
|
+
if (!$no_body) {
|
|
200
|
+
$jf = Struct::getprop($fetched, "json");
|
|
201
|
+
if (is_callable($jf)) {
|
|
202
|
+
try {
|
|
203
|
+
$json_data = $jf();
|
|
204
|
+
} catch (\Throwable $e) {
|
|
205
|
+
// Non-JSON body — leave data null but keep status/ok.
|
|
206
|
+
$json_data = null;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
195
209
|
}
|
|
196
210
|
|
|
197
211
|
return [[
|