@voxgig/sdkgen 4.23.0 → 4.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (138) hide show
  1. package/bin/voxgig-sdkgen +18 -2
  2. package/dist/action/doctor.d.ts +1 -1
  3. package/dist/action/doctor.js +12 -10
  4. package/dist/action/doctor.js.map +1 -1
  5. package/dist/action/edition.js +4 -0
  6. package/dist/action/edition.js.map +1 -1
  7. package/dist/action/feature.js +5 -1
  8. package/dist/action/feature.js.map +1 -1
  9. package/dist/action/remove.d.ts +16 -0
  10. package/dist/action/remove.js +345 -0
  11. package/dist/action/remove.js.map +1 -0
  12. package/dist/action/target.js +6 -1
  13. package/dist/action/target.js.map +1 -1
  14. package/dist/tsconfig.tsbuildinfo +1 -1
  15. package/package.json +1 -1
  16. package/project/.sdk/tm/c/core/context.c +3 -1
  17. package/project/.sdk/tm/c/feature/paging.c +37 -3
  18. package/project/.sdk/tm/c/feature/test.c +51 -27
  19. package/project/.sdk/tm/c/utility/prepare_query.c +2 -1
  20. package/project/.sdk/tm/c/utility/transform_request.c +25 -3
  21. package/project/.sdk/tm/clojure/src/sdk/core.clj +21 -7
  22. package/project/.sdk/tm/clojure/src/sdk/features.clj +68 -18
  23. package/project/.sdk/tm/cpp/core/types.hpp +1 -1
  24. package/project/.sdk/tm/cpp/feature/paging.hpp +36 -3
  25. package/project/.sdk/tm/cpp/feature/test.hpp +41 -13
  26. package/project/.sdk/tm/cpp/utility/pipeline.hpp +16 -4
  27. package/project/.sdk/tm/csharp/core/Context.cs +1 -1
  28. package/project/.sdk/tm/csharp/feature/PagingFeature.cs +58 -4
  29. package/project/.sdk/tm/csharp/feature/TestFeature.cs +13 -21
  30. package/project/.sdk/tm/csharp/utility/Fetcher.cs +19 -12
  31. package/project/.sdk/tm/csharp/utility/PrepareQuery.cs +1 -1
  32. package/project/.sdk/tm/csharp/utility/TransformRequest.cs +23 -3
  33. package/project/.sdk/tm/elixir/lib/projectname/context.ex +1 -1
  34. package/project/.sdk/tm/elixir/lib/projectname/feature/paging.ex +52 -10
  35. package/project/.sdk/tm/elixir/lib/projectname/feature/test.ex +47 -17
  36. package/project/.sdk/tm/elixir/lib/projectname/utility.ex +30 -9
  37. package/project/.sdk/tm/go/core/context.go +13 -13
  38. package/project/.sdk/tm/go/feature/paging_feature.go +40 -2
  39. package/project/.sdk/tm/go/feature/test_feature.go +3 -15
  40. package/project/.sdk/tm/go/test/feature_corpus_test.go +1 -0
  41. package/project/.sdk/tm/go/test/feature_test.go +54 -0
  42. package/project/.sdk/tm/go/utility/fetcher.go +41 -20
  43. package/project/.sdk/tm/go/utility/prepare_query.go +1 -1
  44. package/project/.sdk/tm/go/utility/transform_request.go +22 -3
  45. package/project/.sdk/tm/java/core/Context.java +2 -1
  46. package/project/.sdk/tm/java/feature/PagingFeature.java +46 -4
  47. package/project/.sdk/tm/java/feature/TestFeature.java +10 -13
  48. package/project/.sdk/tm/java/test/FeatureCorpusTest.java +3 -0
  49. package/project/.sdk/tm/java/test/FeatureTest.java +42 -0
  50. package/project/.sdk/tm/java/utility/Fetcher.java +36 -28
  51. package/project/.sdk/tm/java/utility/PrepareQuery.java +1 -1
  52. package/project/.sdk/tm/java/utility/TransformRequest.java +18 -3
  53. package/project/.sdk/tm/js/src/Context.js +7 -2
  54. package/project/.sdk/tm/js/src/feature/paging/PagingFeature.js +24 -6
  55. package/project/.sdk/tm/js/src/feature/test/TestFeature.js +7 -5
  56. package/project/.sdk/tm/js/src/utility/PrepareQueryUtility.js +1 -1
  57. package/project/.sdk/tm/js/test/feature/Corpus.test.js +4 -0
  58. package/project/.sdk/tm/kotlin/core/Context.kt +1 -1
  59. package/project/.sdk/tm/kotlin/feature/PagingFeature.kt +36 -4
  60. package/project/.sdk/tm/kotlin/feature/TestFeature.kt +11 -14
  61. package/project/.sdk/tm/kotlin/utility/Fetcher.kt +33 -24
  62. package/project/.sdk/tm/kotlin/utility/Prepare.kt +1 -1
  63. package/project/.sdk/tm/kotlin/utility/ResultTransform.kt +18 -3
  64. package/project/.sdk/tm/lua/core/context.lua +8 -1
  65. package/project/.sdk/tm/lua/core/control.lua +3 -0
  66. package/project/.sdk/tm/lua/feature/paging_feature.lua +44 -5
  67. package/project/.sdk/tm/lua/feature/test_feature.lua +32 -14
  68. package/project/.sdk/tm/lua/utility/prepare_query.lua +2 -1
  69. package/project/.sdk/tm/lua/utility/transform_request.lua +19 -3
  70. package/project/.sdk/tm/ocaml/sdk_features.ml +72 -21
  71. package/project/.sdk/tm/ocaml/sdk_runtime.ml +23 -8
  72. package/project/.sdk/tm/perl/core/context.pm +2 -1
  73. package/project/.sdk/tm/perl/feature/paging_feature.pm +31 -5
  74. package/project/.sdk/tm/perl/feature/test_feature.pm +46 -16
  75. package/project/.sdk/tm/perl/feature/timeout_feature.pm +17 -13
  76. package/project/.sdk/tm/perl/t/feature_corpus.t +14 -1
  77. package/project/.sdk/tm/perl/utility/fetcher.pm +15 -1
  78. package/project/.sdk/tm/perl/utility/prepare_query.pm +1 -0
  79. package/project/.sdk/tm/perl/utility/transform_request.pm +13 -3
  80. package/project/.sdk/tm/php/core/Context.php +5 -1
  81. package/project/.sdk/tm/php/core/Control.php +5 -0
  82. package/project/.sdk/tm/php/feature/PagingFeature.php +26 -5
  83. package/project/.sdk/tm/php/feature/TestFeature.php +27 -42
  84. package/project/.sdk/tm/php/test/FeatureCorpusTest.php +17 -1
  85. package/project/.sdk/tm/php/test/FeatureTest.php +44 -0
  86. package/project/.sdk/tm/php/utility/Fetcher.php +17 -3
  87. package/project/.sdk/tm/php/utility/PrepareQuery.php +1 -1
  88. package/project/.sdk/tm/php/utility/TransformRequest.php +15 -3
  89. package/project/.sdk/tm/py/pkg/core/context.py +2 -1
  90. package/project/.sdk/tm/py/pkg/feature/paging_feature.py +29 -4
  91. package/project/.sdk/tm/py/pkg/feature/test_feature.py +29 -12
  92. package/project/.sdk/tm/py/pkg/utility/fetcher.py +63 -65
  93. package/project/.sdk/tm/py/pkg/utility/prepare_query.py +2 -1
  94. package/project/.sdk/tm/py/pkg/utility/transform_request.py +11 -3
  95. package/project/.sdk/tm/py/test/test_feature.py +27 -0
  96. package/project/.sdk/tm/py/test/test_feature_corpus.py +11 -1
  97. package/project/.sdk/tm/rb/core/context.rb +1 -1
  98. package/project/.sdk/tm/rb/feature/paging_feature.rb +27 -5
  99. package/project/.sdk/tm/rb/feature/test_feature.rb +33 -7
  100. package/project/.sdk/tm/rb/test/feature_corpus_test.rb +12 -1
  101. package/project/.sdk/tm/rb/test/feature_test.rb +38 -0
  102. package/project/.sdk/tm/rb/utility/fetcher.rb +49 -3
  103. package/project/.sdk/tm/rb/utility/prepare_query.rb +1 -1
  104. package/project/.sdk/tm/rb/utility/transform_request.rb +10 -3
  105. package/project/.sdk/tm/rust/core/context.rs +2 -6
  106. package/project/.sdk/tm/rust/feature/paging.rs +44 -3
  107. package/project/.sdk/tm/rust/feature/secrets.rs +1 -1
  108. package/project/.sdk/tm/rust/feature/test.rs +66 -33
  109. package/project/.sdk/tm/rust/utility/fetcher.rs +41 -14
  110. package/project/.sdk/tm/rust/utility/prepare_query.rs +1 -1
  111. package/project/.sdk/tm/rust/utility/transform_request.rs +31 -5
  112. package/project/.sdk/tm/scala/core/Context.scala +2 -1
  113. package/project/.sdk/tm/scala/feature/PagingFeature.scala +41 -4
  114. package/project/.sdk/tm/scala/feature/TestFeature.scala +11 -12
  115. package/project/.sdk/tm/scala/utility/Fetcher.scala +34 -13
  116. package/project/.sdk/tm/scala/utility/Prepare.scala +1 -1
  117. package/project/.sdk/tm/scala/utility/Transform.scala +19 -3
  118. package/project/.sdk/tm/swift/Sources/ProjectNameSDK/core/Context.swift +1 -1
  119. package/project/.sdk/tm/swift/Sources/ProjectNameSDK/feature/PagingFeature.swift +31 -4
  120. package/project/.sdk/tm/swift/Sources/ProjectNameSDK/feature/TestFeature.swift +12 -9
  121. package/project/.sdk/tm/swift/Sources/ProjectNameSDK/utility/Fetcher.swift +22 -3
  122. package/project/.sdk/tm/swift/Sources/ProjectNameSDK/utility/Prepare.swift +1 -1
  123. package/project/.sdk/tm/swift/Sources/ProjectNameSDK/utility/ResultUtil.swift +14 -3
  124. package/project/.sdk/tm/ts/src/Context.ts +7 -2
  125. package/project/.sdk/tm/ts/src/feature/paging/PagingFeature.ts +19 -3
  126. package/project/.sdk/tm/ts/src/feature/test/TestFeature.ts +3 -0
  127. package/project/.sdk/tm/ts/src/utility/PrepareQueryUtility.ts +1 -1
  128. package/project/.sdk/tm/ts/test/feature/Corpus.test.ts +1 -0
  129. package/project/.sdk/tm/zig/core/context.zig +1 -1
  130. package/project/.sdk/tm/zig/core/utility.zig +19 -5
  131. package/project/.sdk/tm/zig/feature/paging.zig +34 -4
  132. package/project/.sdk/tm/zig/feature/test.zig +54 -26
  133. package/project/sdkgen-package.json +1 -1
  134. package/src/action/doctor.ts +12 -9
  135. package/src/action/edition.ts +8 -0
  136. package/src/action/feature.ts +7 -1
  137. package/src/action/remove.ts +492 -0
  138. package/src/action/target.ts +8 -1
