@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
@@ -1121,9 +1121,15 @@ let paging_feature () : feature =
1121
1121
  | Noval | Null ->
1122
1122
  (match getp q page_param with
1123
1123
  | Noval ->
1124
- let page = (match getp paging "page" with
1125
- | Noval | Null -> (match getp !options "startPage" with Num n -> Num n | _ -> Num 1.)
1126
- | p -> p) in
1124
+ (* A record written back by PreResult holds the page just
1125
+ * fetched as `page` and the one to fetch as `nextPage`,
1126
+ * so nextPage wins. *)
1127
+ let page = (match getp paging "nextPage" with
1128
+ | Noval | Null ->
1129
+ (match getp paging "page" with
1130
+ | Noval | Null -> (match getp !options "startPage" with Num n -> Num n | _ -> Num 1.)
1131
+ | p -> p)
1132
+ | np -> np) in
1127
1133
  setp q page_param page
1128
1134
  | _ -> ())
1129
1135
  | c -> setp q cursor_param c);
@@ -1176,11 +1182,20 @@ let paging_feature () : feature =
1176
1182
  (match body with
1177
1183
  | Map _ ->
1178
1184
  (match getp body "next" with Noval | Null -> () | n -> if is_nullish (getp paging "next") then setp paging "next" n);
1185
+ (* Both spellings, camelCase last so it wins when a body
1186
+ * carries the two. *)
1187
+ (match getp body "next_cursor" with Noval | Null -> () | c -> setp paging "cursor" c);
1179
1188
  (match getp body "cursor" with Noval | Null -> () | c -> setp paging "cursor" c);
1180
1189
  (match getp body "nextCursor" with Noval | Null -> () | c -> setp paging "cursor" c);
1181
- (match getp body "hasMore" with
1182
- | Bool b -> setp paging "hasMore" (Bool b); explicit_more := true
1183
- | _ -> ())
1190
+ if is_nullish (getp paging "nextPage") then begin
1191
+ let np = (match getp body "nextPage" with
1192
+ | Noval | Null -> getp body "next_page"
1193
+ | v -> v) in
1194
+ (match np with Num _ | Str _ -> setp paging "nextPage" np | _ -> ())
1195
+ end;
1196
+ List.iter (fun k -> match getp body k with
1197
+ | Bool b -> setp paging "hasMore" (Bool b); explicit_more := true
1198
+ | _ -> ()) ["has_more"; "hasMore"]
1184
1199
  | _ -> ());
1185
1200
  (* Cursor presence only INFERS another page. When the server
1186
1201
  * stated the answer outright — relay's `hasNextPage: false`, or
@@ -1196,6 +1211,16 @@ let paging_feature () : feature =
1196
1211
  setp paging "hasMore" (Bool hm)
1197
1212
  end;
1198
1213
  result.rt_paging <- paging;
1214
+ (* The record reached the caller through ctrl in every other typed
1215
+ * port and through nothing here, so a caller who asked for paging
1216
+ * got the pages and never the signals to continue. *)
1217
+ (match ctx.c_ctrl.ctrl_paging with
1218
+ | Map _ as held ->
1219
+ (* The caller holds this map: refill it in place. Every key of
1220
+ * the record is written, so no key of an earlier page lives on. *)
1221
+ List.iter (fun k -> setp held k (getp paging k))
1222
+ ["page"; "totalCount"; "nextPage"; "next"; "cursor"; "hasMore"]
1223
+ | _ -> ctx.c_ctrl.ctrl_paging <- paging);
1199
1224
  track_set (cc ctx) "paging" (jo [("last", paging)]))
1200
1225
  | _ -> ());
1201
1226
  f
@@ -1408,21 +1433,43 @@ let test_feature () : feature =
1408
1433
  let out = jo [("status", vint_of status); ("statusText", Str "OK"); ("json", json_thunk (envelope ctx data)); ("body", Str "not-used")] in
1409
1434
  (match extra with Some (Map _ as e) -> List.iter (fun k -> setp out k (getp e k)) (keysof e) | _ -> ());
1410
1435
  (out, None) in
