@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.
Files changed (163) 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/cmp/Deploy.js +9 -6
  15. package/dist/cmp/Deploy.js.map +1 -1
  16. package/dist/helpers/semver.js +3 -1
  17. package/dist/helpers/semver.js.map +1 -1
  18. package/dist/tsconfig.tsbuildinfo +1 -1
  19. package/package.json +2 -2
  20. package/project/.sdk/tm/c/core/context.c +3 -1
  21. package/project/.sdk/tm/c/feature/paging.c +37 -3
  22. package/project/.sdk/tm/c/feature/test.c +51 -27
  23. package/project/.sdk/tm/c/utility/prepare_query.c +2 -1
  24. package/project/.sdk/tm/c/utility/transform_request.c +25 -3
  25. package/project/.sdk/tm/clojure/src/sdk/core.clj +21 -7
  26. package/project/.sdk/tm/clojure/src/sdk/features.clj +68 -18
  27. package/project/.sdk/tm/cpp/core/types.hpp +1 -1
  28. package/project/.sdk/tm/cpp/feature/paging.hpp +36 -3
  29. package/project/.sdk/tm/cpp/feature/test.hpp +41 -13
  30. package/project/.sdk/tm/cpp/utility/pipeline.hpp +16 -4
  31. package/project/.sdk/tm/csharp/core/Context.cs +1 -1
  32. package/project/.sdk/tm/csharp/feature/PagingFeature.cs +58 -4
  33. package/project/.sdk/tm/csharp/feature/TestFeature.cs +13 -21
  34. package/project/.sdk/tm/csharp/utility/Fetcher.cs +19 -12
  35. package/project/.sdk/tm/csharp/utility/PrepareQuery.cs +1 -1
  36. package/project/.sdk/tm/csharp/utility/TransformRequest.cs +23 -3
  37. package/project/.sdk/tm/elixir/lib/projectname/context.ex +1 -1
  38. package/project/.sdk/tm/elixir/lib/projectname/feature/paging.ex +52 -10
  39. package/project/.sdk/tm/elixir/lib/projectname/feature/test.ex +47 -17
  40. package/project/.sdk/tm/elixir/lib/projectname/utility.ex +30 -9
  41. package/project/.sdk/tm/go/core/context.go +13 -13
  42. package/project/.sdk/tm/go/feature/paging_feature.go +40 -2
  43. package/project/.sdk/tm/go/feature/test_feature.go +3 -15
  44. package/project/.sdk/tm/go/test/feature_corpus_test.go +1 -0
  45. package/project/.sdk/tm/go/test/feature_test.go +54 -0
  46. package/project/.sdk/tm/go/test/pipeline_test.go +163 -57
  47. package/project/.sdk/tm/go/test/primary_utility_test.go +53 -0
  48. package/project/.sdk/tm/go/utility/fetcher.go +41 -20
  49. package/project/.sdk/tm/go/utility/prepare_query.go +1 -1
  50. package/project/.sdk/tm/go/utility/transform_request.go +22 -3
  51. package/project/.sdk/tm/java/core/Context.java +2 -1
  52. package/project/.sdk/tm/java/feature/PagingFeature.java +46 -4
  53. package/project/.sdk/tm/java/feature/TestFeature.java +10 -13
  54. package/project/.sdk/tm/java/test/FeatureCorpusTest.java +3 -0
  55. package/project/.sdk/tm/java/test/FeatureTest.java +42 -0
  56. package/project/.sdk/tm/java/test/PipelineTest.java +141 -48
  57. package/project/.sdk/tm/java/test/PrimaryUtilityTest.java +67 -6
  58. package/project/.sdk/tm/java/utility/Fetcher.java +36 -28
  59. package/project/.sdk/tm/java/utility/PrepareQuery.java +1 -1
  60. package/project/.sdk/tm/java/utility/TransformRequest.java +18 -3
  61. package/project/.sdk/tm/js/src/Context.js +7 -2
  62. package/project/.sdk/tm/js/src/feature/paging/PagingFeature.js +24 -6
  63. package/project/.sdk/tm/js/src/feature/test/TestFeature.js +7 -5
  64. package/project/.sdk/tm/js/src/utility/PrepareQueryUtility.js +1 -1
  65. package/project/.sdk/tm/js/test/feature/Corpus.test.js +4 -0
  66. package/project/.sdk/tm/js/test/pipeline.test.js +88 -22
  67. package/project/.sdk/tm/js/test/utility/PrimaryUtility.test.js +47 -1
  68. package/project/.sdk/tm/kotlin/core/Context.kt +1 -1
  69. package/project/.sdk/tm/kotlin/feature/PagingFeature.kt +36 -4
  70. package/project/.sdk/tm/kotlin/feature/TestFeature.kt +11 -14
  71. package/project/.sdk/tm/kotlin/utility/Fetcher.kt +33 -24
  72. package/project/.sdk/tm/kotlin/utility/Prepare.kt +1 -1
  73. package/project/.sdk/tm/kotlin/utility/ResultTransform.kt +18 -3
  74. package/project/.sdk/tm/lua/core/context.lua +8 -1
  75. package/project/.sdk/tm/lua/core/control.lua +3 -0
  76. package/project/.sdk/tm/lua/feature/paging_feature.lua +44 -5
  77. package/project/.sdk/tm/lua/feature/test_feature.lua +32 -14
  78. package/project/.sdk/tm/lua/test/omni.lua +8 -1
  79. package/project/.sdk/tm/lua/test/pipeline_test.lua +107 -28
  80. package/project/.sdk/tm/lua/test/primary_utility_test.lua +83 -3
  81. package/project/.sdk/tm/lua/utility/prepare_query.lua +2 -1
  82. package/project/.sdk/tm/lua/utility/transform_request.lua +19 -3
  83. package/project/.sdk/tm/ocaml/sdk_features.ml +72 -21
  84. package/project/.sdk/tm/ocaml/sdk_runtime.ml +23 -8
  85. package/project/.sdk/tm/perl/core/context.pm +2 -1
  86. package/project/.sdk/tm/perl/feature/paging_feature.pm +31 -5
  87. package/project/.sdk/tm/perl/feature/test_feature.pm +46 -16
  88. package/project/.sdk/tm/perl/feature/timeout_feature.pm +17 -13
  89. package/project/.sdk/tm/perl/t/feature_corpus.t +14 -1
  90. package/project/.sdk/tm/perl/t/omni.pm +7 -1
  91. package/project/.sdk/tm/perl/t/pipeline.t +124 -27
  92. package/project/.sdk/tm/perl/t/primary_utility.t +73 -0
  93. package/project/.sdk/tm/perl/utility/fetcher.pm +15 -1
  94. package/project/.sdk/tm/perl/utility/prepare_query.pm +1 -0
  95. package/project/.sdk/tm/perl/utility/transform_request.pm +13 -3
  96. package/project/.sdk/tm/php/core/Context.php +5 -1
  97. package/project/.sdk/tm/php/core/Control.php +5 -0
  98. package/project/.sdk/tm/php/feature/PagingFeature.php +26 -5
  99. package/project/.sdk/tm/php/feature/TestFeature.php +27 -42
  100. package/project/.sdk/tm/php/test/FeatureCorpusTest.php +17 -1
  101. package/project/.sdk/tm/php/test/FeatureTest.php +44 -0
  102. package/project/.sdk/tm/php/test/PipelineTest.php +143 -26
  103. package/project/.sdk/tm/php/test/PrimaryUtilityTest.php +80 -2
  104. package/project/.sdk/tm/php/utility/Fetcher.php +17 -3
  105. package/project/.sdk/tm/php/utility/PrepareQuery.php +1 -1
  106. package/project/.sdk/tm/php/utility/TransformRequest.php +15 -3
  107. package/project/.sdk/tm/py/pkg/core/context.py +2 -1
  108. package/project/.sdk/tm/py/pkg/feature/paging_feature.py +29 -4
  109. package/project/.sdk/tm/py/pkg/feature/test_feature.py +29 -12
  110. package/project/.sdk/tm/py/pkg/utility/fetcher.py +63 -65
  111. package/project/.sdk/tm/py/pkg/utility/prepare_query.py +2 -1
  112. package/project/.sdk/tm/py/pkg/utility/transform_request.py +11 -3
  113. package/project/.sdk/tm/py/test/test_feature.py +27 -0
  114. package/project/.sdk/tm/py/test/test_feature_corpus.py +11 -1
  115. package/project/.sdk/tm/py/test/test_pipeline.py +108 -31
  116. package/project/.sdk/tm/py/test/test_primary_utility.py +76 -1
  117. package/project/.sdk/tm/rb/core/context.rb +1 -1
  118. package/project/.sdk/tm/rb/feature/paging_feature.rb +27 -5
  119. package/project/.sdk/tm/rb/feature/test_feature.rb +33 -7
  120. package/project/.sdk/tm/rb/test/feature_corpus_test.rb +12 -1
  121. package/project/.sdk/tm/rb/test/feature_test.rb +38 -0
  122. package/project/.sdk/tm/rb/test/pipeline_test.rb +100 -24
  123. package/project/.sdk/tm/rb/test/primary_utility_test.rb +59 -1
  124. package/project/.sdk/tm/rb/utility/fetcher.rb +49 -3
  125. package/project/.sdk/tm/rb/utility/prepare_query.rb +1 -1
  126. package/project/.sdk/tm/rb/utility/transform_request.rb +10 -3
  127. package/project/.sdk/tm/rust/core/context.rs +2 -6
  128. package/project/.sdk/tm/rust/feature/paging.rs +44 -3
  129. package/project/.sdk/tm/rust/feature/secrets.rs +1 -1
  130. package/project/.sdk/tm/rust/feature/test.rs +66 -33
  131. package/project/.sdk/tm/rust/utility/fetcher.rs +41 -14
  132. package/project/.sdk/tm/rust/utility/prepare_query.rs +1 -1
  133. package/project/.sdk/tm/rust/utility/transform_request.rs +31 -5
  134. package/project/.sdk/tm/scala/core/Context.scala +2 -1
  135. package/project/.sdk/tm/scala/feature/PagingFeature.scala +41 -4
  136. package/project/.sdk/tm/scala/feature/TestFeature.scala +11 -12
  137. package/project/.sdk/tm/scala/utility/Fetcher.scala +34 -13
  138. package/project/.sdk/tm/scala/utility/Prepare.scala +1 -1
  139. package/project/.sdk/tm/scala/utility/Transform.scala +19 -3
  140. package/project/.sdk/tm/swift/Sources/ProjectNameSDK/core/Context.swift +1 -1
  141. package/project/.sdk/tm/swift/Sources/ProjectNameSDK/feature/PagingFeature.swift +31 -4
  142. package/project/.sdk/tm/swift/Sources/ProjectNameSDK/feature/TestFeature.swift +12 -9
  143. package/project/.sdk/tm/swift/Sources/ProjectNameSDK/utility/Fetcher.swift +22 -3
  144. package/project/.sdk/tm/swift/Sources/ProjectNameSDK/utility/Prepare.swift +1 -1
  145. package/project/.sdk/tm/swift/Sources/ProjectNameSDK/utility/ResultUtil.swift +14 -3
  146. package/project/.sdk/tm/ts/src/Context.ts +7 -2
  147. package/project/.sdk/tm/ts/src/feature/paging/PagingFeature.ts +19 -3
  148. package/project/.sdk/tm/ts/src/feature/test/TestFeature.ts +3 -0
  149. package/project/.sdk/tm/ts/src/utility/PrepareQueryUtility.ts +1 -1
  150. package/project/.sdk/tm/ts/test/feature/Corpus.test.ts +1 -0
  151. package/project/.sdk/tm/ts/test/pipeline.test.ts +52 -11
  152. package/project/.sdk/tm/zig/core/context.zig +1 -1
  153. package/project/.sdk/tm/zig/core/utility.zig +19 -5
  154. package/project/.sdk/tm/zig/feature/paging.zig +34 -4
  155. package/project/.sdk/tm/zig/feature/test.zig +54 -26
  156. package/project/sdkgen-package.json +1 -1
  157. package/src/action/doctor.ts +12 -9
  158. package/src/action/edition.ts +8 -0
  159. package/src/action/feature.ts +7 -1
  160. package/src/action/remove.ts +492 -0
  161. package/src/action/target.ts +8 -1
  162. package/src/cmp/Deploy.ts +11 -6
  163. package/src/helpers/semver.ts +3 -1