@@ -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
- if (null == ent) {
165
- return respond(404, undefined, { statusText: S_NOT_FOUND })
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
@@ -13,7 +13,7 @@ function prepareQuery(ctx) {
13
13
 
14
14
  const out = {}
15
15
  for (let [key, val] of items(reqmatch)) {
16
- if (null != val && !params.includes(key)) {
16
+ if (null != val && '$action' !== key && !params.includes(key)) {
17
17
  out[key] = val
18
18
  }
19
19
  }
@@ -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
 
@@ -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 records them on
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
- val page = paging["page"]
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 cursors.
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
- var ent = Struct.getelem(found, 0, null)
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 as MutableMap<String, Any?>).putAll(reqdata)
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
- // Get required params.
324
- val paramsPath = Struct.getpath(point, listOf("args", "params"))
325
- val reqdParams = Struct.select(paramsPath, Struct.jm("reqd", true))
326
- val reqd = Struct.transform(reqdParams, Struct.jt("`\$EACH`", "", "`\$KEY.name`"))
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 = clientb.build().send(reqb.build(), HttpResponse.BodyHandlers.ofString())
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
- elseif basectx ~= nil and basectx.ctrl ~= nil then
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
 
@@ -10,6 +10,9 @@ function Control.new(opts)
10
10
  self.throw_err = opts.throw_err
11
11
  self.err = nil
12
12
  self.explain = opts.explain
13
+ -- Per-call feature inputs (audit actor, paging cursor/page).
14
+ self.actor = opts.actor
15
+ self.paging = opts.paging
13
16
  return self
14
17
  end
15
18
 
@@ -5,8 +5,9 @@
5
5
  -- (PreResult) it reads the server's pagination signals — a
6
6
  -- `Link: <...>; rel="next"` header, `X-Page`/`X-Next-Page`/
7
7
  -- `X-Total-Count` headers, or `next`/`cursor`/`nextCursor`/`hasMore`
8
- -- fields in the body — and records them on `ctx.result.paging`. A
9
- -- per-call `ctrl.paging` (cursor or page) wins over the option defaults,
8
+ -- fields in the body (snake_case forms too) — and records them on
9
+ -- `ctx.result.paging` and the caller's `ctrl.paging`. A per-call
10
+ -- `ctrl.paging` (cursor or page) wins over the option defaults,
10
11
  -- so auto-iteration can advance the cursor/page and re-issue the list
11
12
  -- call until `hasMore` is false. Parameter names (`pageParam`,
12
13
  -- `limitParam`, `cursorParam`), `startPage` (default 1) and `limit` are
@@ -95,8 +96,14 @@ function PagingFeature:PreRequest(ctx)
95
96
  if paging["cursor"] ~= nil then
96
97
  spec.query[cursor_param] = paging["cursor"]
97
98
  elseif spec.query[page_param] == nil then
98
- if paging["page"] ~= nil then
99
- spec.query[page_param] = paging["page"]
99
+ -- A record written back by PreResult holds the page just fetched as
100
+ -- "page" and the one to fetch as "nextPage", so nextPage wins.
101
+ local page = paging["nextPage"]
102
+ if page == nil then
103
+ page = paging["page"]
104
+ end
105
+ if page ~= nil then
106
+ spec.query[page_param] = page
100
107
  else
101
108
  spec.query[page_param] = self.options["startPage"] or 1
102
109
  end
@@ -218,17 +225,33 @@ function PagingFeature:PreResult(ctx)
218
225
  end
219
226
  end
220
227
 
221
- -- Body-level cursors.
228
+ -- Body-level signals; snake_case forms are read first so camelCase wins.
222
229
  if type(body) == "table" then
223
230
  if body["next"] ~= nil and paging.next == nil then
224
231
  paging.next = body["next"]
225
232
  end
233
+ if body["next_cursor"] ~= nil then
234
+ paging.cursor = body["next_cursor"]
235
+ end
226
236
  if body["cursor"] ~= nil then
227
237
  paging.cursor = body["cursor"]
228
238
  end
229
239
  if body["nextCursor"] ~= nil then
230
240
  paging.cursor = body["nextCursor"]
231
241
  end
242
+ if paging.nextPage == nil then
243
+ local np = body["nextPage"]
244
+ if np == nil then
245
+ np = body["next_page"]
246
+ end
247
+ if type(np) == "number" or type(np) == "string" then
248
+ paging.nextPage = np
249
+ end
250
+ end
251
+ if type(body["has_more"]) == "boolean" then
252
+ paging.hasMore = body["has_more"]
253
+ explicit_more = true
254
+ end
232
255
  if type(body["hasMore"]) == "boolean" then
233
256
  paging.hasMore = body["hasMore"]
234
257
  explicit_more = true
@@ -247,6 +270,22 @@ function PagingFeature:PreResult(ctx)
247
270
 
248
271
  result.paging = paging
249
272
 
273
+ if ctx.ctrl ~= nil then
274
+ -- The Context shares the caller's paging table by reference, so refill
275
+ -- it in place; a fresh table on ctrl never reaches the caller.
276
+ if type(ctx.ctrl.paging) == "table" then
277
+ local shared = ctx.ctrl.paging
278
+ for k in pairs(shared) do
279
+ shared[k] = nil
280
+ end
281
+ for k, v in pairs(paging) do
282
+ shared[k] = v
283
+ end
284
+ else
285
+ ctx.ctrl.paging = paging
286
+ end
287
+ end
288
+
250
289
  local client = self.client
251
290
  client._paging = { last = paging }
252
291
  end
@@ -167,21 +167,12 @@ function TestFeature:init(ctx, options)
167
167
  local args = test_self:build_args(fctx, op, update_match)
168
168
  local found = vs.select(entmap, args)
169
169
  local ent = vs.getelem(found, 0)
170
- if ent == nil and type(entmap) == "table" then
171
- for _, e in pairs(entmap) do
172
- if type(e) == "table" then ent = e; break end
173
- end
174
- end
175
170
  if ent == nil then
171
+ -- update miss: 404, never another record
176
172
  return respond(404, nil, { statusText = "Not found" })
177
173
  end
178
- if type(ent) == "table" then
179
- local reqdata = fctx.reqdata
180
- if reqdata ~= nil then
181
- for k, v in pairs(reqdata) do
182
- ent[k] = v
183
- end
184
- end
174
+ if type(ent) == "table" and type(fctx.reqdata) == "table" then
175
+ vs.merge({ ent, fctx.reqdata })
185
176
  end
186
177
  vs.delprop(ent, "$KEY")
187
178
  local out = vs.clone(ent)
@@ -312,12 +303,39 @@ function TestFeature:make_netsim(net, inner)
312
303
  end
313
304
 
314
305
 
306
+ local function point_terminal(p)
307
+ local parts = vs.getprop(p, "parts")
308
+ if type(parts) ~= "table" or #parts == 0 then return false end
309
+ local last = parts[#parts]
310
+ return type(last) == "string" and last:sub(1, 1) == "{"
311
+ end
312
+
313
+ local function point_depth(p)
314
+ local parts = vs.getprop(p, "parts")
315
+ return type(parts) == "table" and #parts or 0
316
+ end
317
+
318
+ -- The entity's own endpoint: a terminal `{param}` marks a record route, and
319
+ -- among equals the shallower path wins (the same rule as make_point).
320
+ local function pick_point(points)
321
+ if type(points) ~= "table" or #points == 0 then return nil end
322
+ local point = points[1]
323
+ for i = 2, #points do
324
+ local cand = points[i]
325
+ if point_terminal(cand) ~= point_terminal(point) then
326
+ if point_terminal(cand) then point = cand end
327
+ elseif point_depth(cand) < point_depth(point) then
328
+ point = cand
329
+ end
330
+ end
331
+ return point
332
+ end
333
+
315
334
  function TestFeature:build_args(ctx, op, args)
316
335
  local opname = op.name
317
336
 
318
- -- Get last point from config.
319
337
  local points = vs.getpath(ctx.config, "entity." .. ctx.entity:get_name() .. ".op." .. opname .. ".points")
320
- local point = vs.getelem(points, -1)
338
+ local point = pick_point(points)
321
339
 
322
340
  -- Path AND query: a path-only read misses a query-addressed record
323
341
  -- (e.g. GET /result?trace_id=), which has no path param at all.
@@ -29,7 +29,8 @@ local function prepare_query_util(ctx)
29
29
  for _, item in ipairs(reqmatch_items) do
30
30
  local key = item[1]
31
31
  local val = item[2]
32
- if val ~= nil and type(key) == "string" and not contains_param(params, key) then
32
+ if val ~= nil and type(key) == "string" and key ~= "$action"
33
+ and not contains_param(params, key) then
33
34
  out[key] = val
34
35
  end
35
36
  end
@@ -3,6 +3,22 @@
3
3
  local vs = require("utility.struct.struct")
4
4
  local helpers = require("core.helpers")
5
5
 
6
+ -- `$action` selects the point (see make_point_util); it is never an API
7
+ -- field, so the body is a copy without it. The caller's table is left
8
+ -- untouched.
9
+ local function strip_action(reqdata)
10
+ if not vs.ismap(reqdata) or reqdata["$action"] == nil then
11
+ return reqdata
12
+ end
13
+ local body = {}
14
+ for k, v in pairs(reqdata) do
15
+ if k ~= "$action" then
16
+ body[k] = v
17
+ end
18
+ end
19
+ return body
20
+ end
21
+
6
22
  local function transform_request_util(ctx)
7
23
  local spec = ctx.spec
8
24
  local point = ctx.point
@@ -13,19 +29,19 @@ local function transform_request_util(ctx)
13
29
 
14
30
  local transform = helpers.to_map(vs.getprop(point, "transform"))
15
31
  if transform == nil then
16
- return ctx.reqdata
32
+ return strip_action(ctx.reqdata)
17
33
  end
18
34
 
19
35
  local reqform = vs.getprop(transform, "req")
20
36
  if reqform == nil then
21
- return ctx.reqdata
37
+ return strip_action(ctx.reqdata)
22
38
  end
23
39
 
24
40
  local reqdata = vs.transform({
25
41
  reqdata = ctx.reqdata,
26
42
  }, reqform)
27
43
 
28
- return reqdata
44
+ return strip_action(reqdata)
29
45
  end
30
46
 
31
47
  return transform_request_util