1436
+ let point_terminal p =
1437
+ match getp p "parts" with
1438
+ | List r -> (match List.rev !r with Str s :: _ -> String.length s > 0 && s.[0] = '{' | _ -> false)
1439
+ | _ -> false in
1440
+ let point_depth p = match getp p "parts" with List r -> List.length !r | _ -> 0 in
1441
+ (* The entity's own endpoint: a terminal `{param}` marks a record route, and
1442
+ among equals the shallower path wins (the same rule as make_point). *)
1443
+ let pick_point points =
1444
+ match points with
1445
+ | List r ->
1446
+ (match !r with
1447
+ | [] -> Noval
1448
+ | first :: rest ->
1449
+ List.fold_left (fun point cand ->
1450
+ if point_terminal cand <> point_terminal point then (if point_terminal cand then cand else point)
1451
+ else if point_depth cand < point_depth point then cand
1452
+ else point) first rest)
1453
+ | _ -> Noval in
1454
+ let reqd_names point kind =
1455
+ let reqd_args = select (getpath_s point ("args." ^ kind)) (jo [("reqd", Bool true)]) in
1456
+ transform reqd_args (ja [Str "`$EACH`"; Str ""; Str "`$KEY.name`"]) in
1411
1457
  let build_args ctx (op : operation) args =
1412
1458
  let opname = op.op_name in
1413
1459
  let entname = match ctx.c_entity with Some e -> e.e_name | None -> "_" in
1414
1460
  let points = getpath_s ctx.c_config ("entity." ^ entname ^ ".op." ^ opname ^ ".points") in
1415
- let point = getelem points (Num (-1.0)) in
1416
- let params_path = getpath_s point "args.params" in
1417
- let reqd_params = select params_path (jo [("reqd", Bool true)]) in
1418
- let reqd = transform reqd_params (ja [Str "`$EACH`"; Str ""; Str "`$KEY.name`"]) in
1461
+ let point = pick_point points in
1462
+ (* Path AND query: a path-only read misses a query-addressed record
1463
+ (e.g. GET /result?trace_id=), which has no path param at all. *)
1464
+ let reqd_params = reqd_names point "params" in
1465
+ let reqd_query = reqd_names point "query" in
1419
1466
  let qand = ref [] in
