@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
@@ -1,29 +1,56 @@
1
+ use std::collections::HashMap;
1
2
  use std::rc::Rc;
3
+ use std::sync::{Mutex, OnceLock};
2
4
 
3
5
  use crate::core::context::Context;
4
6
  use crate::core::error::ProjectNameError;
5
7
  use crate::core::helpers::{call_vfn, get_str, getp, getpath, ja, jo, json_thunk, setp};
6
8
  use crate::utility::voxgigstruct::Value;
7
9
 
8
- // Default live transport: ureq (blocking, minimal). Honours a `proxy`
9
- // annotation on the fetch definition (set by the proxy feature) by routing
10
- // the request through an agent configured with that proxy.
10
+ // One agent per process: ureq pools keep-alive connections per agent, so
11
+ // an agent per request reused nothing. Cloning shares the pool.
12
+ pub(crate) fn default_agent() -> ureq::Agent {
13
+ static AGENT: OnceLock<ureq::Agent> = OnceLock::new();
14
+ AGENT.get_or_init(|| ureq::AgentBuilder::new().build()).clone()
15
+ }
16
+
17
+ // Agents keyed by the `proxy` annotation (set by the proxy feature) and
18
+ // manual redirects, for requests the default agent cannot serve.
19
+ fn agent_for(fetchdef: &Value) -> ureq::Agent {
20
+ let proxy = get_str(fetchdef, "proxy").filter(|p| !p.is_empty());
21
+ let manual = get_str(fetchdef, "redirect").as_deref() == Some("manual");
22
+ if proxy.is_none() && !manual {
23
+ return default_agent();
24
+ }
25
+
26
+ static AGENTS: OnceLock<Mutex<HashMap<(Option<String>, bool), ureq::Agent>>> =
27
+ OnceLock::new();
28
+ let mut agents = AGENTS
29
+ .get_or_init(|| Mutex::new(HashMap::new()))
30
+ .lock()
31
+ .unwrap_or_else(|poisoned| poisoned.into_inner());
32
+ agents
33
+ .entry((proxy.clone(), manual))
34
+ .or_insert_with(|| {
35
+ let mut builder = ureq::AgentBuilder::new();
36
+ if let Some(p) = proxy.as_deref().and_then(|p| ureq::Proxy::new(p).ok()) {
37
+ builder = builder.proxy(p);
38
+ }
39
+ if manual {
40
+ builder = builder.redirects(0);
41
+ }
42
+ builder.build()
43
+ })
44
+ .clone()
45
+ }
46
+
47
+ // Default live transport: ureq (blocking, minimal).
11
48
  fn default_http_fetch(fullurl: &str, fetchdef: &Value) -> Result<Value, ProjectNameError> {
12
49
  let method = get_str(fetchdef, "method")
13
50
  .filter(|m| !m.is_empty())
14
51
  .unwrap_or_else(|| "GET".to_string());
15
52
 
16
- let mut builder = ureq::AgentBuilder::new();
17
- if let Some(proxy) = get_str(fetchdef, "proxy").filter(|p| !p.is_empty()) {
18
- if let Ok(p) = ureq::Proxy::new(&proxy) {
19
- builder = builder.proxy(p);
20
- }
21
- }
22
- if get_str(fetchdef, "redirect").as_deref() == Some("manual") {
23
- builder = builder.redirects(0);
24
- }
25
- let agent = builder.build();
26
-
53
+ let agent = agent_for(fetchdef);
27
54
  let mut req = agent.request(&method, fullurl);
28
55
 
29
56
  let mut has_ua = false;
@@ -37,7 +37,7 @@ pub fn prepare_query_util(ctx: &Rc<Context>) -> Value {
37
37
  .map(|(k, v)| (k.clone(), v.clone()))
38
38
  .collect();
39
39
  for (key, val) in entries {
40
- if !val.is_noval() && !val.is_null() && !contains(&key) {
40
+ if !val.is_noval() && !val.is_null() && "$action" != key && !contains(&key) {
41
41
  setp(&out, &key, val);
42
42
  }
43
43
  }
@@ -1,10 +1,36 @@
1
1
  use std::rc::Rc;
2
2
 
3
3
  use crate::core::context::Context;
4
- use crate::core::helpers::{getp, jo, to_map};
4
+ use crate::core::helpers::{getp, jo, setp, to_map};
5
5
  use crate::utility::voxgigstruct as vs;
6
6
  use crate::utility::voxgigstruct::Value;
7
7
 
8
+ // `$action` selects the point (see make_point_util); it is never an API
9
+ // field, so the body is a copy without it. The caller's map is left untouched.
10
+ fn strip_action(reqdata: Value) -> Value {
11
+ let has = match &reqdata {
12
+ Value::Map(src) => src.borrow().contains_key("$action"),
13
+ _ => false,
14
+ };
15
+ if !has {
16
+ return reqdata;
17
+ }
18
+ let body = Value::empty_map();
19
+ if let Value::Map(src) = &reqdata {
20
+ let entries: Vec<(String, Value)> = src
21
+ .borrow()
22
+ .iter()
23
+ .map(|(k, v)| (k.clone(), v.clone()))
24
+ .collect();
25
+ for (k, v) in entries {
26
+ if "$action" != k {
27
+ setp(&body, &k, v);
28
+ }
29
+ }
30
+ }
31
+ body
32
+ }
33
+
8
34
  pub fn transform_request_util(ctx: &Rc<Context>) -> Value {
9
35
  let spec = ctx.spec.borrow().clone();
10
36
  let point = ctx.point.borrow().clone();
@@ -15,17 +41,17 @@ pub fn transform_request_util(ctx: &Rc<Context>) -> Value {
15
41
 
16
42
  let transform = to_map(&getp(&point, "transform"));
17
43
  if transform.is_noval() {
18
- return ctx.reqdata.borrow().clone();
44
+ return strip_action(ctx.reqdata.borrow().clone());
19
45
  }
20
46
 
21
47
  let reqform = getp(&transform, "req");
22
48
  if reqform.is_noval() || reqform.is_null() {
23
- return ctx.reqdata.borrow().clone();
49
+ return strip_action(ctx.reqdata.borrow().clone());
24
50
  }
25
51
 
26
52
  let store = jo(vec![("reqdata", ctx.reqdata.borrow().clone())]);
27
- match vs::transform(&store, &reqform, None) {
53
+ strip_action(match vs::transform(&store, &reqform, None) {
28
54
  Ok(reqdata) => reqdata,
29
55
  Err(_) => ctx.reqdata.borrow().clone(),
30
- }
56
+ })
31
57
  }
@@ -51,7 +51,8 @@ class Context(ctxmap: JMap[String, Object], basectx: Context) {
51
51
  case c: Control => ctrl = c
52
52
  case _ =>
53
53
  }
54
- } else if (basectx != null && basectx.ctrl != null) {
54
+ } else if (basectx != null && basectx.ctrl != null
55
+ && Helpers.getCtxProp(ctxmap, "opname") == null) {
55
56
  ctrl = basectx.ctrl
56
57
  }
57
58
 
@@ -9,8 +9,8 @@ import SCALAPACKAGE.utility.struct.Struct
9
9
  // stamps page/limit (or a cursor) into the request query; on the way back
10
10
  // (PreResult) it reads the server's pagination signals — a `Link:
11
11
  // rel="next"` header, `X-Page`/`X-Next-Page`/`X-Total-Count` headers, or
12
- // `next`/`cursor`/`nextCursor`/`hasMore` fields in the body — and records
13
- // them on `ctx.result.paging`. A per-call cursor/page from ctrl takes
12
+ // `next`/`cursor`/`nextCursor`/`hasMore` fields in the body (snake_case
13
+ // forms too) — and records them on `ctx.result.paging` and `ctrl.paging`. A per-call cursor/page from ctrl takes
14
14
  // priority (used by auto-iteration). Parameter names (`pageParam`,
15
15
  // `limitParam`, `cursorParam`), the page size (`limit`) and the start page
16
16
  // (`startPage`, default 1) are configurable.
@@ -65,7 +65,10 @@ class PagingFeature extends BaseFeature("paging", "0.0.1", true) {
65
65
  if (cursor != null) {
66
66
  spec.query.put(cursorParam, cursor)
67
67
  } else if (spec.query.get(pageParam) == null) {
68
- val page = paging.get("page")
68
+ // A record written back by preResult holds the page just fetched as
69
+ // "page" and the one to fetch as "nextPage", so nextPage wins.
70
+ var page = paging.get("nextPage")
71
+ if (page == null) page = paging.get("page")
69
72
  if (page != null) {
70
73
  spec.query.put(pageParam, page)
71
74
  } else {
@@ -192,19 +195,37 @@ class PagingFeature extends BaseFeature("paging", "0.0.1", true) {
192
195
  case _ =>
193
196
  }
194
197
 
195
- // Body-level cursors.
198
+ // Body-level signals; snake_case forms are read first so camelCase wins.
196
199
  body match {
197
200
  case bm0: JMap[_, _] =>
198
201
  val bm = bm0.asInstanceOf[JMap[String, Object]]
199
202
  if (bm.get("next") != null && paging.get("next") == null) {
200
203
  paging.put("next", bm.get("next"))
201
204
  }
205
+ if (bm.get("next_cursor") != null) {
206
+ paging.put("cursor", bm.get("next_cursor"))
207
+ }
202
208
  if (bm.get("cursor") != null) {
203
209
  paging.put("cursor", bm.get("cursor"))
204
210
  }
205
211
  if (bm.get("nextCursor") != null) {
206
212
  paging.put("cursor", bm.get("nextCursor"))
207
213
  }
214
+ if (paging.get("nextPage") == null) {
215
+ var np = bm.get("nextPage")
216
+ if (np == null) np = bm.get("next_page")
217
+ np match {
218
+ case n: java.lang.Number => paging.put("nextPage", java.lang.Integer.valueOf(n.intValue()))
219
+ case s: String => paging.put("nextPage", s)
220
+ case _ =>
221
+ }
222
+ }
223
+ bm.get("has_more") match {
224
+ case b: java.lang.Boolean =>
225
+ paging.put("hasMore", b)
226
+ explicitMore = true
227
+ case _ =>
228
+ }
208
229
  bm.get("hasMore") match {
209
230
  case b: java.lang.Boolean =>
210
231
  paging.put("hasMore", b)
@@ -227,6 +248,22 @@ class PagingFeature extends BaseFeature("paging", "0.0.1", true) {
227
248
 
228
249
  result.paging = paging
229
250
  this.last = paging
251
+
252
+ if (ctx.ctrl != null) {
253
+ // The Context shares the caller's paging map by reference, so refill
254
+ // it in place; an immutable map falls back to replacing the reference.
255
+ val shared = ctx.ctrl.paging
256
+ if (shared == null) {
257
+ ctx.ctrl.paging = paging
258
+ } else {
259
+ try {
260
+ shared.clear()
261
+ shared.putAll(paging)
262
+ } catch {
263
+ case _: UnsupportedOperationException => ctx.ctrl.paging = paging
264
+ }
265
+ }
266
+ }
230
267
  }
231
268
 
232
269
  private def isList(ctx: Context): Boolean = {
@@ -138,16 +138,10 @@ class TestFeature extends BaseFeature("test", "0.0.1", true) {
138
138
  if (updateMatch.isEmpty) updateMatch = resolveMatch(ctx, new LinkedHashMap[String, Object]()).asInstanceOf[LinkedHashMap[String, Object]]
139
139
  val args = buildArgs(ctx, op, updateMatch)
140
140
  val found = Struct.select(entmap, args)
141
- var ent = Struct.getelem(found, java.lang.Integer.valueOf(0))
142
- if (ent == null && entmap != null) {
143
- val vit = entmap.values().iterator()
144
- var brk = false
145
- while (vit.hasNext && !brk) {
146
- vit.next() match { case e: JMap[_, _] => ent = e; brk = true; case _ => }
147
- }
148
- }
141
+ val ent = Struct.getelem(found, java.lang.Integer.valueOf(0))
142
+ // update miss: 404, never another record
149
143
  if (ent == null) return respond(ctx, 404, null, extra("statusText", "Not found"))
150
- ent match { case m: JMap[_, _] if ctx.reqdata != null => m.asInstanceOf[JMap[String, Object]].putAll(ctx.reqdata); case _ => }
144
+ ent match { case _: JMap[_, _] if ctx.reqdata != null => Struct.merge(Struct.jt(ent, ctx.reqdata)); case _ => }
151
145
  Struct.delprop(ent, "$KEY")
152
146
  val out = Struct.clone(ent)
153
147
  respond(ctx, 200, out, null)
@@ -281,9 +275,14 @@ class TestFeature extends BaseFeature("test", "0.0.1", true) {
281
275
  case _ =>
282
276
  }
283
277
 
284
- val paramsPath = Struct.getpath(point, java.util.List.of("args", "params"))
285
- val reqdParams = Struct.select(paramsPath, Struct.jm("reqd", java.lang.Boolean.TRUE))
286
- val reqd = Struct.transform(reqdParams, Struct.jt("`$EACH`", "", "`$KEY.name`"))
278
+ // Path AND query: a path-only read misses a query-addressed record
279
+ // (e.g. GET /result?trace_id=), which has no path param at all.
280
+ val reqdArgs = Struct.jt()
281
+ for (kind <- List("params", "query")) {
282
+ val argsPath = Struct.getpath(point, java.util.List.of("args", kind))
283
+ reqdArgs.addAll(Struct.select(argsPath, Struct.jm("reqd", java.lang.Boolean.TRUE)))
284
+ }
285
+ val reqd = Struct.transform(reqdArgs, Struct.jt("`$EACH`", "", "`$KEY.name`"))
287
286
 
288
287
  val qand = Struct.jt()
289
288
  val q = Struct.jm("`$AND`", qand)
@@ -3,12 +3,44 @@ package SCALAPACKAGE.utility
3
3
  import java.net.{InetSocketAddress, ProxySelector, URI}
4
4
  import java.net.http.{HttpClient, HttpRequest, HttpResponse}
5
5
  import java.util.{LinkedHashMap, List => JList, Map => JMap}
6
- import java.util.function.{BiFunction, Supplier}
6
+ import java.util.concurrent.ConcurrentHashMap
7
+ import java.util.function.{BiFunction, Function => JFunction, Supplier}
7
8
  import SCALAPACKAGE.core._
8
9
  import SCALAPACKAGE.utility.struct.Struct
9
10
 
10
11
  object Fetcher {
11
12
 
13
+ // Clients keyed by redirect policy and proxy. Each HttpClient owns a
14
+ // connection pool and a selector thread, so one per request reused
15
+ // nothing; one per key pools across calls.
16
+ private val clients = new ConcurrentHashMap[String, HttpClient]()
17
+
18
+ private def clientFor(fetchdef: JMap[String, Object]): HttpClient = {
19
+ // "manual" (set by the station feature's middleware under a hosts
20
+ // policy) returns a 3xx like any other response: an automatic follow
21
+ // would carry injected credentials to a host no policy approved.
22
+ val redirectPolicy =
23
+ if ("manual" == fetchdef.get("redirect")) HttpClient.Redirect.NEVER
24
+ else HttpClient.Redirect.NORMAL
25
+
26
+ val proxy = fetchdef.get("proxy") match { case p: String => p; case _ => "" }
27
+
28
+ clients.computeIfAbsent(redirectPolicy.toString + "|" + proxy,
29
+ new JFunction[String, HttpClient] {
30
+ def apply(key: String): HttpClient = {
31
+ val clientb = HttpClient.newBuilder().followRedirects(redirectPolicy)
32
+ if (proxy != "") {
33
+ try {
34
+ val proxyUri = URI.create(proxy)
35
+ val port = if (proxyUri.getPort < 0) 80 else proxyUri.getPort
36
+ clientb.proxy(ProxySelector.of(new InetSocketAddress(proxyUri.getHost, port)))
37
+ } catch { case _: RuntimeException => }
38
+ }
39
+ clientb.build()
40
+ }
41
+ })
42
+ }
43
+
12
44
  def defaultHttpFetch(fullurl: String, fetchdef: JMap[String, Object]): JMap[String, Object] = {
13
45
  var method = fetchdef.get("method") match { case s: String => s; case _ => "" }
14
46
  if ("" == method) method = "GET"
@@ -40,19 +72,8 @@ object Fetcher {
40
72
  }
41
73
  if (!hasUA) reqb.setHeader("User-Agent", "Mozilla/5.0 (compatible; ProjectNameSDK/1.0)")
42
74
 
43
- val clientb = HttpClient.newBuilder().followRedirects(HttpClient.Redirect.NORMAL)
44
- fetchdef.get("proxy") match {
45
- case proxy: String if proxy != "" =>
46
- try {
47
- val proxyUri = URI.create(proxy)
48
- val port = if (proxyUri.getPort < 0) 80 else proxyUri.getPort
49
- clientb.proxy(ProxySelector.of(new InetSocketAddress(proxyUri.getHost, port)))
50
- } catch { case _: RuntimeException => }
51
- case _ =>
52
- }
53
-
54
75
  val resp =
55
- try clientb.build().send(reqb.build(), HttpResponse.BodyHandlers.ofString())
76
+ try clientFor(fetchdef).send(reqb.build(), HttpResponse.BodyHandlers.ofString())
56
77
  catch { case e: Exception => throw new RuntimeException("fetch: " + e.getMessage, e) }
57
78
 
58
79
  val headers = new LinkedHashMap[String, Object]()
@@ -79,7 +79,7 @@ object PrepareQuery {
79
79
  val item = it.next()
80
80
  val key = item.get(0) match { case s: String => s; case _ => "" }
81
81
  val v = item.get(1)
82
- if (v != null && !containsStr(params, key)) out.put(key, v)
82
+ if (v != null && "$action" != key && !containsStr(params, key)) out.put(key, v)
83
83
  }
84
84
  out
85
85
  }
@@ -9,15 +9,31 @@ object TransformRequest {
9
9
  if (ctx.spec != null) ctx.spec.step = "reqform"
10
10
 
11
11
  val transform = Helpers.toMapAny(Struct.getprop(ctx.point, "transform"))
12
- if (transform == null) return ctx.reqdata
12
+ if (transform == null) return stripAction(ctx.reqdata)
13
13
 
14
14
  val reqform = Struct.getprop(transform, "req", null)
15
- if (reqform == null) return ctx.reqdata
15
+ if (reqform == null) return stripAction(ctx.reqdata)
16
16
 
17
17
  val data = new LinkedHashMap[String, Object]()
18
18
  data.put("reqdata", ctx.reqdata)
19
19
 
20
- Struct.transform(data, reqform)
20
+ stripAction(Struct.transform(data, reqform))
21
+ }
22
+
23
+ // `$action` selects the point (see MakePoint); it is never an API field, so
24
+ // the body is a copy without it. The caller's map is left untouched.
25
+ private def stripAction(reqdata: Object): Object = {
26
+ reqdata match {
27
+ case src: JMap[_, _] if src.containsKey("$action") =>
28
+ val body = new LinkedHashMap[String, Object]()
29
+ val it = src.asInstanceOf[JMap[String, Object]].entrySet().iterator()
30
+ while (it.hasNext) {
31
+ val e = it.next()
32
+ if ("$action" != e.getKey) body.put(e.getKey, e.getValue)
33
+ }
34
+ body
35
+ case _ => reqdata
36
+ }
21
37
  }
22
38
  }
23
39
 
@@ -56,7 +56,7 @@ public final class Context {
56
56
  if let e = cm.entries["explain"]?.asMap { ctrl.explain = e }
57
57
  if let a = cm.entries["actor"]?.asString { ctrl.actor = a }
58
58
  if let p = cm.entries["paging"]?.asMap { ctrl.paging = p }
59
- } else if let b = basectx {
59
+ } else if let b = basectx, ctxProp(ctxmap, "opname") == nil {
60
60
  ctrl = b.ctrl
61
61
  }
62
62
 
@@ -2,8 +2,8 @@
2
2
  // stamps page/limit (or a cursor) into the request query; on the way back
3
3
  // (PreResult) it reads the server's pagination signals - a `Link:
4
4
  // rel="next"` header, `X-Page`/`X-Next-Page`/`X-Total-Count` headers, or
5
- // `next`/`cursor`/`nextCursor`/`hasMore` fields in the body - and records
6
- // them on `ctx.result.paging`. A per-call cursor/page from ctrl takes
5
+ // `next`/`cursor`/`nextCursor`/`hasMore` fields in the body (snake_case
6
+ // forms too) - and records them on `ctx.result.paging` and `ctrl.paging`. A per-call cursor/page from ctrl takes
7
7
  // priority (used by auto-iteration). Parameter names (`pageParam`,
8
8
  // `limitParam`, `cursorParam`), the page size (`limit`) and the start page
9
9
  // (`startPage`, default 1) are configurable.
@@ -68,8 +68,11 @@ public final class PagingFeature: BaseFeature {
68
68
  } else if isNil(gp(spec.query, pageParam)) {
69
69
  var page: Value? = nil
70
70
  if let paging = paging {
71
+ // A record written back by preResult holds the page just fetched as
72
+ // "page" and the one to fetch as "nextPage", so nextPage wins.
73
+ let np = gp(paging, "nextPage")
71
74
  let p = gp(paging, "page")
72
- if !isNil(p) { page = p }
75
+ if !isNil(np) { page = np } else if !isNil(p) { page = p }
73
76
  }
74
77
  spec.query.entries[pageParam] = page ?? .int(Int64(foptInt(options, "startPage", 1)))
75
78
  }
@@ -177,12 +180,16 @@ public final class PagingFeature: BaseFeature {
177
180
  }
178
181
  }
179
182
 
180
- // Body-level cursors.
183
+ // Body-level signals; snake_case forms are read first so camelCase wins.
181
184
  if let bm = body.asMap {
182
185
  let next = gp(bm, "next")
183
186
  if !isNil(next) && isNil(gp(paging, "next")) {
184
187
  paging.entries["next"] = next
185
188
  }
189
+ let snakeCursor = gp(bm, "next_cursor")
190
+ if !isNil(snakeCursor) {
191
+ paging.entries["cursor"] = snakeCursor
192
+ }
186
193
  let cursor = gp(bm, "cursor")
187
194
  if !isNil(cursor) {
188
195
  paging.entries["cursor"] = cursor
@@ -191,6 +198,18 @@ public final class PagingFeature: BaseFeature {
191
198
  if !isNil(nextCursor) {
192
199
  paging.entries["cursor"] = nextCursor
193
200
  }
201
+ if isNil(gp(paging, "nextPage")) {
202
+ var np = gp(bm, "nextPage")
203
+ if isNil(np) { np = gp(bm, "next_page") }
204
+ switch np {
205
+ case .int, .double, .string: paging.entries["nextPage"] = np
206
+ default: break
207
+ }
208
+ }
209
+ if let smb = gp(bm, "has_more").asBool {
210
+ paging.entries["hasMore"] = .bool(smb)
211
+ explicitMore = true
212
+ }
194
213
  if let hmb = gp(bm, "hasMore").asBool {
195
214
  paging.entries["hasMore"] = .bool(hmb)
196
215
  explicitMore = true
@@ -210,6 +229,14 @@ public final class PagingFeature: BaseFeature {
210
229
 
211
230
  result.paging = paging
212
231
  last = paging
232
+
233
+ // VMap is a class, so the caller's map is refilled in place; a fresh map
234
+ // on ctrl never reaches the caller.
235
+ if let shared = ctx.ctrl.paging {
236
+ shared.entries = paging.entries
237
+ } else {
238
+ ctx.ctrl.paging = paging
239
+ }
213
240
  }
214
241
 
215
242
  private func isList(_ ctx: Context) -> Bool {
@@ -93,9 +93,14 @@ private func testBuildArgs(_ ctx: Context, _ op: Operation, _ args: VMap?) -> Va
93
93
  }
94
94
  }
95
95
 
96
- let paramsPath = getpath(point, jtp("args", "params"))
97
- let reqdParams = select(paramsPath, .map(vm(("reqd", .bool(true)))))
98
- let reqd = transform(reqdParams, jtp("`$EACH`", "", "`$KEY.name`"))
96
+ // Path AND query: a path-only read misses a query-addressed record
97
+ // (e.g. GET /result?trace_id=), which has no path param at all.
98
+ let reqdArgs = VList()
99
+ for kind in ["params", "query"] {
100
+ let argsPath = getpath(point, jtp("args", kind))
101
+ reqdArgs.items.append(contentsOf: select(argsPath, .map(vm(("reqd", .bool(true))))).asList?.items ?? [])
102
+ }
103
+ let reqd = transform(.list(reqdArgs), jtp("`$EACH`", "", "`$KEY.name`"))
99
104
 
100
105
  let qand = VList()
101
106
  let q = VMap()
@@ -206,17 +211,15 @@ public final class TestFeature: BaseFeature {
206
211
  }
207
212
  let args = testBuildArgs(ctx2, op, updateMatch)
208
213
  let found = select(.map(entmap), args)
209
- var ent = getelem(found, .int(0))
210
- if isNil(ent) {
211
- for (_, e) in entmap.entries where e.isMap { ent = e; break }
212
- }
214
+ let ent = getelem(found, .int(0))
213
215
  if isNil(ent) {
216
+ // update miss: 404, never another record
214
217
  let extra = VMap()
215
218
  extra.entries["statusText"] = .string("Not found")
216
219
  return testRespond(ctx2, 404, .noval, extra)
217
220
  }
218
- if let entm = ent.asMap {
219
- for (k, v) in ctx2.reqdata.entries { entm.entries[k] = v }
221
+ if ent.isMap {
222
+ _ = merge(.list(VList([ent, .map(ctx2.reqdata)])))
220
223
  }
221
224
  delprop(ent, .string("$KEY"))
222
225
  return testRespond(ctx2, 200, clone(ent), nil)
@@ -6,7 +6,21 @@ import Foundation
6
6
  import FoundationNetworking
7
7
  #endif
8
8
 
9
- // Non-following twin of URLSession.shared. The station feature's
9
+ // Cookies OFF on both sessions: the process-wide session carries the
10
+ // process-wide cookie storage, so one call's Set-Cookie would come back on
11
+ // the next, under a different credential. Pooling is per configuration and
12
+ // is unaffected.
13
+ private func cookielessConfiguration() -> URLSessionConfiguration {
14
+ let cfg = URLSessionConfiguration.default
15
+ cfg.httpCookieStorage = nil
16
+ cfg.httpShouldSetCookies = false
17
+ cfg.httpCookieAcceptPolicy = .never
18
+ return cfg
19
+ }
20
+
21
+ private let defaultSession = URLSession(configuration: cookielessConfiguration())
22
+
23
+ // Non-following twin of defaultSession. The station feature's
10
24
  // middleware sets `redirect: manual` on the fetch definition under a
11
25
  // hosts egress policy: a 3xx must come back as a response like any
12
26
  // other, because an automatic follow would carry injected credentials
@@ -25,7 +39,8 @@ private final class ManualRedirectDelegate: NSObject, URLSessionTaskDelegate {
25
39
  }
26
40
 
27
41
  private let manualRedirectSession = URLSession(
28
- configuration: .default, delegate: ManualRedirectDelegate(), delegateQueue: nil)
42
+ configuration: cookielessConfiguration(),
43
+ delegate: ManualRedirectDelegate(), delegateQueue: nil)
29
44
 
30
45
  func defaultHttpFetch(_ fullurl: String, _ fetchdef: VMap) throws -> Value {
31
46
  guard let url = URL(string: fullurl) else {
@@ -59,6 +74,10 @@ func defaultHttpFetch(_ fullurl: String, _ fetchdef: VMap) throws -> Value {
59
74
  req.setValue("Mozilla/5.0 (compatible; ProjectNameSDK/1.0)", forHTTPHeaderField: "User-Agent")
60
75
  }
61
76
 
77
+ // Also per request, so no store reached another way can rewrite the
78
+ // Cookie header set above — an `apiKey in: cookie` scheme sets one.
79
+ req.httpShouldHandleCookies = false
80
+
62
81
  final class FetchBox: @unchecked Sendable {
63
82
  var data: Data?
64
83
  var resp: URLResponse?
@@ -67,7 +86,7 @@ func defaultHttpFetch(_ fullurl: String, _ fetchdef: VMap) throws -> Value {
67
86
  let box = FetchBox()
68
87
  let sem = DispatchSemaphore(value: 0)
69
88
  let session = gp(fetchdef, "redirect").asString == "manual"
70
- ? manualRedirectSession : URLSession.shared
89
+ ? manualRedirectSession : defaultSession
71
90
  let task = session.dataTask(with: req) { d, r, e in
72
91
  box.data = d; box.resp = r; box.err = e; sem.signal()
73
92
  }
@@ -71,7 +71,7 @@ func prepareQueryUtil(_ ctx: Context) -> VMap {
71
71
  for item in items(.map(reqmatch)) {
72
72
  let key = item[0].asString ?? ""
73
73
  let val = item[1]
74
- if !isNil(val) && !containsStr(paramnames, key) {
74
+ if !isNil(val) && "$action" != key && !containsStr(paramnames, key) {
75
75
  query.entries[key] = val
76
76
  }
77
77
  }
@@ -54,14 +54,25 @@ func resultHeadersUtil(_ ctx: Context) -> Result {
54
54
  return result!
55
55
  }
56
56
 
57
+ // `$action` selects the point (see makePointUtil); it is never an API field,
58
+ // so the body is a copy without it. The caller's map is left untouched.
59
+ private func stripAction(_ reqdata: Value) -> Value {
60
+ guard let src = reqdata.asMap, src.entries["$action"] != nil else { return reqdata }
61
+ let body = VMap()
62
+ for (key, val) in src.entries where "$action" != key {
63
+ body.entries[key] = val
64
+ }
65
+ return .map(body)
66
+ }
67
+
57
68
  func transformRequestUtil(_ ctx: Context) -> Value {
58
69
  if let sp = ctx.spec { sp.step = "reqform" }
59
70
 
60
- guard let tfm = gp(ctx.point, "transform").asMap else { return .map(ctx.reqdata) }
71
+ guard let tfm = gp(ctx.point, "transform").asMap else { return stripAction(.map(ctx.reqdata)) }
61
72
  let reqform = gp(tfm, "req")
62
- if isNil(reqform) { return .map(ctx.reqdata) }
73
+ if isNil(reqform) { return stripAction(.map(ctx.reqdata)) }
63
74
 
64
- return transform(.map(vm(("reqdata", .map(ctx.reqdata)))), reqform)
75
+ return stripAction(transform(.map(vm(("reqdata", .map(ctx.reqdata)))), reqform))
65
76
  }
66
77
 
67
78
  func transformResponseUtil(_ ctx: Context) -> Value {
@@ -58,7 +58,13 @@ class Context {
58
58
  this.client = getprop(ctxmap, 'client', getprop(basectx, 'client'))
59
59
  this.utility = getprop(ctxmap, 'utility', getprop(basectx, 'utility'))
60
60
 
61
- this.ctrl = getprop(ctxmap, 'ctrl', getprop(basectx, 'ctrl', this.ctrl))
61
+ // An operation gets its OWN control unless the caller passed one: a
62
+ // paging cursor left on the client's control would otherwise be picked
63
+ // up by the next, unrelated call. A context with no opname is not an
64
+ // operation, so it still shares (an entity's, with the client's).
65
+ const opname = getprop(ctxmap, 'opname')
66
+ const basectrl = null == opname ? getprop(basectx, 'ctrl', this.ctrl) : this.ctrl
67
+ this.ctrl = getprop(ctxmap, 'ctrl', basectrl)
62
68
  this.meta = getprop(ctxmap, 'meta', getprop(basectx, 'meta', this.meta))
63
69
 
64
70
  this.config = getprop(ctxmap, 'config', getprop(basectx, 'config'))
@@ -79,7 +85,6 @@ class Context {
79
85
  this.result = getprop(ctxmap, 'result', getprop(basectx, 'result'))
80
86
  this.response = getprop(ctxmap, 'response', getprop(basectx, 'response'))
81
87
 
82
- const opname = getprop(ctxmap, 'opname')
83
88
  this.op = this.resolveOp(opname)
84
89
  }
85
90