@voxgig/sdkgen 4.23.0 → 4.24.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 +18 -2
- package/dist/action/doctor.d.ts +1 -1
- package/dist/action/doctor.js +12 -10
- package/dist/action/doctor.js.map +1 -1
- package/dist/action/edition.js +4 -0
- package/dist/action/edition.js.map +1 -1
- package/dist/action/feature.js +5 -1
- package/dist/action/feature.js.map +1 -1
- package/dist/action/remove.d.ts +16 -0
- package/dist/action/remove.js +345 -0
- package/dist/action/remove.js.map +1 -0
- package/dist/action/target.js +6 -1
- package/dist/action/target.js.map +1 -1
- package/dist/cmp/Deploy.js +9 -6
- package/dist/cmp/Deploy.js.map +1 -1
- package/dist/helpers/semver.js +3 -1
- package/dist/helpers/semver.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -2
- package/project/.sdk/tm/c/core/context.c +3 -1
- package/project/.sdk/tm/c/feature/paging.c +37 -3
- package/project/.sdk/tm/c/feature/test.c +51 -27
- package/project/.sdk/tm/c/utility/prepare_query.c +2 -1
- package/project/.sdk/tm/c/utility/transform_request.c +25 -3
- package/project/.sdk/tm/clojure/src/sdk/core.clj +21 -7
- package/project/.sdk/tm/clojure/src/sdk/features.clj +68 -18
- package/project/.sdk/tm/cpp/core/types.hpp +1 -1
- package/project/.sdk/tm/cpp/feature/paging.hpp +36 -3
- package/project/.sdk/tm/cpp/feature/test.hpp +41 -13
- package/project/.sdk/tm/cpp/utility/pipeline.hpp +16 -4
- package/project/.sdk/tm/csharp/core/Context.cs +1 -1
- package/project/.sdk/tm/csharp/feature/PagingFeature.cs +58 -4
- package/project/.sdk/tm/csharp/feature/TestFeature.cs +13 -21
- package/project/.sdk/tm/csharp/utility/Fetcher.cs +19 -12
- package/project/.sdk/tm/csharp/utility/PrepareQuery.cs +1 -1
- package/project/.sdk/tm/csharp/utility/TransformRequest.cs +23 -3
- package/project/.sdk/tm/elixir/lib/projectname/context.ex +1 -1
- package/project/.sdk/tm/elixir/lib/projectname/feature/paging.ex +52 -10
- package/project/.sdk/tm/elixir/lib/projectname/feature/test.ex +47 -17
- package/project/.sdk/tm/elixir/lib/projectname/utility.ex +30 -9
- package/project/.sdk/tm/go/core/context.go +13 -13
- package/project/.sdk/tm/go/feature/paging_feature.go +40 -2
- package/project/.sdk/tm/go/feature/test_feature.go +3 -15
- package/project/.sdk/tm/go/test/feature_corpus_test.go +1 -0
- package/project/.sdk/tm/go/test/feature_test.go +54 -0
- package/project/.sdk/tm/go/test/pipeline_test.go +163 -57
- package/project/.sdk/tm/go/test/primary_utility_test.go +53 -0
- package/project/.sdk/tm/go/utility/fetcher.go +41 -20
- package/project/.sdk/tm/go/utility/prepare_query.go +1 -1
- package/project/.sdk/tm/go/utility/transform_request.go +22 -3
- package/project/.sdk/tm/java/core/Context.java +2 -1
- package/project/.sdk/tm/java/feature/PagingFeature.java +46 -4
- package/project/.sdk/tm/java/feature/TestFeature.java +10 -13
- package/project/.sdk/tm/java/test/FeatureCorpusTest.java +3 -0
- package/project/.sdk/tm/java/test/FeatureTest.java +42 -0
- package/project/.sdk/tm/java/test/PipelineTest.java +141 -48
- package/project/.sdk/tm/java/test/PrimaryUtilityTest.java +67 -6
- package/project/.sdk/tm/java/utility/Fetcher.java +36 -28
- package/project/.sdk/tm/java/utility/PrepareQuery.java +1 -1
- package/project/.sdk/tm/java/utility/TransformRequest.java +18 -3
- package/project/.sdk/tm/js/src/Context.js +7 -2
- package/project/.sdk/tm/js/src/feature/paging/PagingFeature.js +24 -6
- package/project/.sdk/tm/js/src/feature/test/TestFeature.js +7 -5
- package/project/.sdk/tm/js/src/utility/PrepareQueryUtility.js +1 -1
- package/project/.sdk/tm/js/test/feature/Corpus.test.js +4 -0
- package/project/.sdk/tm/js/test/pipeline.test.js +88 -22
- package/project/.sdk/tm/js/test/utility/PrimaryUtility.test.js +47 -1
- package/project/.sdk/tm/kotlin/core/Context.kt +1 -1
- package/project/.sdk/tm/kotlin/feature/PagingFeature.kt +36 -4
- package/project/.sdk/tm/kotlin/feature/TestFeature.kt +11 -14
- package/project/.sdk/tm/kotlin/utility/Fetcher.kt +33 -24
- package/project/.sdk/tm/kotlin/utility/Prepare.kt +1 -1
- package/project/.sdk/tm/kotlin/utility/ResultTransform.kt +18 -3
- package/project/.sdk/tm/lua/core/context.lua +8 -1
- package/project/.sdk/tm/lua/core/control.lua +3 -0
- package/project/.sdk/tm/lua/feature/paging_feature.lua +44 -5
- package/project/.sdk/tm/lua/feature/test_feature.lua +32 -14
- package/project/.sdk/tm/lua/test/omni.lua +8 -1
- package/project/.sdk/tm/lua/test/pipeline_test.lua +107 -28
- package/project/.sdk/tm/lua/test/primary_utility_test.lua +83 -3
- package/project/.sdk/tm/lua/utility/prepare_query.lua +2 -1
- package/project/.sdk/tm/lua/utility/transform_request.lua +19 -3
- package/project/.sdk/tm/ocaml/sdk_features.ml +72 -21
- package/project/.sdk/tm/ocaml/sdk_runtime.ml +23 -8
- package/project/.sdk/tm/perl/core/context.pm +2 -1
- package/project/.sdk/tm/perl/feature/paging_feature.pm +31 -5
- package/project/.sdk/tm/perl/feature/test_feature.pm +46 -16
- package/project/.sdk/tm/perl/feature/timeout_feature.pm +17 -13
- package/project/.sdk/tm/perl/t/feature_corpus.t +14 -1
- package/project/.sdk/tm/perl/t/omni.pm +7 -1
- package/project/.sdk/tm/perl/t/pipeline.t +124 -27
- package/project/.sdk/tm/perl/t/primary_utility.t +73 -0
- package/project/.sdk/tm/perl/utility/fetcher.pm +15 -1
- package/project/.sdk/tm/perl/utility/prepare_query.pm +1 -0
- package/project/.sdk/tm/perl/utility/transform_request.pm +13 -3
- package/project/.sdk/tm/php/core/Context.php +5 -1
- package/project/.sdk/tm/php/core/Control.php +5 -0
- package/project/.sdk/tm/php/feature/PagingFeature.php +26 -5
- package/project/.sdk/tm/php/feature/TestFeature.php +27 -42
- package/project/.sdk/tm/php/test/FeatureCorpusTest.php +17 -1
- package/project/.sdk/tm/php/test/FeatureTest.php +44 -0
- package/project/.sdk/tm/php/test/PipelineTest.php +143 -26
- package/project/.sdk/tm/php/test/PrimaryUtilityTest.php +80 -2
- package/project/.sdk/tm/php/utility/Fetcher.php +17 -3
- package/project/.sdk/tm/php/utility/PrepareQuery.php +1 -1
- package/project/.sdk/tm/php/utility/TransformRequest.php +15 -3
- package/project/.sdk/tm/py/pkg/core/context.py +2 -1
- package/project/.sdk/tm/py/pkg/feature/paging_feature.py +29 -4
- package/project/.sdk/tm/py/pkg/feature/test_feature.py +29 -12
- package/project/.sdk/tm/py/pkg/utility/fetcher.py +63 -65
- package/project/.sdk/tm/py/pkg/utility/prepare_query.py +2 -1
- package/project/.sdk/tm/py/pkg/utility/transform_request.py +11 -3
- package/project/.sdk/tm/py/test/test_feature.py +27 -0
- package/project/.sdk/tm/py/test/test_feature_corpus.py +11 -1
- package/project/.sdk/tm/py/test/test_pipeline.py +108 -31
- package/project/.sdk/tm/py/test/test_primary_utility.py +76 -1
- package/project/.sdk/tm/rb/core/context.rb +1 -1
- package/project/.sdk/tm/rb/feature/paging_feature.rb +27 -5
- package/project/.sdk/tm/rb/feature/test_feature.rb +33 -7
- package/project/.sdk/tm/rb/test/feature_corpus_test.rb +12 -1
- package/project/.sdk/tm/rb/test/feature_test.rb +38 -0
- package/project/.sdk/tm/rb/test/pipeline_test.rb +100 -24
- package/project/.sdk/tm/rb/test/primary_utility_test.rb +59 -1
- package/project/.sdk/tm/rb/utility/fetcher.rb +49 -3
- package/project/.sdk/tm/rb/utility/prepare_query.rb +1 -1
- package/project/.sdk/tm/rb/utility/transform_request.rb +10 -3
- package/project/.sdk/tm/rust/core/context.rs +2 -6
- package/project/.sdk/tm/rust/feature/paging.rs +44 -3
- package/project/.sdk/tm/rust/feature/secrets.rs +1 -1
- package/project/.sdk/tm/rust/feature/test.rs +66 -33
- package/project/.sdk/tm/rust/utility/fetcher.rs +41 -14
- package/project/.sdk/tm/rust/utility/prepare_query.rs +1 -1
- package/project/.sdk/tm/rust/utility/transform_request.rs +31 -5
- package/project/.sdk/tm/scala/core/Context.scala +2 -1
- package/project/.sdk/tm/scala/feature/PagingFeature.scala +41 -4
- package/project/.sdk/tm/scala/feature/TestFeature.scala +11 -12
- package/project/.sdk/tm/scala/utility/Fetcher.scala +34 -13
- package/project/.sdk/tm/scala/utility/Prepare.scala +1 -1
- package/project/.sdk/tm/scala/utility/Transform.scala +19 -3
- package/project/.sdk/tm/swift/Sources/ProjectNameSDK/core/Context.swift +1 -1
- package/project/.sdk/tm/swift/Sources/ProjectNameSDK/feature/PagingFeature.swift +31 -4
- package/project/.sdk/tm/swift/Sources/ProjectNameSDK/feature/TestFeature.swift +12 -9
- package/project/.sdk/tm/swift/Sources/ProjectNameSDK/utility/Fetcher.swift +22 -3
- package/project/.sdk/tm/swift/Sources/ProjectNameSDK/utility/Prepare.swift +1 -1
- package/project/.sdk/tm/swift/Sources/ProjectNameSDK/utility/ResultUtil.swift +14 -3
- package/project/.sdk/tm/ts/src/Context.ts +7 -2
- package/project/.sdk/tm/ts/src/feature/paging/PagingFeature.ts +19 -3
- package/project/.sdk/tm/ts/src/feature/test/TestFeature.ts +3 -0
- package/project/.sdk/tm/ts/src/utility/PrepareQueryUtility.ts +1 -1
- package/project/.sdk/tm/ts/test/feature/Corpus.test.ts +1 -0
- package/project/.sdk/tm/ts/test/pipeline.test.ts +52 -11
- package/project/.sdk/tm/zig/core/context.zig +1 -1
- package/project/.sdk/tm/zig/core/utility.zig +19 -5
- package/project/.sdk/tm/zig/feature/paging.zig +34 -4
- package/project/.sdk/tm/zig/feature/test.zig +54 -26
- package/project/sdkgen-package.json +1 -1
- package/src/action/doctor.ts +12 -9
- package/src/action/edition.ts +8 -0
- package/src/action/feature.ts +7 -1
- package/src/action/remove.ts +492 -0
- package/src/action/target.ts +8 -1
- package/src/cmp/Deploy.ts +11 -6
- package/src/helpers/semver.ts +3 -1
|
@@ -7,8 +7,9 @@ const { BaseFeature } = require('../base/BaseFeature')
|
|
|
7
7
|
// stamps page/limit (or a cursor) into the request query; on the way back
|
|
8
8
|
// (PreResult) it reads the server's pagination signals — a `Link:
|
|
9
9
|
// rel="next"` header, `X-Next-Page`/`X-Total-Count` headers, or `next`/
|
|
10
|
-
// `cursor`/`hasMore` fields in the body
|
|
11
|
-
// `ctx.result.paging
|
|
10
|
+
// `cursor`/`hasMore` fields in the body (snake_case forms too) — and
|
|
11
|
+
// records them on `ctx.result.paging` and the caller's `ctrl.paging`.
|
|
12
|
+
// Generated SDKs build auto-iteration on top of this
|
|
12
13
|
// (advance the cursor/page and re-issue the list call until `hasMore` is
|
|
13
14
|
// false). Parameter names and page size are configurable.
|
|
14
15
|
class PagingFeature extends BaseFeature {
|
|
@@ -28,7 +29,7 @@ class PagingFeature extends BaseFeature {
|
|
|
28
29
|
|
|
29
30
|
|
|
30
31
|
PreRequest(ctx) {
|
|
31
|
-
if (!this._isList(ctx)) {
|
|
32
|
+
if (!this.active || !this._isList(ctx)) {
|
|
32
33
|
return
|
|
33
34
|
}
|
|
34
35
|
const spec = ctx.spec
|
|
@@ -58,7 +59,10 @@ class PagingFeature extends BaseFeature {
|
|
|
58
59
|
spec.query[cursorParam] = paging.cursor
|
|
59
60
|
}
|
|
60
61
|
else if (null == spec.query[pageParam]) {
|
|
61
|
-
|
|
62
|
+
// A record written back by PreResult holds the page just fetched as
|
|
63
|
+
// `page` and the one to fetch as `nextPage`, so nextPage wins.
|
|
64
|
+
spec.query[pageParam] = null != paging.nextPage ? paging.nextPage :
|
|
65
|
+
null != paging.page ? paging.page : (this._options.startPage || 1)
|
|
62
66
|
}
|
|
63
67
|
|
|
64
68
|
if (null != this._options.limit && null == spec.query[limitParam]) {
|
|
@@ -68,7 +72,7 @@ class PagingFeature extends BaseFeature {
|
|
|
68
72
|
|
|
69
73
|
|
|
70
74
|
PreResult(ctx) {
|
|
71
|
-
if (!this._isList(ctx)) {
|
|
75
|
+
if (!this.active || !this._isList(ctx)) {
|
|
72
76
|
return
|
|
73
77
|
}
|
|
74
78
|
const result = ctx.result
|
|
@@ -124,11 +128,20 @@ class PagingFeature extends BaseFeature {
|
|
|
124
128
|
}
|
|
125
129
|
}
|
|
126
130
|
|
|
127
|
-
// Body-level
|
|
131
|
+
// Body-level signals; snake_case forms are read first so camelCase wins.
|
|
128
132
|
if (body && 'object' === typeof body) {
|
|
129
133
|
if (null != body.next) { paging.next = paging.next || body.next }
|
|
134
|
+
if (null != body.next_cursor) { paging.cursor = body.next_cursor }
|
|
130
135
|
if (null != body.cursor) { paging.cursor = body.cursor }
|
|
131
136
|
if (null != body.nextCursor) { paging.cursor = body.nextCursor }
|
|
137
|
+
if (null == paging.nextPage) {
|
|
138
|
+
const np = null != body.nextPage ? body.nextPage : body.next_page
|
|
139
|
+
if ('number' === typeof np || 'string' === typeof np) { paging.nextPage = np }
|
|
140
|
+
}
|
|
141
|
+
if ('boolean' === typeof body.has_more) {
|
|
142
|
+
paging.hasMore = body.has_more
|
|
143
|
+
explicitMore = true
|
|
144
|
+
}
|
|
132
145
|
if ('boolean' === typeof body.hasMore) {
|
|
133
146
|
paging.hasMore = body.hasMore
|
|
134
147
|
explicitMore = true
|
|
@@ -147,6 +160,11 @@ class PagingFeature extends BaseFeature {
|
|
|
147
160
|
|
|
148
161
|
result.paging = paging
|
|
149
162
|
|
|
163
|
+
// ctx.ctrl is the caller's own object, so the record reaches them.
|
|
164
|
+
if (null != ctx.ctrl) {
|
|
165
|
+
ctx.ctrl.paging = paging
|
|
166
|
+
}
|
|
167
|
+
|
|
150
168
|
const client = this._client
|
|
151
169
|
client._paging = { last: paging }
|
|
152
170
|
}
|
|
@@ -148,6 +148,7 @@ class TestFeature extends BaseFeature {
|
|
|
148
148
|
const found = select(entmap, args)
|
|
149
149
|
const ent = getelem(found, 0)
|
|
150
150
|
if (null == ent) {
|
|
151
|
+
// update miss: 404, never another record
|
|
151
152
|
return respond(404, undefined, { statusText: S_NOT_FOUND })
|
|
152
153
|
}
|
|
153
154
|
else {
|
|
@@ -161,13 +162,12 @@ class TestFeature extends BaseFeature {
|
|
|
161
162
|
const args = self.buildArgs(ctx, op, ctx.reqmatch)
|
|
162
163
|
const found = select(entmap, args)
|
|
163
164
|
const ent = getelem(found, 0)
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
else {
|
|
165
|
+
// Remove only the first matched entity. If nothing matches,
|
|
166
|
+
// succeed as a no-op rather than erroring.
|
|
167
|
+
if (null != ent) {
|
|
168
168
|
delprop(entmap, getprop(ent, 'id'))
|
|
169
|
-
return respond(200)
|
|
170
169
|
}
|
|
170
|
+
return respond(200)
|
|
171
171
|
}
|
|
172
172
|
else if ('create' === op.name) {
|
|
173
173
|
const args = self.buildArgs(ctx, op, ctx.reqdata)
|
|
@@ -183,6 +183,8 @@ class TestFeature extends BaseFeature {
|
|
|
183
183
|
const out = clone(ent)
|
|
184
184
|
return respond(200, out)
|
|
185
185
|
}
|
|
186
|
+
|
|
187
|
+
return respond(404, undefined, { statusText: 'Unknown operation' })
|
|
186
188
|
}
|
|
187
189
|
|
|
188
190
|
// Optional network behaviour simulation over the mock transport. Enable
|
|
@@ -254,6 +254,10 @@ describe('FeatureCorpus', () => {
|
|
|
254
254
|
|
|
255
255
|
const probe = makeClient({})
|
|
256
256
|
if (!probe._rootctx.config.hasFeature(name)) {
|
|
257
|
+
// The one line every runner prints for a section that names a
|
|
258
|
+
// feature this SDK does not generate, so an inert section is
|
|
259
|
+
// visible in the output rather than inferred from a green tick.
|
|
260
|
+
t.diagnostic(`feature.${name}: inert (this SDK does not generate the feature)`)
|
|
257
261
|
return t.skip(`this SDK was generated without the ${name} feature`)
|
|
258
262
|
}
|
|
259
263
|
|
|
@@ -323,45 +323,111 @@ describe('pipeline:feature order', () => {
|
|
|
323
323
|
})
|
|
324
324
|
|
|
325
325
|
|
|
326
|
+
// A cookie credential as prepareAuth writes it: `<scheme>=K` for the probe
|
|
327
|
+
// key, with no scheme prefix and nothing else in the bag.
|
|
328
|
+
const COOKIE_PAIR = /^[^=;]+=K$/
|
|
329
|
+
|
|
330
|
+
|
|
326
331
|
describe('pipeline:prepareAuth', () => {
|
|
327
332
|
|
|
328
333
|
// Fake client so the exact options.auth / apikey shape is controlled.
|
|
329
|
-
function authCtx(options,
|
|
330
|
-
return base({ client: { options: () => options }, spec
|
|
334
|
+
function authCtx(options, spec) {
|
|
335
|
+
return base({ client: { options: () => options }, spec })
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function bags() {
|
|
339
|
+
return { headers: {}, query: {} }
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// `basic: false` is explicit: an HTTP Basic API's generated config carries
|
|
343
|
+
// `auth.basic: true`, and a client that merges it in takes a branch that
|
|
344
|
+
// needs a secret as well. With none supplied that branch deliberately
|
|
345
|
+
// writes nothing, which the probe below would read as a public API.
|
|
346
|
+
function auth(prefix) {
|
|
347
|
+
return { prefix, basic: false }
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// Run prepareAuth with both containers present and see which one the
|
|
351
|
+
// generated utility writes to, and under what name. Null means this SDK
|
|
352
|
+
// places no credential at all — a public API — which is a legitimate
|
|
353
|
+
// shape, and the tests below assert exactly that instead.
|
|
354
|
+
//
|
|
355
|
+
// `pair` is the `<scheme>=` lead-in of a COOKIE credential, which rides the
|
|
356
|
+
// header bag under the key `cookie` instead of taking a header of its own.
|
|
357
|
+
function probe(options) {
|
|
358
|
+
const ctx = authCtx(options, bags())
|
|
359
|
+
stdutil.prepareAuth(ctx)
|
|
360
|
+
for (const where of ['headers', 'query']) {
|
|
361
|
+
const name = Object.keys(ctx.spec[where])[0]
|
|
362
|
+
if (null == name) continue
|
|
363
|
+
const value = ctx.spec[where][name]
|
|
364
|
+
const pair = 'headers' === where && 'cookie' === name
|
|
365
|
+
&& COOKIE_PAIR.test(String(value))
|
|
366
|
+
? String(value).slice(0, -1)
|
|
367
|
+
: ''
|
|
368
|
+
return { where, name, value, pair }
|
|
369
|
+
}
|
|
370
|
+
return null
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
const CRED = probe({ apikey: 'K', auth: auth('Bearer') })
|
|
374
|
+
|
|
375
|
+
// Every credential this SDK could possibly place: both credentials and
|
|
376
|
+
// Basic switched on, so whichever branch the API has, something lands
|
|
377
|
+
// unless the API is public.
|
|
378
|
+
const ANY = probe({ apikey: 'K', secret: 'S', auth: { prefix: 'Bearer', basic: true } })
|
|
379
|
+
|
|
380
|
+
function placed(options, seed) {
|
|
381
|
+
const spec = bags()
|
|
382
|
+
if (null != CRED && null != seed) spec[CRED.where][CRED.name] = seed
|
|
383
|
+
const ctx = authCtx(options, spec)
|
|
384
|
+
stdutil.prepareAuth(ctx)
|
|
385
|
+
return null == CRED ? undefined : ctx.spec[CRED.where][CRED.name]
|
|
331
386
|
}
|
|
332
387
|
|
|
333
388
|
test('guards a missing spec', () => {
|
|
334
|
-
strictEqual(stdutil.prepareAuth(authCtx({ auth:
|
|
389
|
+
strictEqual(stdutil.prepareAuth(authCtx({ auth: auth(''), apikey: 'K' }, null)).code, 'auth_no_spec')
|
|
335
390
|
})
|
|
336
391
|
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
strictEqual(
|
|
392
|
+
// Without this the cases below cannot fail for an SDK whose credential the
|
|
393
|
+
// probe misses: every one of them takes the public-API path instead.
|
|
394
|
+
test('the probe finds the credential this SDK places', () => {
|
|
395
|
+
strictEqual(null != CRED, null != ANY)
|
|
396
|
+
})
|
|
397
|
+
|
|
398
|
+
test('the apikey is placed where this API puts it', () => {
|
|
399
|
+
if (null == CRED) {
|
|
400
|
+
// A public API places nothing, and that is the whole assertion.
|
|
401
|
+
strictEqual(placed({ apikey: 'K', auth: auth('Bearer') }), undefined)
|
|
402
|
+
return
|
|
403
|
+
}
|
|
404
|
+
ok('headers' === CRED.where || 'query' === CRED.where)
|
|
405
|
+
if ('' !== CRED.pair) {
|
|
406
|
+
// A cookie credential is a `<scheme>=<key>` pair, and the scheme name
|
|
407
|
+
// leaves no room for the option's prefix.
|
|
408
|
+
ok(COOKIE_PAIR.test(String(CRED.value)), String(CRED.value))
|
|
409
|
+
return
|
|
410
|
+
}
|
|
411
|
+
// A header credential is prefix-joined; a query credential is the raw
|
|
412
|
+
// key, because a query parameter has nowhere to put a scheme name.
|
|
413
|
+
strictEqual(CRED.value, 'headers' === CRED.where ? 'Bearer K' : 'K')
|
|
341
414
|
})
|
|
342
415
|
|
|
343
416
|
test('a raw apikey (empty prefix) goes in as-is', () => {
|
|
344
|
-
const
|
|
345
|
-
|
|
346
|
-
strictEqual(ctx.spec.headers.authorization, 'K')
|
|
417
|
+
const raw = null == CRED ? undefined : CRED.pair + 'K'
|
|
418
|
+
strictEqual(placed({ apikey: 'K', auth: auth('') }), raw)
|
|
347
419
|
})
|
|
348
420
|
|
|
349
|
-
test('an empty apikey drops the
|
|
350
|
-
|
|
351
|
-
stdutil.prepareAuth(ctx)
|
|
352
|
-
strictEqual(ctx.spec.headers.authorization, undefined)
|
|
421
|
+
test('an empty apikey drops the credential', () => {
|
|
422
|
+
strictEqual(placed({ apikey: '', auth: auth('Bearer') }, 'stale'), undefined)
|
|
353
423
|
})
|
|
354
424
|
|
|
355
|
-
test('a public API (no auth block) drops the
|
|
356
|
-
|
|
357
|
-
stdutil.prepareAuth(ctx)
|
|
358
|
-
strictEqual(ctx.spec.headers.authorization, undefined)
|
|
425
|
+
test('a public API (no auth block) drops the credential', () => {
|
|
426
|
+
strictEqual(placed({ apikey: 'K' }, 'stale'), undefined)
|
|
359
427
|
})
|
|
360
428
|
|
|
361
|
-
test('a missing apikey option drops the
|
|
362
|
-
|
|
363
|
-
stdutil.prepareAuth(ctx)
|
|
364
|
-
strictEqual(ctx.spec.headers.authorization, undefined)
|
|
429
|
+
test('a missing apikey option drops the credential', () => {
|
|
430
|
+
strictEqual(placed({ auth: auth('Bearer') }, 'stale'), undefined)
|
|
365
431
|
})
|
|
366
432
|
})
|
|
367
433
|
|
|
@@ -79,10 +79,56 @@ describe('PrimaryUtility', async () => {
|
|
|
79
79
|
})
|
|
80
80
|
|
|
81
81
|
|
|
82
|
+
function credential() {
|
|
83
|
+
const ctx = {
|
|
84
|
+
utility,
|
|
85
|
+
// `basic: false`: a Basic API's config defaults it true, and the
|
|
86
|
+
// branch it selects needs a secret, so the probe would find nothing.
|
|
87
|
+
client: { options: () => ({ apikey: 'PROBE', auth: { prefix: '', basic: false } }) },
|
|
88
|
+
spec: { headers: {}, query: {} },
|
|
89
|
+
error: (code, msg) => Object.assign(new Error(msg), { code }),
|
|
90
|
+
}
|
|
91
|
+
try { utility.prepareAuth(ctx) } catch (e) { return null }
|
|
92
|
+
for (const where of ['headers', 'query']) {
|
|
93
|
+
const name = Object.keys(ctx.spec[where] || {})[0]
|
|
94
|
+
if (null != name) return { where, name }
|
|
95
|
+
}
|
|
96
|
+
return null
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Rename the corpus's `headers` bag to the real container, and the
|
|
100
|
+
// `authorization` key inside it to the real credential name. Applied only
|
|
101
|
+
// to the prepareAuth section, so real header assertions elsewhere are
|
|
102
|
+
// untouched.
|
|
103
|
+
function retarget(node, cred) {
|
|
104
|
+
if (null == node || 'object' !== typeof node) return node
|
|
105
|
+
if (Array.isArray(node)) return node.map((n) => retarget(n, cred))
|
|
106
|
+
const out = {}
|
|
107
|
+
for (const key of Object.keys(node)) {
|
|
108
|
+
if ('headers' === key) {
|
|
109
|
+
const bag = {}
|
|
110
|
+
for (const bk of Object.keys(node[key] || {})) {
|
|
111
|
+
bag['authorization' === bk ? cred.name : bk] = retarget(node[key][bk], cred)
|
|
112
|
+
}
|
|
113
|
+
out[cred.where] = bag
|
|
114
|
+
}
|
|
115
|
+
else out[key] = retarget(node[key], cred)
|
|
116
|
+
}
|
|
117
|
+
return out
|
|
118
|
+
}
|
|
119
|
+
|
|
82
120
|
test('auth-basic', async () => {
|
|
83
121
|
const sdkopts = spec.prepareAuth?.DEF?.setup?.a || {}
|
|
84
122
|
const authClient = SDK.test({}, sdkopts)
|
|
85
|
-
|
|
123
|
+
|
|
124
|
+
const cred = credential()
|
|
125
|
+
ok(null != cred, 'prepareAuth placed no credential in headers or query')
|
|
126
|
+
|
|
127
|
+
const cases = 'headers' === cred.where && 'authorization' === cred.name
|
|
128
|
+
? spec.prepareAuth.basic
|
|
129
|
+
: retarget(spec.prepareAuth.basic, cred)
|
|
130
|
+
|
|
131
|
+
await runset(cases, (ctx) => {
|
|
86
132
|
ctx.client = authClient
|
|
87
133
|
fixctx(ctx)
|
|
88
134
|
return utility.prepareAuth(ctx)
|
|
@@ -76,7 +76,7 @@ class Context(ctxmap: MutableMap<String, Any?>?, basectx: Context?) {
|
|
|
76
76
|
} else if (cv is Control) {
|
|
77
77
|
this.ctrl = cv
|
|
78
78
|
}
|
|
79
|
-
} else if (basectx != null) {
|
|
79
|
+
} else if (basectx != null && Helpers.getCtxProp(ctxmap, "opname") == null) {
|
|
80
80
|
this.ctrl = basectx.ctrl
|
|
81
81
|
}
|
|
82
82
|
|
|
@@ -8,8 +8,9 @@ import KOTLINPACKAGE.utility.struct.Struct
|
|
|
8
8
|
|
|
9
9
|
// Pagination support for list operations. On the way out (PreRequest) it
|
|
10
10
|
// stamps page/limit (or a cursor) into the request query; on the way back
|
|
11
|
-
// (PreResult) it reads the server's pagination signals and
|
|
12
|
-
// `ctx.result.paging
|
|
11
|
+
// (PreResult) it reads the server's pagination signals (camelCase and
|
|
12
|
+
// snake_case body forms) and records them on `ctx.result.paging` and the
|
|
13
|
+
// caller's `ctrl.paging`.
|
|
13
14
|
@Suppress("UNCHECKED_CAST")
|
|
14
15
|
class PagingFeature : BaseFeature("paging", "0.0.1", true) {
|
|
15
16
|
|
|
@@ -53,7 +54,9 @@ class PagingFeature : BaseFeature("paging", "0.0.1", true) {
|
|
|
53
54
|
if (cursor != null) {
|
|
54
55
|
spec.query[cursorParam] = cursor
|
|
55
56
|
} else if (spec.query[pageParam] == null) {
|
|
56
|
-
|
|
57
|
+
// A record written back by preResult holds the page just fetched as
|
|
58
|
+
// "page" and the one to fetch as "nextPage", so nextPage wins.
|
|
59
|
+
val page = paging["nextPage"] ?: paging["page"]
|
|
57
60
|
if (page != null) {
|
|
58
61
|
spec.query[pageParam] = page
|
|
59
62
|
} else {
|
|
@@ -166,18 +169,33 @@ class PagingFeature : BaseFeature("paging", "0.0.1", true) {
|
|
|
166
169
|
}
|
|
167
170
|
}
|
|
168
171
|
|
|
169
|
-
// Body-level
|
|
172
|
+
// Body-level signals; snake_case forms are read first so camelCase wins.
|
|
170
173
|
if (body is MutableMap<*, *>) {
|
|
171
174
|
val bm = body as MutableMap<String, Any?>
|
|
172
175
|
if (bm["next"] != null && paging["next"] == null) {
|
|
173
176
|
paging["next"] = bm["next"]
|
|
174
177
|
}
|
|
178
|
+
if (bm["next_cursor"] != null) {
|
|
179
|
+
paging["cursor"] = bm["next_cursor"]
|
|
180
|
+
}
|
|
175
181
|
if (bm["cursor"] != null) {
|
|
176
182
|
paging["cursor"] = bm["cursor"]
|
|
177
183
|
}
|
|
178
184
|
if (bm["nextCursor"] != null) {
|
|
179
185
|
paging["cursor"] = bm["nextCursor"]
|
|
180
186
|
}
|
|
187
|
+
if (paging["nextPage"] == null) {
|
|
188
|
+
val np = bm["nextPage"] ?: bm["next_page"]
|
|
189
|
+
if (np is Number) {
|
|
190
|
+
paging["nextPage"] = np.toInt()
|
|
191
|
+
} else if (np is String) {
|
|
192
|
+
paging["nextPage"] = np
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
if (bm["has_more"] is Boolean) {
|
|
196
|
+
paging["hasMore"] = bm["has_more"]
|
|
197
|
+
explicitMore = true
|
|
198
|
+
}
|
|
181
199
|
if (bm["hasMore"] is Boolean) {
|
|
182
200
|
paging["hasMore"] = bm["hasMore"]
|
|
183
201
|
explicitMore = true
|
|
@@ -197,6 +215,20 @@ class PagingFeature : BaseFeature("paging", "0.0.1", true) {
|
|
|
197
215
|
|
|
198
216
|
result.paging = paging
|
|
199
217
|
this.last = paging
|
|
218
|
+
|
|
219
|
+
// The Context shares the caller's paging map by reference, so refill it
|
|
220
|
+
// in place; an immutable map falls back to replacing the reference.
|
|
221
|
+
val shared = ctx.ctrl.paging
|
|
222
|
+
if (shared == null) {
|
|
223
|
+
ctx.ctrl.paging = paging
|
|
224
|
+
} else {
|
|
225
|
+
try {
|
|
226
|
+
shared.clear()
|
|
227
|
+
shared.putAll(paging)
|
|
228
|
+
} catch (e: UnsupportedOperationException) {
|
|
229
|
+
ctx.ctrl.paging = paging
|
|
230
|
+
}
|
|
231
|
+
}
|
|
200
232
|
}
|
|
201
233
|
|
|
202
234
|
private fun isList(ctx: Context): Boolean {
|
|
@@ -160,20 +160,13 @@ class TestFeature : BaseFeature("test", "0.0.1", true) {
|
|
|
160
160
|
}
|
|
161
161
|
val args = buildArgs(ctx, op, updateMatch)
|
|
162
162
|
val found = Struct.select(entmap, args)
|
|
163
|
-
|
|
164
|
-
if (ent == null) {
|
|
165
|
-
for (e in entmap.values) {
|
|
166
|
-
if (e is MutableMap<*, *>) {
|
|
167
|
-
ent = e
|
|
168
|
-
break
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
}
|
|
163
|
+
val ent = Struct.getelem(found, 0, null)
|
|
172
164
|
if (ent == null) {
|
|
165
|
+
// update miss: 404, never another record
|
|
173
166
|
return respond(ctx, 404, null, extra("statusText", "Not found"))
|
|
174
167
|
}
|
|
175
168
|
if (ent is MutableMap<*, *>) {
|
|
176
|
-
(ent
|
|
169
|
+
Struct.merge(Struct.jt(ent, reqdata))
|
|
177
170
|
}
|
|
178
171
|
Struct.delprop(ent, "\$KEY")
|
|
179
172
|
val out = Struct.clone(ent)
|
|
@@ -320,10 +313,14 @@ class TestFeature : BaseFeature("test", "0.0.1", true) {
|
|
|
320
313
|
}
|
|
321
314
|
}
|
|
322
315
|
|
|
323
|
-
//
|
|
324
|
-
|
|
325
|
-
val
|
|
326
|
-
|
|
316
|
+
// Path AND query: a path-only read misses a query-addressed record
|
|
317
|
+
// (e.g. GET /result?trace_id=), which has no path param at all.
|
|
318
|
+
val reqdArgs = Struct.jt()
|
|
319
|
+
for (kind in listOf("params", "query")) {
|
|
320
|
+
val argsPath = Struct.getpath(point, listOf("args", kind))
|
|
321
|
+
reqdArgs.addAll(Struct.select(argsPath, Struct.jm("reqd", true)))
|
|
322
|
+
}
|
|
323
|
+
val reqd = Struct.transform(reqdArgs, Struct.jt("`\$EACH`", "", "`\$KEY.name`"))
|
|
327
324
|
|
|
328
325
|
val qand = Struct.jt()
|
|
329
326
|
val q = Struct.jm("`\$AND`", qand)
|
|
@@ -6,12 +6,44 @@ import java.net.URI
|
|
|
6
6
|
import java.net.http.HttpClient
|
|
7
7
|
import java.net.http.HttpRequest
|
|
8
8
|
import java.net.http.HttpResponse
|
|
9
|
+
import java.util.concurrent.ConcurrentHashMap
|
|
9
10
|
import java.util.function.BiFunction
|
|
10
11
|
import java.util.function.Supplier
|
|
11
12
|
|
|
12
13
|
import KOTLINPACKAGE.core.Context
|
|
13
14
|
import KOTLINPACKAGE.utility.struct.Struct
|
|
14
15
|
|
|
16
|
+
// Clients keyed by redirect policy and proxy. Each HttpClient owns a
|
|
17
|
+
// connection pool and a selector thread, so one per request reused
|
|
18
|
+
// nothing; one per key pools across calls.
|
|
19
|
+
private val clients = ConcurrentHashMap<String, HttpClient>()
|
|
20
|
+
|
|
21
|
+
private fun clientFor(fetchdef: MutableMap<String, Any?>): HttpClient {
|
|
22
|
+
// "manual" (set by the station feature's middleware under a hosts
|
|
23
|
+
// policy) returns a 3xx like any other response: an automatic follow
|
|
24
|
+
// would carry injected credentials to a host no policy approved.
|
|
25
|
+
val redirectPolicy =
|
|
26
|
+
if ("manual" == fetchdef["redirect"]) HttpClient.Redirect.NEVER
|
|
27
|
+
else HttpClient.Redirect.NORMAL
|
|
28
|
+
|
|
29
|
+
val praw = fetchdef["proxy"]
|
|
30
|
+
val proxy = if (praw is String) praw else ""
|
|
31
|
+
|
|
32
|
+
return clients.computeIfAbsent("$redirectPolicy|$proxy") {
|
|
33
|
+
val clientb = HttpClient.newBuilder().followRedirects(redirectPolicy)
|
|
34
|
+
if ("" != proxy) {
|
|
35
|
+
try {
|
|
36
|
+
val proxyUri = URI.create(proxy)
|
|
37
|
+
val port = if (proxyUri.port < 0) 80 else proxyUri.port
|
|
38
|
+
clientb.proxy(ProxySelector.of(InetSocketAddress(proxyUri.host, port)))
|
|
39
|
+
} catch (e: RuntimeException) {
|
|
40
|
+
// Unparseable proxy target: direct connection, as the go transport.
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
clientb.build()
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
15
47
|
@Suppress("UNCHECKED_CAST")
|
|
16
48
|
fun defaultHttpFetch(fullurl: String, fetchdef: MutableMap<String, Any?>): MutableMap<String, Any?> {
|
|
17
49
|
var method = if (fetchdef["method"] is String) fetchdef["method"] as String else ""
|
|
@@ -50,32 +82,9 @@ fun defaultHttpFetch(fullurl: String, fetchdef: MutableMap<String, Any?>): Mutab
|
|
|
50
82
|
reqb.setHeader("User-Agent", "Mozilla/5.0 (compatible; ProjectNameSDK/1.0)")
|
|
51
83
|
}
|
|
52
84
|
|
|
53
|
-
// Honour a redirect annotation on the fetch definition (set by the
|
|
54
|
-
// station feature's middleware under a hosts policy): "manual" returns
|
|
55
|
-
// a 3xx like any other response instead of following it — the ts
|
|
56
|
-
// donor's fetch honours the same key natively, and an automatic follow
|
|
57
|
-
// would carry injected credentials to a host no policy approved.
|
|
58
|
-
val redirectPolicy =
|
|
59
|
-
if ("manual" == fetchdef["redirect"]) HttpClient.Redirect.NEVER
|
|
60
|
-
else HttpClient.Redirect.NORMAL
|
|
61
|
-
|
|
62
|
-
// Honour a proxy annotation on the fetch definition (set by the proxy
|
|
63
|
-
// feature): route the request through a proxied HttpClient.
|
|
64
|
-
val clientb = HttpClient.newBuilder().followRedirects(redirectPolicy)
|
|
65
|
-
val proxy = fetchdef["proxy"]
|
|
66
|
-
if (proxy is String && "" != proxy) {
|
|
67
|
-
try {
|
|
68
|
-
val proxyUri = URI.create(proxy)
|
|
69
|
-
val port = if (proxyUri.port < 0) 80 else proxyUri.port
|
|
70
|
-
clientb.proxy(ProxySelector.of(InetSocketAddress(proxyUri.host, port)))
|
|
71
|
-
} catch (e: RuntimeException) {
|
|
72
|
-
// Unparseable proxy target: fall through to a direct connection.
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
85
|
val resp: HttpResponse<String>
|
|
77
86
|
try {
|
|
78
|
-
resp =
|
|
87
|
+
resp = clientFor(fetchdef).send(reqb.build(), HttpResponse.BodyHandlers.ofString())
|
|
79
88
|
} catch (e: Exception) {
|
|
80
89
|
throw RuntimeException("fetch: " + e.message, e)
|
|
81
90
|
}
|
|
@@ -194,7 +194,7 @@ fun prepareQuery(ctx: Context): MutableMap<String, Any?> {
|
|
|
194
194
|
for (item in Struct.items(reqmatch)) {
|
|
195
195
|
val key = if (item[0] is String) item[0] as String else ""
|
|
196
196
|
val v = item[1]
|
|
197
|
-
if (v != null && !containsStr(params, key)) {
|
|
197
|
+
if (v != null && "\$action" != key && !containsStr(params, key)) {
|
|
198
198
|
out[key] = v
|
|
199
199
|
}
|
|
200
200
|
}
|
|
@@ -58,21 +58,36 @@ fun resultHeaders(ctx: Context): KOTLINPACKAGE.core.Result {
|
|
|
58
58
|
return result!!
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
+
// `$action` selects the point (see makePoint); it is never an API field, so
|
|
62
|
+
// the body is a copy without it. The caller's map is left untouched.
|
|
63
|
+
private fun stripAction(reqdata: Any?): Any? {
|
|
64
|
+
if (reqdata !is Map<*, *> || !reqdata.containsKey("\$action")) {
|
|
65
|
+
return reqdata
|
|
66
|
+
}
|
|
67
|
+
val body = linkedMapOf<String, Any?>()
|
|
68
|
+
for ((k, v) in reqdata) {
|
|
69
|
+
if ("\$action" != k) {
|
|
70
|
+
body[k.toString()] = v
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return body
|
|
74
|
+
}
|
|
75
|
+
|
|
61
76
|
fun transformRequest(ctx: Context): Any? {
|
|
62
77
|
if (ctx.spec != null) {
|
|
63
78
|
ctx.spec!!.step = "reqform"
|
|
64
79
|
}
|
|
65
80
|
|
|
66
81
|
val transform = Helpers.toMapAny(Struct.getprop(ctx.point, "transform"))
|
|
67
|
-
?: return ctx.reqdata
|
|
82
|
+
?: return stripAction(ctx.reqdata)
|
|
68
83
|
|
|
69
84
|
val reqform = Struct.getprop(transform, "req", null)
|
|
70
|
-
?: return ctx.reqdata
|
|
85
|
+
?: return stripAction(ctx.reqdata)
|
|
71
86
|
|
|
72
87
|
val data = linkedMapOf<String, Any?>()
|
|
73
88
|
data["reqdata"] = ctx.reqdata
|
|
74
89
|
|
|
75
|
-
return Struct.transform(data, reqform)
|
|
90
|
+
return stripAction(Struct.transform(data, reqform))
|
|
76
91
|
}
|
|
77
92
|
|
|
78
93
|
fun transformResponse(ctx: Context): Any? {
|
|
@@ -49,7 +49,14 @@ function Context.new(ctxmap, basectx)
|
|
|
49
49
|
if type(ctrl_raw.explain) == "table" then
|
|
50
50
|
self.ctrl.explain = ctrl_raw.explain
|
|
51
51
|
end
|
|
52
|
-
|
|
52
|
+
if ctrl_raw.actor ~= nil then
|
|
53
|
+
self.ctrl.actor = ctrl_raw.actor
|
|
54
|
+
end
|
|
55
|
+
if type(ctrl_raw.paging) == "table" then
|
|
56
|
+
self.ctrl.paging = ctrl_raw.paging
|
|
57
|
+
end
|
|
58
|
+
elseif basectx ~= nil and basectx.ctrl ~= nil
|
|
59
|
+
and helpers.get_ctx_prop(ctxmap, "opname") == nil then
|
|
53
60
|
self.ctrl = basectx.ctrl
|
|
54
61
|
end
|
|
55
62
|
|