1420
1467
  (match args with
1421
1468
  | Map _ ->
1422
1469
  List.iter (fun key ->
1423
1470
  let is_id = key = "id" in
1424
- let selected = select reqd (Str key) in
1425
- let is_reqd = not (isempty selected) in
1471
+ let is_reqd = not (isempty (select reqd_params (Str key)))
1472
+ || not (isempty (select reqd_query (Str key))) in
1426
1473
  if is_id || is_reqd then begin
1427
1474
  let v = (cu ctx).u_param ctx (Str key) in
1428
1475
  let ka = (match op.op_alias with Map _ -> (match getp op.op_alias key with Str s -> Some s | _ -> None) | _ -> None) in
@@ -1463,18 +1510,22 @@ let test_feature () : feature =
1463
1510
  end
1464
1511
  | "update" ->
1465
1512
  let update_match = empty_map () in
1466
- (match fctx.c_reqdata with Map _ -> (match getp fctx.c_reqdata "id" with Noval -> () | v -> setp update_match "id" v) | _ -> ());
1513
+ (match fctx.c_reqdata with
1514
+ | Map _ ->
1515
+ (match getp fctx.c_reqdata "id" with Noval -> () | v -> setp update_match "id" v);
1516
+ (match getp op.op_alias "id" with
1517
+ | Str alias_id -> (match getp fctx.c_reqdata alias_id with Noval -> () | v -> setp update_match alias_id v)
1518
+ | _ -> ())
1519
+ | _ -> ());
1467
1520
  let update_match = if size update_match > 0 then update_match else resolve_match fctx (empty_map ()) in
1468
1521
  let args = build_args fctx op update_match in
1469
- let ent = ref (getelem (select entmap args) (Num 0.0)) in
1470
- (if is_nullish !ent then match entmap with
1471
- | Map m -> (try (match List.find (fun (_, v) -> match v with Map _ -> true | _ -> false) m.entries with (_, v) -> ent := v) with Not_found -> ())
1472
- | _ -> ());
1473
- if is_nullish !ent then respond fctx 404 Noval (Some (jo [("statusText", Str "Not found")]))
1522
+ let ent = getelem (select entmap args) (Num 0.0) in
1523
+ (* update miss: 404, never another record *)
1524
+ if is_nullish ent then respond fctx 404 Noval (Some (jo [("statusText", Str "Not found")]))
1474
1525
  else begin
1475
- (match !ent with Map _ -> (match fctx.c_reqdata with Map _ -> List.iter (fun k -> setp !ent k (getp fctx.c_reqdata k)) (keysof fctx.c_reqdata) | _ -> ()) | _ -> ());
1476
- ignore (delprop !ent (Str "$KEY"));
1477
- respond fctx 200 (clone !ent) None
1526
+ (match ent, fctx.c_reqdata with Map _, Map _ -> ignore (merge (ja [ent; fctx.c_reqdata])) | _ -> ());
1527
+ ignore (delprop ent (Str "$KEY"));
1528
+ respond fctx 200 (clone ent) None
1478
1529
  end
1479
1530
  | "remove" ->
1480
1531
  let args = build_args fctx op (resolve_match fctx fctx.c_reqmatch) in
@@ -189,7 +189,9 @@ let make_context_impl (cs : ctxspec) (basectx : ctx option) : ctx =
189
189
  (match getp cr "actor" with Noval -> () | a -> c.ctrl_actor <- a);
190
190
  (match getp cr "paging" with Map _ as m -> c.ctrl_paging <- m | _ -> ());
191
191
  c
192
- | _ -> (match basectx with Some b -> b.c_ctrl | None -> new_control ())
192
+ | _ -> (match basectx with
193
+ | Some b when cs.cs_opname = None -> b.c_ctrl
194
+ | _ -> new_control ())
193
195
  in
194
196
  let meta =
195
197
  match cs.cs_meta with Some (Map _ as m) -> m
@@ -413,7 +415,7 @@ let prepare_query_util (ctx : ctx) : value =
413
415
  let out = empty_map () in
414
416
  List.iter (fun k ->
415
417
  let v = getp reqmatch k in
416
- if not (is_noval v) && not (contains_param k) then setp out k v)
418
+ if not (is_noval v) && k <> "$action" && not (contains_param k) then setp out k v)
417
419
  (keysof reqmatch);
418
420
  out
419
421
 
@@ -555,14 +557,27 @@ let prepare_auth_util = Sdk_prepare_auth.prepare_auth_util
555
557
 
556
558
  (* ----- transforms / result helpers ----- *)
557
559
 
560
+ (* `$action` selects the point (see make_point_util); it is never an API
561
+ field, so the body is a copy without it. The caller's map is left
562
+ untouched. *)
563
+ let strip_action (reqdata : value) : value =
564
+ match reqdata with
565
+ | Map _ when List.mem "$action" (keysof reqdata) ->
566
+ let body = empty_map () in
567
+ List.iter (fun k -> if k <> "$action" then setp body k (getp reqdata k))
568
+ (keysof reqdata);
569
+ body
570
+ | _ -> reqdata
571
+
558
572
  let transform_request_util (ctx : ctx) : value =
559
573
  (match ctx.c_spec with Some s -> s.sp_step <- "reqform" | None -> ());
560
- match to_map (getp ctx.c_point "transform") with
561
- | Map _ as tr ->
562
- (match getp tr "req" with
563
- | Noval -> ctx.c_reqdata
564
- | reqform -> transform (jo [("reqdata", ctx.c_reqdata)]) reqform)
565
- | _ -> ctx.c_reqdata
574
+ strip_action
575
+ (match to_map (getp ctx.c_point "transform") with
576
+ | Map _ as tr ->
577
+ (match getp tr "req" with
578
+ | Noval -> ctx.c_reqdata
579
+ | reqform -> transform (jo [("reqdata", ctx.c_reqdata)]) reqform)
580
+ | _ -> ctx.c_reqdata)
566
581
 
567
582
  let transform_response_util (ctx : ctx) : value =
568
583
  (match ctx.c_spec with Some s -> s.sp_step <- "resform" | None -> ());
@@ -45,7 +45,8 @@ sub new {
45
45
  $self->{ctrl}{actor} = $ctrl_raw->{actor} if exists $ctrl_raw->{actor};
46
46
  $self->{ctrl}{paging} = $ctrl_raw->{paging} if Voxgig::Struct::ismap($ctrl_raw->{paging});
47
47
  }
48
- elsif ($basectx && $basectx->{ctrl}) {
48
+ elsif ($basectx && $basectx->{ctrl}
49
+ && !defined ProjectNameHelpers::get_ctx_prop($ctxmap, 'opname')) {
49
50
  $self->{ctrl} = $basectx->{ctrl};
50
51
  }
51
52
 
@@ -4,8 +4,8 @@
4
4
  # stamps page/limit (or a cursor) into the request query; on the way back
5
5
  # (PreResult) it reads the server's pagination signals - a Link rel="next"
6
6
  # header, X-Page/X-Next-Page/X-Total-Count headers, or next/cursor/
7
- # nextCursor/hasMore fields in the body - and records them on
8
- # result.paging. A per-call ctrl paging value (page or cursor) takes
7
+ # nextCursor/hasMore fields in the body (snake_case forms too) - and records
8
+ # them on result.paging and the caller's ctrl paging. A per-call ctrl paging value (page or cursor) takes
9
9
  # priority. Parameter names ("pageParam", "limitParam", "cursorParam"),
10
10
  # "startPage" (default 1) and page size ("limit") are configurable.
11
11
 
@@ -76,8 +76,11 @@ sub PreRequest {
76
76
  $spec->{query}{$cursor_param} = $paging->{cursor};
77
77
  }
78
78
  elsif (!defined $spec->{query}{$page_param}) {
79
- $spec->{query}{$page_param} = defined $paging->{page}
80
- ? $paging->{page}
79
+ # A record written back by PreResult holds the page just fetched as
80
+ # "page" and the one to fetch as "nextPage", so nextPage wins.
81
+ my $page = defined $paging->{nextPage} ? $paging->{nextPage} : $paging->{page};
82
+ $spec->{query}{$page_param} = defined $page
83
+ ? $page
81
84
  : (defined $self->{options}{startPage} ? $self->{options}{startPage} : 1);
82
85
  }
83
86
 
@@ -186,15 +189,27 @@ sub PreResult {
186
189
  }
187
190
  }
188
191
 
189
- # Body-level cursors.
192
+ # Body-level signals; snake_case forms are read first so camelCase wins.
190
193
  if (Voxgig::Struct::ismap($body)) {
191
194
  my $bnext = ProjectNameHelpers::gp($body, 'next');
192
195
  $paging->{next} = defined $paging->{next} ? $paging->{next} : $bnext
193
196
  if defined $bnext;
197
+ my $bsnakecursor = ProjectNameHelpers::gp($body, 'next_cursor');
198
+ $paging->{cursor} = $bsnakecursor if defined $bsnakecursor;
194
199
  my $bcursor = ProjectNameHelpers::gp($body, 'cursor');
195
200
  $paging->{cursor} = $bcursor if defined $bcursor;
196
201
  my $bnextcursor = ProjectNameHelpers::gp($body, 'nextCursor');
197
202
  $paging->{cursor} = $bnextcursor if defined $bnextcursor;
203
+ unless (defined $paging->{nextPage}) {
204
+ my $np = ProjectNameHelpers::gp($body, 'nextPage');
205
+ $np = ProjectNameHelpers::gp($body, 'next_page') unless defined $np;
206
+ $paging->{nextPage} = $np if defined $np && !ref($np);
207
+ }
208
+ my $bsnakemore = $body->{has_more};
209
+ if (ProjectNameHelpers::is_true($bsnakemore) || ProjectNameHelpers::is_false($bsnakemore)) {
210
+ $paging->{hasMore} = ProjectNameHelpers::is_true($bsnakemore) ? 1 : 0;
211
+ $explicit_more = 1;
212
+ }
198
213
  my $bhasmore = $body->{hasMore};
199
214
  if (ProjectNameHelpers::is_true($bhasmore) || ProjectNameHelpers::is_false($bhasmore)) {
200
215
  $paging->{hasMore} = ProjectNameHelpers::is_true($bhasmore) ? 1 : 0;
@@ -215,6 +230,17 @@ sub PreResult {
215
230
 
216
231
  $result->{paging} = $paging;
217
232
 
233
+ if ($ctx->{ctrl}) {
234
+ # The Context shares the caller's paging hash by reference, so refill it
235
+ # in place; a fresh hash on ctrl never reaches the caller.
236
+ if (Voxgig::Struct::ismap($ctx->{ctrl}{paging})) {
237
+ %{ $ctx->{ctrl}{paging} } = %$paging;
238
+ }
239
+ else {
240
+ $ctx->{ctrl}{paging} = $paging;
241
+ }
242
+ }
243
+
218
244
  $self->{client}{_paging} = { 'last' => $paging };
219
245
  return;
220
246
  }
@@ -154,18 +154,11 @@ sub init {
154
154
  my $args = $test_self->build_args($fctx, $op, $update_match);
155
155
  my $found = Voxgig::Struct::select($entmap, $args);
156
156
  my $ent = ProjectNameHelpers::ge($found, 0);
157
- if (!defined $ent && Voxgig::Struct::ismap($entmap) && keys %$entmap) {
158
- for my $k (sort keys %$entmap) {
159
- if (Voxgig::Struct::ismap($entmap->{$k})) {
160
- $ent = $entmap->{$k};
161
- last;
162
- }
163
- }
164
- }
157
+ # update miss: 404, never another record
165
158
  return $respond->($fctx, 404, undef, { 'statusText' => 'Not found' })
166
159
  unless ProjectNameHelpers::rb_truthy($ent);
167
- if (Voxgig::Struct::ismap($ent) && $fctx->{reqdata}) {
168
- $ent->{$_} = $fctx->{reqdata}{$_} for keys %{ $fctx->{reqdata} };
160
+ if (Voxgig::Struct::ismap($ent) && Voxgig::Struct::ismap($fctx->{reqdata})) {
161
+ Voxgig::Struct::merge([$ent, $fctx->{reqdata}]);
169
162
  }
170
163
  Voxgig::Struct::delprop($ent, '$KEY');
171
164
  my $out = Voxgig::Struct::clone($ent);
@@ -290,18 +283,55 @@ sub make_netsim {
290
283
  };
291
284
  }
292
285
 
286
+ sub _point_terminal {
287
+ my ($p) = @_;
288
+ my $parts = ProjectNameHelpers::gp($p, 'parts');
289
+ return 0 unless Voxgig::Struct::islist($parts) && @$parts;
290
+ my $last = $parts->[-1];
291
+ return (defined $last && !ref $last && 0 == index($last, '{')) ? 1 : 0;
292
+ }
293
+
294
+ sub _point_depth {
295
+ my ($p) = @_;
296
+ my $parts = ProjectNameHelpers::gp($p, 'parts');
297
+ return Voxgig::Struct::islist($parts) ? scalar @$parts : 0;
298
+ }
299
+
300
+ # The entity's own endpoint: a terminal `{param}` marks a record route, and
301
+ # among equals the shallower path wins (the same rule as make_point).
302
+ sub _pick_point {
303
+ my ($points) = @_;
304
+ return undef unless Voxgig::Struct::islist($points) && @$points;
305
+ my $point = $points->[0];
306
+ for my $cand (@$points[1 .. $#$points]) {
307
+ if (_point_terminal($cand) != _point_terminal($point)) {
308
+ $point = $cand if _point_terminal($cand);
309
+ }
310
+ elsif (_point_depth($cand) < _point_depth($point)) {
311
+ $point = $cand;
312
+ }
313
+ }
314
+ return $point;
315
+ }
316
+
293
317
  sub build_args {
294
318
  my ($self, $ctx, $op, $args) = @_;
295
319
  my $opname = $op->{name};
296
320
  my $entname = $ctx->{entity}->get_name;
297
321
  my $points = ProjectNameHelpers::gpath($ctx->{config},
298
322
  "entity.$entname.op.$opname.points");
299
- my $point = ProjectNameHelpers::ge($points, -1);
300
-
301
- my $params_path = ProjectNameHelpers::gpath($point, 'args.params');
302
- my $reqd_params = Voxgig::Struct::select($params_path,
303
- { 'reqd' => Voxgig::Struct::JTRUE() });
304
- my $reqd = Voxgig::Struct::transform($reqd_params, ['`$EACH`', '', '`$KEY.name`']);
323
+ my $point = _pick_point($points);
324
+
325
+ # Path AND query: a path-only read misses a query-addressed record
326
+ # (e.g. GET /result?trace_id=), which has no path param at all.
327
+ my $reqd = [];
328
+ for my $kind ('params', 'query') {
329
+ my $args_path = ProjectNameHelpers::gpath($point, "args.$kind");
330
+ my $reqd_args = Voxgig::Struct::select($args_path,
331
+ { 'reqd' => Voxgig::Struct::JTRUE() });
332
+ my $names = Voxgig::Struct::transform($reqd_args, ['`$EACH`', '', '`$KEY.name`']);
333
+ push @$reqd, @$names if Voxgig::Struct::islist($names);
334
+ }
305
335
 
306
336
  my $qand = [];
307
337
  my $q = { '`$AND`' => $qand };
@@ -3,9 +3,11 @@
3
3
  # Per-request timeout. Wraps the active transport with a deadline of "ms"
4
4
  # milliseconds (default 30000; <= 0 disables). The transport is synchronous
5
5
  # (HTTP::Tiny), so a hanging request is interrupted with a SIGALRM timer
6
- # (Time::HiRes::ualarm); when an injectable "now" clock is supplied the
7
- # elapsed wall-clock time is checked instead, so tests can assert the
8
- # deadline deterministically. Expiry yields an error with code "timeout".
6
+ # (Time::HiRes::ualarm) where the platform has one; where it has none
7
+ # (Windows) the deadline is checked once the call returns. When an
8
+ # injectable "now" clock is supplied the elapsed clock time is checked
9
+ # instead, so tests can assert the deadline deterministically. Expiry
10
+ # yields an error with code "timeout".
9
11
 
10
12
  use strict;
11
13
  use warnings;
@@ -75,24 +77,26 @@ sub with_timeout {
75
77
  # Live path: interrupt a hanging synchronous transport.
76
78
  my ($res, $err);
77
79
  my $timed_out = 0;
78
- my $ok = do {
79
- local $SIG{ALRM} = sub { $timed_out = 1; die "ProjectNameTimeout\n" };
80
- eval {
81
- Time::HiRes::ualarm(int($ms * 1000));
82
- ($res, $err) = $inner->($ctx, $url, $fetchdef);
83
- Time::HiRes::ualarm(0);
84
- 1;
85
- };
80
+ my $start = Time::HiRes::time();
81
+ local $SIG{ALRM} = sub { $timed_out = 1; die "ProjectNameTimeout\n" };
82
+ my $armed = eval { Time::HiRes::ualarm(int($ms * 1000)); 1 };
83
+ my $ok = eval {
84
+ ($res, $err) = $inner->($ctx, $url, $fetchdef);
85
+ 1;
86
86
  };
87
- Time::HiRes::ualarm(0);
87
+ my $e = $@;
88
+ eval { Time::HiRes::ualarm(0) } if $armed;
88
89
  if (!$ok) {
89
- my $e = $@;
90
90
  if ($timed_out) {
91
91
  $self->_track($ctx, $ms);
92
92
  return (undef, $ctx->make_error('timeout', "Request exceeded timeout of ${ms}ms"));
93
93
  }
94
94
  die $e;
95
95
  }
96
+ if (!$armed && (Time::HiRes::time() - $start) * 1000 > $ms) {
97
+ $self->_track($ctx, $ms);
98
+ return (undef, $ctx->make_error('timeout', "Request exceeded timeout of ${ms}ms"));
99
+ }
96
100
  return ($res, $err);
97
101
  }
98
102
 
@@ -267,6 +267,15 @@ sub subset {
267
267
  }
268
268
 
269
269
 
270
+ # Whether the SDK built the feature at all. The activity record is the
271
+ # wrong probe: most features create theirs on first use, so an idle client
272
+ # has none and every section but the eager ones read as inert.
273
+ sub present {
274
+ my ($client, $name) = @_;
275
+ return scalar grep { ref $_ && ($_->{name} // '') eq $name }
276
+ @{ $client->{features} || [] };
277
+ }
278
+
270
279
  sub record {
271
280
  my ($client, $name) = @_;
272
281
  return $client->{"_$name"};
@@ -295,7 +304,11 @@ for my $name (sort keys %{ $CORPUS->{feature} || {} }) {
295
304
  # Probed by ACTIVATING it: the feature defaults to inactive, so an idle
296
305
  # client never builds it and its absence says nothing.
297
306
  my $probe = build_client({ feature => [ { name => $name, active => 1 } ] });
298
- next unless defined record($probe, $name);
307
+ unless (present($probe, $name)) {
308
+ # The one line every runner prints for an inert section.
309
+ diag("feature.$name: inert (this SDK does not generate the feature)");
310
+ next;
311
+ }
299
312
 
300
313
  my %by_key = map { $_->{key} => $_ } @ops;
301
314
 
@@ -16,6 +16,16 @@ package ProjectNameUtilities;
16
16
 
17
17
  our %REGISTRY;
18
18
 
19
+ # One HTTP::Tiny per (proxy, redirect) setting: a client keeps its
20
+ # connection alive between requests, so one per request reused nothing.
21
+ my %HTTP_CLIENTS;
22
+
23
+ sub _http_client {
24
+ my (%new_args) = @_;
25
+ my $key = join "\n", map { "$_=$new_args{$_}" } sort keys %new_args;
26
+ return $HTTP_CLIENTS{$key} //= HTTP::Tiny->new(%new_args);
27
+ }
28
+
19
29
  our $DefaultHttpFetch = sub {
20
30
  my ($fullurl, $fetchdef) = @_;
21
31
  $fetchdef = {} unless defined $fetchdef;
@@ -47,8 +57,12 @@ our $DefaultHttpFetch = sub {
47
57
  $new_args{max_redirect} = 0
48
58
  if defined $fetchdef->{redirect} && !ref $fetchdef->{redirect}
49
59
  && 'manual' eq $fetchdef->{redirect};
60
+ # Both keys: HTTP::Tiny lets the environment override its generic proxy.
61
+ $new_args{http_proxy} = $new_args{https_proxy} = "$fetchdef->{proxy}"
62
+ if defined $fetchdef->{proxy} && !ref $fetchdef->{proxy}
63
+ && length $fetchdef->{proxy};
50
64
 
51
- my $res = eval { HTTP::Tiny->new(%new_args)->request($method, $fullurl, $opts) };
65
+ my $res = eval { _http_client(%new_args)->request($method, $fullurl, $opts) };
52
66
  if (!$res) {
53
67
  my $e = defined $@ ? "$@" : 'request failed';
54
68
  $e =~ s/\s+\z//;
@@ -30,6 +30,7 @@ $REGISTRY{prepare_query} = sub {
30
30
  for my $item (@$items) {
31
31
  my ($key, $val) = @$item;
32
32
  next unless ProjectNameHelpers::rb_truthy($val) && defined $key && !ref $key;
33
+ next if '$action' eq $key;
33
34
  next if grep { defined $_ && !ref $_ && $_ eq $key } @$params;
34
35
  $out->{$key} = $val;
35
36
  }
@@ -15,16 +15,26 @@ package ProjectNameUtilities;
15
15
 
16
16
  our %REGISTRY;
17
17
 
18
+ # `$action` selects the point (see make_point); it is never an API field, so
19
+ # the body is a copy without it. The caller's hash is left untouched.
20
+ my $strip_action = sub {
21
+ my ($reqdata) = @_;
22
+ return $reqdata unless 'HASH' eq ref $reqdata && exists $reqdata->{'$action'};
23
+ my %body = %$reqdata;
24
+ delete $body{'$action'};
25
+ return \%body;
26
+ };
27
+
18
28
  $REGISTRY{transform_request} = sub {
19
29
  my ($ctx) = @_;
20
30
  my $spec = $ctx->{spec};
21
31
  my $point = $ctx->{point};
22
32
  $spec->{step} = 'reqform' if $spec;
23
33
  my $transform = ProjectNameHelpers::to_map(ProjectNameHelpers::gp($point, 'transform'));
24
- return $ctx->{reqdata} unless $transform;
34
+ return $strip_action->($ctx->{reqdata}) unless $transform;
25
35
  my $reqform = ProjectNameHelpers::gp($transform, 'req');
26
- return $ctx->{reqdata} unless ProjectNameHelpers::rb_truthy($reqform);
27
- return Voxgig::Struct::transform({ 'reqdata' => $ctx->{reqdata} }, $reqform);
36
+ return $strip_action->($ctx->{reqdata}) unless ProjectNameHelpers::rb_truthy($reqform);
37
+ return $strip_action->(Voxgig::Struct::transform({ 'reqdata' => $ctx->{reqdata} }, $reqform));
28
38
  };
29
39
 
30
40
  1;
@@ -55,7 +55,11 @@ class ProjectNameContext
55
55
  if (array_key_exists('actor', $ctrl_raw)) {
56
56
  $this->ctrl->actor = $ctrl_raw['actor'];
57
57
  }
58
- } elseif ($basectx !== null && $basectx->ctrl !== null) {
58
+ if (isset($ctrl_raw['paging']) && is_array($ctrl_raw['paging'])) {
59
+ $this->ctrl->paging = $ctrl_raw['paging'];
60
+ }
61
+ } elseif ($basectx !== null && $basectx->ctrl !== null
62
+ && ProjectNameHelpers::get_ctx_prop($ctxmap, "opname") === null) {
59
63
  $this->ctrl = $basectx->ctrl;
60
64
  }
61
65
 
@@ -21,11 +21,16 @@ class ProjectNameControl
21
21
  // passed. The shared feature corpus caught it.
22
22
  public mixed $actor;
23
23
 
24
+ // Per-call paging record: read by the paging feature before the request
25
+ // and written back after it. Declared for the same reason as `$actor`.
26
+ public mixed $paging;
27
+
24
28
  public function __construct(array $opts = [])
25
29
  {
26
30
  $this->throw_err = $opts['throw_err'] ?? null;
27
31
  $this->err = null;
28
32
  $this->explain = $opts['explain'] ?? null;
29
33
  $this->actor = $opts['actor'] ?? null;
34
+ $this->paging = $opts['paging'] ?? null;
30
35
  }
31
36
  }
@@ -9,8 +9,8 @@ require_once __DIR__ . '/BaseFeature.php';
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 `ctrl->paging` (page or
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 `ctrl->paging` (page or
14
14
  // cursor) takes priority over the stamped defaults; parameter names and
15
15
  // page size are configurable; `startPage` defaults to 1. Mirrors
16
16
  // ts/src/feature/paging/PagingFeature.ts.
@@ -71,8 +71,10 @@ class ProjectNamePagingFeature extends ProjectNameBaseFeature
71
71
  } elseif (($spec->query[$page_param] ?? null) === null) {
72
72
  $start_page = is_numeric($this->options['startPage'] ?? null)
73
73
  ? $this->options['startPage'] : 1;
74
- $spec->query[$page_param] = ($paging['page'] ?? null) !== null
75
- ? $paging['page'] : $start_page;
74
+ // A record written back by PreResult holds the page just fetched
75
+ // as "page" and the one to fetch as "nextPage", so nextPage wins.
76
+ $page = $paging['nextPage'] ?? $paging['page'] ?? null;
77
+ $spec->query[$page_param] = $page !== null ? $page : $start_page;
76
78
  }
77
79
 
78
80
  if (($this->options['limit'] ?? null) !== null
@@ -194,17 +196,30 @@ class ProjectNamePagingFeature extends ProjectNameBaseFeature
194
196
  }
195
197
  }
196
198
 
197
- // Body-level cursors.
199
+ // Body-level signals; snake_case forms are read first so camelCase wins.
198
200
  if (is_array($body)) {
199
201
  if (($body['next'] ?? null) !== null) {
200
202
  $paging['next'] = $paging['next'] ?? $body['next'];
201
203
  }
204
+ if (($body['next_cursor'] ?? null) !== null) {
205
+ $paging['cursor'] = $body['next_cursor'];
206
+ }
202
207
  if (($body['cursor'] ?? null) !== null) {
203
208
  $paging['cursor'] = $body['cursor'];
204
209
  }
205
210
  if (($body['nextCursor'] ?? null) !== null) {
206
211
  $paging['cursor'] = $body['nextCursor'];
207
212
  }
213
+ if ($paging['nextPage'] === null) {
214
+ $np = $body['nextPage'] ?? $body['next_page'] ?? null;
215
+ if (is_int($np) || is_float($np) || is_string($np)) {
216
+ $paging['nextPage'] = $np;
217
+ }
218
+ }
219
+ if (is_bool($body['has_more'] ?? null)) {
220
+ $paging['hasMore'] = $body['has_more'];
221
+ $explicit_more = true;
222
+ }
208
223
  if (is_bool($body['hasMore'] ?? null)) {
209
224
  $paging['hasMore'] = $body['hasMore'];
210
225
  $explicit_more = true;
@@ -225,6 +240,12 @@ class ProjectNamePagingFeature extends ProjectNameBaseFeature
225
240
 
226
241
  $result->paging = $paging;
227
242
 
243
+ // PHP arrays are values, so the record lands on this call's Control
244
+ // rather than on an array the caller passed.
245
+ if ($ctx->ctrl !== null) {
246
+ $ctx->ctrl->paging = $paging;
247
+ }
248
+
228
249
  $this->client->_paging = ['last' => $paging];
229
250
  }
230
251