@@ -533,7 +533,24 @@ class PrimaryUtilityTest < Minitest::Test
533
533
  unwrap(@utility.prepare_auth.call(ctx))
534
534
  end
535
535
 
536
- runsection("prepareAuth", subject)
536
+ # The corpus writes the credential as `headers.authorization`: a
537
+ # PLACEHOLDER each runner points at the container and name this API
538
+ # actually uses.
539
+ cred = auth_credential(auth_client)
540
+ refute_nil cred, "prepare_auth placed no credential in headers or query"
541
+
542
+ # An absent section is runsection's report to make.
543
+ section = @spec.is_a?(Hash) ? @spec["prepareAuth"] : nil
544
+ swap = section.is_a?(Hash) &&
545
+ !("headers" == cred["where"] && "authorization" == cred["name"])
546
+ original = swap ? section["basic"] : nil
547
+ section["basic"] = retarget_auth(original, cred) if swap
548
+
549
+ begin
550
+ runsection("prepareAuth", subject)
551
+ ensure
552
+ section["basic"] = original if swap
553
+ end
537
554
  end
538
555
 
539
556
 
@@ -650,6 +667,47 @@ class PrimaryUtilityTest < Minitest::Test
650
667
  utility.make_context.call(ctxmap, client.get_root_ctx)
651
668
  end
652
669
 
670
+ # Where this SDK's prepare_auth actually puts the credential: the name is
671
+ # the API's OWN scheme name, not always `authorization`, so it is
672
+ # discovered by running prepare_auth once.
673
+ def auth_credential(client)
674
+ utility = client.get_utility
675
+ ctx = make_test_ctx(client, utility, nil)
676
+ ctx.spec = ProjectNameSpec.new({ "headers" => {}, "query" => {}, "step" => "s" })
677
+ _, err = utility.prepare_auth.call(ctx)
678
+ return nil unless err.nil?
679
+ ["headers", "query"].each do |where|
680
+ # A cookie credential rides the header bag, because a cookie IS a header.
681
+ bag = "query" == where ? ctx.spec.query : ctx.spec.headers
682
+ bag.each { |name, _value| return { "where" => where, "name" => name } }
683
+ end
684
+ nil
685
+ end
686
+
687
+ # Rename the corpus's `headers` bag to the real container, and the
688
+ # `authorization` key inside it to the real credential name. Applied only
689
+ # to the prepareAuth section, so real header assertions elsewhere are
690
+ # untouched.
691
+ def retarget_auth(node, cred)
692
+ return node.map { |child| retarget_auth(child, cred) } if node.is_a?(Array)
693
+ return node unless node.is_a?(Hash)
694
+
695
+ out = {}
696
+ node.each do |key, value|
697
+ if "headers" == key
698
+ bag = {}
699
+ (value.is_a?(Hash) ? value : {}).each do |bagkey, bagvalue|
700
+ name = "authorization" == bagkey ? cred["name"] : bagkey
701
+ bag[name] = retarget_auth(bagvalue, cred)
702
+ end
703
+ out[cred["where"]] = bag
704
+ else
705
+ out[key] = retarget_auth(value, cred)
706
+ end
707
+ end
708
+ out
709
+ end
710
+
653
711
  # === Helper: make_test_full_ctx ===
654
712
  def make_test_full_ctx(client, utility)
655
713
  ctx = make_test_ctx(client, utility, nil)
@@ -5,6 +5,42 @@ require 'uri'
5
5
  require 'json'
6
6
 
7
7
  module ProjectNameUtilities
8
+ # Started Net::HTTP connections keyed by scheme, host, port and proxy,
9
+ # checked out for one request and returned. A Net::HTTP serves one
10
+ # request at a time, so the pool holds idle ones rather than sharing one.
11
+ HTTP_POOL = Hash.new { |h, k| h[k] = [] }
12
+ HTTP_POOL_LOCK = Mutex.new
13
+ HTTP_POOL_IDLE_MAX = 4
14
+
15
+ def self.http_checkout(key, uri, proxy)
16
+ http = HTTP_POOL_LOCK.synchronize { HTTP_POOL[key].pop }
17
+ return http if http && http.started?
18
+
19
+ if proxy
20
+ purl = URI.parse(proxy)
21
+ http = Net::HTTP.new(uri.host, uri.port, purl.host, purl.port, purl.user, purl.password)
22
+ else
23
+ http = Net::HTTP.new(uri.host, uri.port)
24
+ end
25
+ http.use_ssl = (uri.scheme == "https")
26
+ http.start
27
+ end
28
+
29
+ def self.http_checkin(key, http)
30
+ kept = HTTP_POOL_LOCK.synchronize do
31
+ HTTP_POOL[key].size < HTTP_POOL_IDLE_MAX && HTTP_POOL[key].push(http)
32
+ end
33
+ http.finish unless kept
34
+ rescue IOError
35
+ nil
36
+ end
37
+
38
+ def self.http_discard(http)
39
+ http.finish if http.started?
40
+ rescue IOError
41
+ nil
42
+ end
43
+
8
44
  DefaultHttpFetch = ->(fullurl, fetchdef) {
9
45
  method_str = fetchdef["method"] || "GET"
10
46
  body_str = fetchdef["body"]
@@ -12,8 +48,9 @@ module ProjectNameUtilities
12
48
 
13
49
  begin
14
50
  uri = URI.parse(fullurl)
15
- http = Net::HTTP.new(uri.host, uri.port)
16
- http.use_ssl = (uri.scheme == "https")
51
+ proxy = fetchdef["proxy"]
52
+ proxy = nil unless proxy.is_a?(String) && !proxy.empty?
53
+ key = [uri.scheme, uri.host, uri.port, proxy]
17
54
 
18
55
  klass = case method_str.upcase
19
56
  when "POST" then Net::HTTP::Post
@@ -35,7 +72,16 @@ module ProjectNameUtilities
35
72
  request['User-Agent'] = 'Mozilla/5.0 (compatible; ProjectNameSDK/1.0)' unless has_ua
36
73
  request.body = body_str if body_str.is_a?(String)
37
74
 
38
- resp = http.request(request)
75
+ http = ProjectNameUtilities.http_checkout(key, uri, proxy)
76
+ begin
77
+ # Net::HTTP retries idempotent requests; an EOF can follow a committed POST.
78
+ resp = http.request(request)
79
+ rescue StandardError
80
+ ProjectNameUtilities.http_discard(http)
81
+ raise
82
+ end
83
+ ProjectNameUtilities.http_checkin(key, http)
84
+
39
85
  resp_headers = {}
40
86
  resp.each_header { |k, v| resp_headers[k.downcase] = v }
41
87
 
@@ -14,7 +14,7 @@ module ProjectNameUtilities
14
14
  if items
15
15
  items.each do |item|
16
16
  key, val = item[0], item[1]
17
- out[key] = val if val && key.is_a?(String) && !params.include?(key)
17
+ out[key] = val if val && key.is_a?(String) && key != "$action" && !params.include?(key)
18
18
  end
19
19
  end
20
20
  out
@@ -2,14 +2,21 @@
2
2
  require_relative 'struct/voxgig_struct'
3
3
  require_relative '../core/helpers'
4
4
  module ProjectNameUtilities
5
+ # `$action` selects the point (see MakePoint); it is never an API field, so
6
+ # the body is a copy without it. The caller's hash is left untouched.
7
+ def self.strip_action(reqdata)
8
+ return reqdata unless reqdata.is_a?(Hash) && reqdata.key?("$action")
9
+ reqdata.reject { |k, _| k == "$action" }
10
+ end
11
+
5
12
  TransformRequest = ->(ctx) {
6
13
  spec = ctx.spec
7
14
  point = ctx.point
8
15
  spec.step = "reqform" if spec
9
16
  transform = ProjectNameHelpers.to_map(VoxgigStruct.getprop(point, "transform"))
10
- return ctx.reqdata unless transform
17
+ return ProjectNameUtilities.strip_action(ctx.reqdata) unless transform
11
18
  reqform = VoxgigStruct.getprop(transform, "req")
12
- return ctx.reqdata unless reqform
13
- VoxgigStruct.transform({ "reqdata" => ctx.reqdata }, reqform)
19
+ return ProjectNameUtilities.strip_action(ctx.reqdata) unless reqform
20
+ ProjectNameUtilities.strip_action(VoxgigStruct.transform({ "reqdata" => ctx.reqdata }, reqform))
14
21
  }
15
22
  end
@@ -1,4 +1,3 @@
1
-
2
1
  use std::cell::RefCell;
3
2
  use std::collections::HashMap;
4
3
  use std::rc::Rc;
@@ -101,7 +100,7 @@ impl Context {
101
100
  Rc::new(RefCell::new(c))
102
101
  } else if let Some(co) = ctxspec.ctrl_obj {
103
102
  co
104
- } else if let Some(b) = basectx {
103
+ } else if let Some(b) = basectx.filter(|_| ctxspec.opname.is_none()) {
105
104
  b.ctrl.borrow().clone()
106
105
  } else {
107
106
  Rc::new(RefCell::new(Control::new()))
@@ -249,10 +248,7 @@ impl Context {
249
248
  return Rc::new(Operation::new(&Value::empty_map()));
250
249
  }
251
250
 
252
- let opcfg = getpath(
253
- &["entity", &entname, "op", opname],
254
- &self.config.borrow(),
255
- );
251
+ let opcfg = getpath(&["entity", &entname, "op", opname], &self.config.borrow());
256
252
 
257
253
  let input = if opname == "update" || opname == "create" {
258
254
  "data"
@@ -7,6 +7,7 @@ use crate::core::spec::Spec;
7
7
  use crate::core::helpers::{get_bool, getp, getpath, setp};
8
8
  use crate::core::types::Feature;
9
9
  use crate::feature::support::*;
10
+ use crate::utility::voxgigstruct::ordered_map::OrderedMap;
10
11
  use crate::utility::voxgigstruct::Value;
11
12
 
12
13
  pub struct PagingFeature {
@@ -193,7 +194,12 @@ impl Feature for PagingFeature {
193
194
  if !cursor.is_noval() && !cursor.is_null() {
194
195
  setp(&query, &cursor_param, cursor);
195
196
  } else if getp(&query, &page_param).is_noval() {
196
- let page = getp(&paging, "page");
197
+ // A record written back by pre_result holds the page just fetched
198
+ // as "page" and the one to fetch as "nextPage", so nextPage wins.
199
+ let mut page = getp(&paging, "nextPage");
200
+ if page.is_noval() || page.is_null() {
201
+ page = getp(&paging, "page");
202
+ }
197
203
  if !page.is_noval() && !page.is_null() {
198
204
  setp(&query, &page_param, page);
199
205
  } else {
@@ -281,12 +287,16 @@ impl Feature for PagingFeature {
281
287
  }
282
288
  }
283
289
 
284
- // Body-level cursors.
290
+ // Body-level signals; snake_case forms are read first so camelCase wins.
285
291
  if let Value::Map(_) = body {
286
292
  let next = getp(&body, "next");
287
293
  if !next.is_noval() && !next.is_null() && getp(&paging, "next").is_noval() {
288
294
  setp(&paging, "next", next);
289
295
  }
296
+ let snake_cursor = getp(&body, "next_cursor");
297
+ if !snake_cursor.is_noval() && !snake_cursor.is_null() {
298
+ setp(&paging, "cursor", snake_cursor);
299
+ }
290
300
  let cursor = getp(&body, "cursor");
291
301
  if !cursor.is_noval() && !cursor.is_null() {
292
302
  setp(&paging, "cursor", cursor);
@@ -295,6 +305,19 @@ impl Feature for PagingFeature {
295
305
  if !next_cursor.is_noval() && !next_cursor.is_null() {
296
306
  setp(&paging, "cursor", next_cursor);
297
307
  }
308
+ if getp(&paging, "nextPage").is_noval() {
309
+ let mut np = getp(&body, "nextPage");
310
+ if np.is_noval() || np.is_null() {
311
+ np = getp(&body, "next_page");
312
+ }
313
+ if matches!(np, Value::Num(_) | Value::Str(_)) {
314
+ setp(&paging, "nextPage", np);
315
+ }
316
+ }
317
+ if let Some(has_more) = get_bool(&body, "has_more") {
318
+ setp(&paging, "hasMore", Value::Bool(has_more));
319
+ explicit_more = true;
320
+ }
298
321
  if let Some(has_more) = get_bool(&body, "hasMore") {
299
322
  setp(&paging, "hasMore", Value::Bool(has_more));
300
323
  explicit_more = true;
@@ -316,6 +339,24 @@ impl Feature for PagingFeature {
316
339
  }
317
340
 
318
341
  result.borrow_mut().paging = paging.clone();
319
- self.last = paging;
342
+ self.last = paging.clone();
343
+
344
+ // Value::Map is Rc-shared, so the caller's map is refilled in place;
345
+ // a fresh map on ctrl never reaches the caller.
346
+ let ctrl = ctx.ctrl.borrow().clone();
347
+ let shared = ctrl.borrow().paging.clone();
348
+ if let Value::Map(m) = &shared {
349
+ let entries: Vec<(String, Value)> = match &paging {
350
+ Value::Map(pm) => pm.borrow().iter().map(|(k, v)| (k.clone(), v.clone())).collect(),
351
+ _ => Vec::new(),
352
+ };
353
+ let mut mm = m.borrow_mut();
354
+ *mm = OrderedMap::new();
355
+ for (k, v) in entries {
356
+ mm.insert(k, v);
357
+ }
358
+ } else {
359
+ ctrl.borrow_mut().paging = paging;
360
+ }
320
361
  }
321
362
  }
@@ -754,7 +754,7 @@ fn raw_exchange_fetch(fullurl: &str, fetchdef: &Value) -> Result<Value, ProjectN
754
754
  .filter(|m| !m.is_empty())
755
755
  .unwrap_or_else(|| "POST".to_string());
756
756
 
757
- let agent = ureq::AgentBuilder::new().build();
757
+ let agent = crate::utility::fetcher::default_agent();
758
758
  let mut req = agent.request(&method, fullurl);
759
759
 
760
760
  if let Value::Map(m) = getp(fetchdef, "headers") {
@@ -80,6 +80,61 @@ fn resolve_match(ctx: &Rc<Context>, explicit: &Value) -> Value {
80
80
  Value::empty_map()
81
81
  }
82
82
 
83
+ fn point_terminal(p: &Value) -> bool {
84
+ match getp(p, "parts") {
85
+ Value::List(l) => match l.borrow().last() {
86
+ Some(Value::Str(s)) => s.starts_with('{'),
87
+ _ => false,
88
+ },
89
+ _ => false,
90
+ }
91
+ }
92
+
93
+ fn point_depth(p: &Value) -> usize {
94
+ match getp(p, "parts") {
95
+ Value::List(l) => l.borrow().len(),
96
+ _ => 0,
97
+ }
98
+ }
99
+
100
+ // The entity's own endpoint: a terminal `{param}` marks a record route, and
101
+ // among equals the shallower path wins (the same rule as make_point).
102
+ fn pick_point(points: &Value) -> Value {
103
+ let list: Vec<Value> = match points {
104
+ Value::List(l) => l.borrow().clone(),
105
+ _ => return Value::Noval,
106
+ };
107
+ let mut point = match list.first() {
108
+ Some(p) => p.clone(),
109
+ None => return Value::Noval,
110
+ };
111
+ for cand in list.iter().skip(1) {
112
+ if point_terminal(cand) != point_terminal(&point) {
113
+ if point_terminal(cand) {
114
+ point = cand.clone();
115
+ }
116
+ } else if point_depth(cand) < point_depth(&point) {
117
+ point = cand.clone();
118
+ }
119
+ }
120
+ point
121
+ }
122
+
123
+ fn reqd_names(point: &Value, kind: &str) -> Value {
124
+ let args_path = crate::core::helpers::getpath(&["args", kind], point);
125
+ let reqd_args = vs::select(&args_path, &jo(vec![("reqd", Value::Bool(true))]));
126
+ vs::transform(
127
+ &reqd_args,
128
+ &ja(vec![
129
+ Value::str("`$EACH`"),
130
+ Value::str(""),
131
+ Value::str("`$KEY.name`"),
132
+ ]),
133
+ None,
134
+ )
135
+ .unwrap_or_else(|_| Value::empty_list())
136
+ }
137
+
83
138
  fn build_args(ctx: &Rc<Context>, args: &Value) -> Value {
84
139
  let op = ctx.op.borrow().clone();
85
140
  let opname = op.name.clone();
@@ -88,26 +143,16 @@ fn build_args(ctx: &Rc<Context>, args: &Value) -> Value {
88
143
  None => op.entity.clone(),
89
144
  };
90
145
 
91
- // Get last point from config.
92
146
  let points = crate::core::helpers::getpath(
93
147
  &["entity", &entname, "op", &opname, "points"],
94
148
  &ctx.config.borrow(),
95
149
  );
96
- let point = vs::get_elem(&points, &Value::Num(-1.0), Value::Noval);
150
+ let point = pick_point(&points);
97
151
 
98
- // Get required params.
99
- let params_path = crate::core::helpers::getpath(&["args", "params"], &point);
100
- let reqd_params = vs::select(&params_path, &jo(vec![("reqd", Value::Bool(true))]));
101
- let reqd = vs::transform(
102
- &reqd_params,
103
- &ja(vec![
104
- Value::str("`$EACH`"),
105
- Value::str(""),
106
- Value::str("`$KEY.name`"),
107
- ]),
108
- None,
109
- )
110
- .unwrap_or_else(|_| Value::empty_list());
152
+ // Path AND query: a path-only read misses a query-addressed record
153
+ // (e.g. GET /result?trace_id=), which has no path param at all.
154
+ let reqd_params = reqd_names(&point, "params");
155
+ let reqd_query = reqd_names(&point, "query");
111
156
 
112
157
  let qand = Value::empty_list();
113
158
  let q = jo(vec![("`$AND`", qand.clone())]);
@@ -116,8 +161,8 @@ fn build_args(ctx: &Rc<Context>, args: &Value) -> Value {
116
161
  let keys = vs::keysof_vec(args);
117
162
  for key in keys {
118
163
  let is_id = key == "id";
119
- let selected = vs::select(&reqd, &Value::str(key.clone()));
120
- let is_reqd = !vs::is_empty(&selected);
164
+ let is_reqd = !vs::is_empty(&vs::select(&reqd_params, &Value::str(key.clone())))
165
+ || !vs::is_empty(&vs::select(&reqd_query, &Value::str(key.clone())));
121
166
 
122
167
  if is_id || is_reqd {
123
168
  let v = ctx.util().param(ctx, &Value::str(key.clone()));
@@ -215,28 +260,16 @@ fn test_fetch(
215
260
  };
216
261
  let args = build_args(ctx, &update_match);
217
262
  let found = vs::select(&entmap, &args);
218
- let mut ent = vs::get_elem(&found, &Value::Num(0.0), Value::Noval);
219
- if (ent.is_noval() || ent.is_null()) && vs::size(&entmap) > 0 {
220
- // Fall back to any entity in the fixture.
221
- for (_k, e) in vs::items_vec(&entmap) {
222
- if let Value::Map(_) = e {
223
- ent = e;
224
- break;
225
- }
226
- }
227
- }
263
+ let ent = vs::get_elem(&found, &Value::Num(0.0), Value::Noval);
228
264
  if ent.is_noval() || ent.is_null() {
265
+ // update miss: 404, never another record
229
266
  return Ok(respond(ctx, 404,
230
267
  Value::Noval,
231
268
  vec![("statusText", Value::str("Not found"))],
232
269
  ));
233
270
  }
234
- if let Value::Map(_) = &ent {
235
- if let Value::Map(rm) = &reqdata {
236
- for (k, v) in rm.borrow().iter() {
237
- setp(&ent, k, v.clone());
238
- }
239
- }
271
+ if let (Value::Map(_), Value::Map(_)) = (&ent, &reqdata) {
272
+ vs::merge(&Value::list(vec![ent.clone(), reqdata.clone()]), None);
240
273
  }
241
274
  vs::del_prop(ent.clone(), &Value::str("$KEY"));
242
275
  Ok(respond(ctx, 200, vs::clone(&ent), vec![]))
@@ -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