@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.
- package/bin/voxgig-sdkgen +18 -2
- package/dist/action/doctor.d.ts +1 -1
- package/dist/action/doctor.js +12 -10
- package/dist/action/doctor.js.map +1 -1
- package/dist/action/edition.js +4 -0
- package/dist/action/edition.js.map +1 -1
- package/dist/action/feature.js +5 -1
- package/dist/action/feature.js.map +1 -1
- package/dist/action/remove.d.ts +16 -0
- package/dist/action/remove.js +345 -0
- package/dist/action/remove.js.map +1 -0
- package/dist/action/target.js +6 -1
- package/dist/action/target.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/project/.sdk/tm/c/core/context.c +3 -1
- package/project/.sdk/tm/c/feature/paging.c +37 -3
- package/project/.sdk/tm/c/feature/test.c +51 -27
- package/project/.sdk/tm/c/utility/prepare_query.c +2 -1
- package/project/.sdk/tm/c/utility/transform_request.c +25 -3
- package/project/.sdk/tm/clojure/src/sdk/core.clj +21 -7
- package/project/.sdk/tm/clojure/src/sdk/features.clj +68 -18
- package/project/.sdk/tm/cpp/core/types.hpp +1 -1
- package/project/.sdk/tm/cpp/feature/paging.hpp +36 -3
- package/project/.sdk/tm/cpp/feature/test.hpp +41 -13
- package/project/.sdk/tm/cpp/utility/pipeline.hpp +16 -4
- package/project/.sdk/tm/csharp/core/Context.cs +1 -1
- package/project/.sdk/tm/csharp/feature/PagingFeature.cs +58 -4
- package/project/.sdk/tm/csharp/feature/TestFeature.cs +13 -21
- package/project/.sdk/tm/csharp/utility/Fetcher.cs +19 -12
- package/project/.sdk/tm/csharp/utility/PrepareQuery.cs +1 -1
- package/project/.sdk/tm/csharp/utility/TransformRequest.cs +23 -3
- package/project/.sdk/tm/elixir/lib/projectname/context.ex +1 -1
- package/project/.sdk/tm/elixir/lib/projectname/feature/paging.ex +52 -10
- package/project/.sdk/tm/elixir/lib/projectname/feature/test.ex +47 -17
- package/project/.sdk/tm/elixir/lib/projectname/utility.ex +30 -9
- package/project/.sdk/tm/go/core/context.go +13 -13
- package/project/.sdk/tm/go/feature/paging_feature.go +40 -2
- package/project/.sdk/tm/go/feature/test_feature.go +3 -15
- package/project/.sdk/tm/go/test/feature_corpus_test.go +1 -0
- package/project/.sdk/tm/go/test/feature_test.go +54 -0
- package/project/.sdk/tm/go/utility/fetcher.go +41 -20
- package/project/.sdk/tm/go/utility/prepare_query.go +1 -1
- package/project/.sdk/tm/go/utility/transform_request.go +22 -3
- package/project/.sdk/tm/java/core/Context.java +2 -1
- package/project/.sdk/tm/java/feature/PagingFeature.java +46 -4
- package/project/.sdk/tm/java/feature/TestFeature.java +10 -13
- package/project/.sdk/tm/java/test/FeatureCorpusTest.java +3 -0
- package/project/.sdk/tm/java/test/FeatureTest.java +42 -0
- package/project/.sdk/tm/java/utility/Fetcher.java +36 -28
- package/project/.sdk/tm/java/utility/PrepareQuery.java +1 -1
- package/project/.sdk/tm/java/utility/TransformRequest.java +18 -3
- package/project/.sdk/tm/js/src/Context.js +7 -2
- package/project/.sdk/tm/js/src/feature/paging/PagingFeature.js +24 -6
- package/project/.sdk/tm/js/src/feature/test/TestFeature.js +7 -5
- package/project/.sdk/tm/js/src/utility/PrepareQueryUtility.js +1 -1
- package/project/.sdk/tm/js/test/feature/Corpus.test.js +4 -0
- package/project/.sdk/tm/kotlin/core/Context.kt +1 -1
- package/project/.sdk/tm/kotlin/feature/PagingFeature.kt +36 -4
- package/project/.sdk/tm/kotlin/feature/TestFeature.kt +11 -14
- package/project/.sdk/tm/kotlin/utility/Fetcher.kt +33 -24
- package/project/.sdk/tm/kotlin/utility/Prepare.kt +1 -1
- package/project/.sdk/tm/kotlin/utility/ResultTransform.kt +18 -3
- package/project/.sdk/tm/lua/core/context.lua +8 -1
- package/project/.sdk/tm/lua/core/control.lua +3 -0
- package/project/.sdk/tm/lua/feature/paging_feature.lua +44 -5
- package/project/.sdk/tm/lua/feature/test_feature.lua +32 -14
- package/project/.sdk/tm/lua/utility/prepare_query.lua +2 -1
- package/project/.sdk/tm/lua/utility/transform_request.lua +19 -3
- package/project/.sdk/tm/ocaml/sdk_features.ml +72 -21
- package/project/.sdk/tm/ocaml/sdk_runtime.ml +23 -8
- package/project/.sdk/tm/perl/core/context.pm +2 -1
- package/project/.sdk/tm/perl/feature/paging_feature.pm +31 -5
- package/project/.sdk/tm/perl/feature/test_feature.pm +46 -16
- package/project/.sdk/tm/perl/feature/timeout_feature.pm +17 -13
- package/project/.sdk/tm/perl/t/feature_corpus.t +14 -1
- package/project/.sdk/tm/perl/utility/fetcher.pm +15 -1
- package/project/.sdk/tm/perl/utility/prepare_query.pm +1 -0
- package/project/.sdk/tm/perl/utility/transform_request.pm +13 -3
- package/project/.sdk/tm/php/core/Context.php +5 -1
- package/project/.sdk/tm/php/core/Control.php +5 -0
- package/project/.sdk/tm/php/feature/PagingFeature.php +26 -5
- package/project/.sdk/tm/php/feature/TestFeature.php +27 -42
- package/project/.sdk/tm/php/test/FeatureCorpusTest.php +17 -1
- package/project/.sdk/tm/php/test/FeatureTest.php +44 -0
- package/project/.sdk/tm/php/utility/Fetcher.php +17 -3
- package/project/.sdk/tm/php/utility/PrepareQuery.php +1 -1
- package/project/.sdk/tm/php/utility/TransformRequest.php +15 -3
- package/project/.sdk/tm/py/pkg/core/context.py +2 -1
- package/project/.sdk/tm/py/pkg/feature/paging_feature.py +29 -4
- package/project/.sdk/tm/py/pkg/feature/test_feature.py +29 -12
- package/project/.sdk/tm/py/pkg/utility/fetcher.py +63 -65
- package/project/.sdk/tm/py/pkg/utility/prepare_query.py +2 -1
- package/project/.sdk/tm/py/pkg/utility/transform_request.py +11 -3
- package/project/.sdk/tm/py/test/test_feature.py +27 -0
- package/project/.sdk/tm/py/test/test_feature_corpus.py +11 -1
- package/project/.sdk/tm/rb/core/context.rb +1 -1
- package/project/.sdk/tm/rb/feature/paging_feature.rb +27 -5
- package/project/.sdk/tm/rb/feature/test_feature.rb +33 -7
- package/project/.sdk/tm/rb/test/feature_corpus_test.rb +12 -1
- package/project/.sdk/tm/rb/test/feature_test.rb +38 -0
- package/project/.sdk/tm/rb/utility/fetcher.rb +49 -3
- package/project/.sdk/tm/rb/utility/prepare_query.rb +1 -1
- package/project/.sdk/tm/rb/utility/transform_request.rb +10 -3
- package/project/.sdk/tm/rust/core/context.rs +2 -6
- package/project/.sdk/tm/rust/feature/paging.rs +44 -3
- package/project/.sdk/tm/rust/feature/secrets.rs +1 -1
- package/project/.sdk/tm/rust/feature/test.rs +66 -33
- package/project/.sdk/tm/rust/utility/fetcher.rs +41 -14
- package/project/.sdk/tm/rust/utility/prepare_query.rs +1 -1
- package/project/.sdk/tm/rust/utility/transform_request.rs +31 -5
- package/project/.sdk/tm/scala/core/Context.scala +2 -1
- package/project/.sdk/tm/scala/feature/PagingFeature.scala +41 -4
- package/project/.sdk/tm/scala/feature/TestFeature.scala +11 -12
- package/project/.sdk/tm/scala/utility/Fetcher.scala +34 -13
- package/project/.sdk/tm/scala/utility/Prepare.scala +1 -1
- package/project/.sdk/tm/scala/utility/Transform.scala +19 -3
- package/project/.sdk/tm/swift/Sources/ProjectNameSDK/core/Context.swift +1 -1
- package/project/.sdk/tm/swift/Sources/ProjectNameSDK/feature/PagingFeature.swift +31 -4
- package/project/.sdk/tm/swift/Sources/ProjectNameSDK/feature/TestFeature.swift +12 -9
- package/project/.sdk/tm/swift/Sources/ProjectNameSDK/utility/Fetcher.swift +22 -3
- package/project/.sdk/tm/swift/Sources/ProjectNameSDK/utility/Prepare.swift +1 -1
- package/project/.sdk/tm/swift/Sources/ProjectNameSDK/utility/ResultUtil.swift +14 -3
- package/project/.sdk/tm/ts/src/Context.ts +7 -2
- package/project/.sdk/tm/ts/src/feature/paging/PagingFeature.ts +19 -3
- package/project/.sdk/tm/ts/src/feature/test/TestFeature.ts +3 -0
- package/project/.sdk/tm/ts/src/utility/PrepareQueryUtility.ts +1 -1
- package/project/.sdk/tm/ts/test/feature/Corpus.test.ts +1 -0
- package/project/.sdk/tm/zig/core/context.zig +1 -1
- package/project/.sdk/tm/zig/core/utility.zig +19 -5
- package/project/.sdk/tm/zig/feature/paging.zig +34 -4
- package/project/.sdk/tm/zig/feature/test.zig +54 -26
- package/project/sdkgen-package.json +1 -1
- package/src/action/doctor.ts +12 -9
- package/src/action/edition.ts +8 -0
- package/src/action/feature.ts +7 -1
- package/src/action/remove.ts +492 -0
- package/src/action/target.ts +8 -1
|
@@ -724,6 +724,33 @@ class TestPagingFeature:
|
|
|
724
724
|
assert res["result"].paging["cursor"] == "abc"
|
|
725
725
|
assert res["result"].paging["hasMore"] is True
|
|
726
726
|
|
|
727
|
+
def test_snake_case_signals_and_ctrl_write_back(self):
|
|
728
|
+
server, calls = recording_server(lambda n, _fd: make_response(
|
|
729
|
+
200, {"has_more": True, "next_cursor": "c2"} if n == 1
|
|
730
|
+
else {"has_more": False}))
|
|
731
|
+
h = make_client([{"name": "paging"}], server=server)
|
|
732
|
+
pg = {}
|
|
733
|
+
ctrl = {"paging": pg}
|
|
734
|
+
res = h.op(op="list", path="/w", ctrl=ctrl)
|
|
735
|
+
assert res["result"].paging["hasMore"] is True
|
|
736
|
+
assert res["result"].paging["cursor"] == "c2"
|
|
737
|
+
assert pg["cursor"] == "c2" and pg["hasMore"] is True
|
|
738
|
+
h.op(op="list", path="/w", ctrl=ctrl)
|
|
739
|
+
assert re.search(r"[?&]cursor=c2(&|$)", calls[1]["url"])
|
|
740
|
+
assert pg["hasMore"] is False and pg["cursor"] is None
|
|
741
|
+
|
|
742
|
+
def test_continues_from_written_back_next_page(self):
|
|
743
|
+
server, calls = recording_server(lambda n, _fd: make_response(
|
|
744
|
+
200, {"next_page": 2} if n == 1 else {}, {"x-page": str(n)}))
|
|
745
|
+
h = make_client([{"name": "paging"}], server=server)
|
|
746
|
+
pg = {}
|
|
747
|
+
ctrl = {"paging": pg}
|
|
748
|
+
h.op(op="list", path="/w", ctrl=ctrl)
|
|
749
|
+
assert pg["page"] == 1 and pg["nextPage"] == 2 and pg["hasMore"] is True
|
|
750
|
+
h.op(op="list", path="/w", ctrl=ctrl)
|
|
751
|
+
assert re.search(r"[?&]page=2(&|$)", calls[1]["url"])
|
|
752
|
+
assert pg["hasMore"] is False
|
|
753
|
+
|
|
727
754
|
def test_non_list_op_is_not_paged(self):
|
|
728
755
|
server, calls = recording_server()
|
|
729
756
|
h = make_client([{"name": "paging"}], server=server)
|
|
@@ -240,6 +240,14 @@ def _subset(actual, expect, path):
|
|
|
240
240
|
"{}: got {!r}, want {!r}".format(path, actual, expect)
|
|
241
241
|
|
|
242
242
|
|
|
243
|
+
# Whether the SDK built the feature at all. The activity record is the
|
|
244
|
+
# wrong probe: most features create theirs on first use, so an idle client
|
|
245
|
+
# has none and every section but the eager ones read as inert.
|
|
246
|
+
def _present(client, name):
|
|
247
|
+
return any(getattr(f, "name", None) == name
|
|
248
|
+
for f in (getattr(client, "features", None) or []))
|
|
249
|
+
|
|
250
|
+
|
|
243
251
|
def _record(client, name):
|
|
244
252
|
return getattr(client, "_" + name, None)
|
|
245
253
|
|
|
@@ -280,7 +288,9 @@ class TestFeatureCorpus:
|
|
|
280
288
|
# Probed by ACTIVATING it: the feature defaults to inactive, so an
|
|
281
289
|
# idle client never builds it and its absence says nothing.
|
|
282
290
|
probe = _client({"feature": [{"name": name, "active": True}]})
|
|
283
|
-
if
|
|
291
|
+
if not _present(probe, name):
|
|
292
|
+
# The one line every runner prints for an inert section.
|
|
293
|
+
print("feature.{}: inert (this SDK does not generate the feature)".format(name))
|
|
284
294
|
pytest.skip("this SDK was generated without the {} feature".format(name))
|
|
285
295
|
|
|
286
296
|
ops = _usable_ops(2)
|
|
@@ -30,7 +30,7 @@ class ProjectNameContext
|
|
|
30
30
|
@ctrl.explain = ctrl_raw["explain"] if ctrl_raw["explain"].is_a?(Hash)
|
|
31
31
|
@ctrl.actor = ctrl_raw["actor"] if ctrl_raw.key?("actor")
|
|
32
32
|
@ctrl.paging = ctrl_raw["paging"] if ctrl_raw["paging"].is_a?(Hash)
|
|
33
|
-
elsif basectx&.ctrl
|
|
33
|
+
elsif basectx&.ctrl && ProjectNameHelpers.get_ctx_prop(ctxmap, "opname").nil?
|
|
34
34
|
@ctrl = basectx.ctrl
|
|
35
35
|
end
|
|
36
36
|
|
|
@@ -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
|
|
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
|
|
|
@@ -56,8 +56,10 @@ class ProjectNamePagingFeature < ProjectNameBaseFeature
|
|
|
56
56
|
if !paging["cursor"].nil?
|
|
57
57
|
spec.query[cursor_param] = paging["cursor"]
|
|
58
58
|
elsif spec.query[page_param].nil?
|
|
59
|
-
|
|
60
|
-
|
|
59
|
+
# A record written back by PreResult holds the page just fetched as
|
|
60
|
+
# "page" and the one to fetch as "nextPage", so nextPage wins.
|
|
61
|
+
page = paging["nextPage"].nil? ? paging["page"] : paging["nextPage"]
|
|
62
|
+
spec.query[page_param] = page.nil? ? (@options["startPage"] || 1) : page
|
|
61
63
|
end
|
|
62
64
|
|
|
63
65
|
if !@options["limit"].nil? && spec.query[limit_param].nil?
|
|
@@ -158,11 +160,20 @@ class ProjectNamePagingFeature < ProjectNameBaseFeature
|
|
|
158
160
|
paging["next"] = m[1] if m
|
|
159
161
|
end
|
|
160
162
|
|
|
161
|
-
# Body-level
|
|
163
|
+
# Body-level signals; snake_case forms are read first so camelCase wins.
|
|
162
164
|
if body.is_a?(Hash)
|
|
163
165
|
paging["next"] = paging["next"] || body["next"] unless body["next"].nil?
|
|
166
|
+
paging["cursor"] = body["next_cursor"] unless body["next_cursor"].nil?
|
|
164
167
|
paging["cursor"] = body["cursor"] unless body["cursor"].nil?
|
|
165
168
|
paging["cursor"] = body["nextCursor"] unless body["nextCursor"].nil?
|
|
169
|
+
if paging["nextPage"].nil?
|
|
170
|
+
np = body["nextPage"].nil? ? body["next_page"] : body["nextPage"]
|
|
171
|
+
paging["nextPage"] = np if np.is_a?(Numeric) || np.is_a?(String)
|
|
172
|
+
end
|
|
173
|
+
if body["has_more"] == true || body["has_more"] == false
|
|
174
|
+
paging["hasMore"] = body["has_more"]
|
|
175
|
+
explicit_more = true
|
|
176
|
+
end
|
|
166
177
|
if body["hasMore"] == true || body["hasMore"] == false
|
|
167
178
|
paging["hasMore"] = body["hasMore"]
|
|
168
179
|
explicit_more = true
|
|
@@ -181,6 +192,17 @@ class ProjectNamePagingFeature < ProjectNameBaseFeature
|
|
|
181
192
|
|
|
182
193
|
result.paging = paging
|
|
183
194
|
|
|
195
|
+
if ctx.ctrl && ctx.ctrl.respond_to?(:paging=)
|
|
196
|
+
# The Context shares the caller's paging hash by reference, so refill
|
|
197
|
+
# it in place; a fresh hash on ctrl never reaches the caller.
|
|
198
|
+
if ctx.ctrl.paging.is_a?(Hash)
|
|
199
|
+
ctx.ctrl.paging.clear
|
|
200
|
+
ctx.ctrl.paging.merge!(paging)
|
|
201
|
+
else
|
|
202
|
+
ctx.ctrl.paging = paging
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
|
|
184
206
|
@client.instance_variable_set(:@_paging, { "last" => paging })
|
|
185
207
|
end
|
|
186
208
|
|
|
@@ -127,12 +127,10 @@ class ProjectNameTestFeature < ProjectNameBaseFeature
|
|
|
127
127
|
args = test_self.build_args(fctx, op, update_match)
|
|
128
128
|
found = VoxgigStruct.select(entmap, args)
|
|
129
129
|
ent = VoxgigStruct.getelem(found, 0)
|
|
130
|
-
|
|
131
|
-
ent = entmap.values.find { |e| e.is_a?(Hash) }
|
|
132
|
-
end
|
|
130
|
+
# update miss: 404, never another record
|
|
133
131
|
return respond.call(404, nil, { "statusText" => "Not found" }) unless ent
|
|
134
|
-
if ent.is_a?(Hash) && fctx.reqdata
|
|
135
|
-
fctx.reqdata
|
|
132
|
+
if ent.is_a?(Hash) && fctx.reqdata.is_a?(Hash)
|
|
133
|
+
VoxgigStruct.merge([ent, fctx.reqdata])
|
|
136
134
|
end
|
|
137
135
|
VoxgigStruct.delprop(ent, "$KEY")
|
|
138
136
|
out = VoxgigStruct.clone(ent)
|
|
@@ -237,14 +235,42 @@ class ProjectNameTestFeature < ProjectNameBaseFeature
|
|
|
237
235
|
}
|
|
238
236
|
end
|
|
239
237
|
|
|
238
|
+
# The entity's own endpoint: a terminal `{param}` marks a record route,
|
|
239
|
+
# and among equals the shallower path wins (the same rule as make_point).
|
|
240
|
+
def pick_point(points)
|
|
241
|
+
return nil unless points.is_a?(Array) && !points.empty?
|
|
242
|
+
terminal = ->(p) {
|
|
243
|
+
parts = VoxgigStruct.getprop(p, "parts")
|
|
244
|
+
parts.is_a?(Array) && !parts.empty? && parts[-1].is_a?(String) && parts[-1].start_with?("{")
|
|
245
|
+
}
|
|
246
|
+
depth = ->(p) {
|
|
247
|
+
parts = VoxgigStruct.getprop(p, "parts")
|
|
248
|
+
parts.is_a?(Array) ? parts.length : 0
|
|
249
|
+
}
|
|
250
|
+
point = points[0]
|
|
251
|
+
points[1..].each do |cand|
|
|
252
|
+
if terminal.call(cand) != terminal.call(point)
|
|
253
|
+
point = cand if terminal.call(cand)
|
|
254
|
+
elsif depth.call(cand) < depth.call(point)
|
|
255
|
+
point = cand
|
|
256
|
+
end
|
|
257
|
+
end
|
|
258
|
+
point
|
|
259
|
+
end
|
|
260
|
+
|
|
240
261
|
def build_args(ctx, op, args)
|
|
241
262
|
opname = op.name
|
|
242
263
|
points = VoxgigStruct.getpath(ctx.config, "entity.#{ctx.entity.get_name}.op.#{opname}.points")
|
|
243
|
-
point =
|
|
264
|
+
point = pick_point(points)
|
|
244
265
|
|
|
266
|
+
# Path AND query: a path-only read misses a query-addressed record
|
|
267
|
+
# (e.g. GET /result?trace_id=), which has no path param at all.
|
|
245
268
|
params_path = VoxgigStruct.getpath(point, "args.params")
|
|
246
269
|
reqd_params = VoxgigStruct.select(params_path, { "reqd" => true })
|
|
247
|
-
|
|
270
|
+
query_path = VoxgigStruct.getpath(point, "args.query")
|
|
271
|
+
reqd_query = VoxgigStruct.select(query_path, { "reqd" => true })
|
|
272
|
+
reqd = (VoxgigStruct.transform(reqd_params, ["`$EACH`", "", "`$KEY.name`"]) || []) +
|
|
273
|
+
(VoxgigStruct.transform(reqd_query, ["`$EACH`", "", "`$KEY.name`"]) || [])
|
|
248
274
|
|
|
249
275
|
qand = []
|
|
250
276
|
q = { "`$AND`" => qand }
|
|
@@ -200,6 +200,13 @@ class FeatureCorpusTest < Minitest::Test
|
|
|
200
200
|
assert_equal expect, actual, path
|
|
201
201
|
end
|
|
202
202
|
|
|
203
|
+
# Whether the SDK built the feature at all. The activity record is the
|
|
204
|
+
# wrong probe: most features create theirs on first use, so an idle
|
|
205
|
+
# client has none and every section but the eager ones read as inert.
|
|
206
|
+
def present?(client, name)
|
|
207
|
+
(client.features || []).any? { |f| f.respond_to?(:name) && f.name == name }
|
|
208
|
+
end
|
|
209
|
+
|
|
203
210
|
def record(client, name)
|
|
204
211
|
# A corpus key is any manifest item name, so it may carry characters
|
|
205
212
|
# (foo-bar, foo.2) that cannot form a Ruby instance-variable name. The
|
|
@@ -246,7 +253,11 @@ class FeatureCorpusTest < Minitest::Test
|
|
|
246
253
|
# Probed by ACTIVATING it: the feature defaults to inactive, so an idle
|
|
247
254
|
# client never builds it and its absence says nothing.
|
|
248
255
|
probe = build_client({ "feature" => [{ "name" => name, "active" => true }] })
|
|
249
|
-
|
|
256
|
+
unless present?(probe, name)
|
|
257
|
+
# The one line every runner prints for an inert section.
|
|
258
|
+
puts "feature.#{name}: inert (this SDK does not generate the feature)"
|
|
259
|
+
next
|
|
260
|
+
end
|
|
250
261
|
|
|
251
262
|
ops = usable_ops(2)
|
|
252
263
|
by_key = ops.each_with_object({}) { |o, h| h[o["key"]] = o }
|
|
@@ -911,6 +911,44 @@ class FeatureTest < Minitest::Test
|
|
|
911
911
|
assert_equal true, res["result"].paging["hasMore"]
|
|
912
912
|
end
|
|
913
913
|
|
|
914
|
+
def test_paging_snake_case_signals_and_ctrl_write_back
|
|
915
|
+
skip_unless_feature("paging")
|
|
916
|
+
server, calls = H.recording_server { |n, _fd|
|
|
917
|
+
body = 1 == n ? { "has_more" => true, "next_cursor" => "c2" } : { "has_more" => false }
|
|
918
|
+
[H.make_response(200, body), nil]
|
|
919
|
+
}
|
|
920
|
+
h = harness([fspec("paging")], server: server)
|
|
921
|
+
pg = {}
|
|
922
|
+
ctrl = { "paging" => pg }
|
|
923
|
+
res = h.op(opname: "list", path: "/w", ctrl: ctrl)
|
|
924
|
+
assert_equal true, res["result"].paging["hasMore"]
|
|
925
|
+
assert_equal "c2", res["result"].paging["cursor"]
|
|
926
|
+
assert_equal "c2", pg["cursor"], "record written back into ctrl"
|
|
927
|
+
assert_equal true, pg["hasMore"]
|
|
928
|
+
h.op(opname: "list", path: "/w", ctrl: ctrl)
|
|
929
|
+
assert_match(/[?&]cursor=c2(&|\z)/, calls[1]["url"])
|
|
930
|
+
assert_equal false, pg["hasMore"]
|
|
931
|
+
assert_nil pg["cursor"]
|
|
932
|
+
end
|
|
933
|
+
|
|
934
|
+
def test_paging_continues_from_written_back_next_page
|
|
935
|
+
skip_unless_feature("paging")
|
|
936
|
+
server, calls = H.recording_server { |n, _fd|
|
|
937
|
+
body = 1 == n ? { "next_page" => 2 } : {}
|
|
938
|
+
[H.make_response(200, body, "x-page" => n.to_s), nil]
|
|
939
|
+
}
|
|
940
|
+
h = harness([fspec("paging")], server: server)
|
|
941
|
+
pg = {}
|
|
942
|
+
ctrl = { "paging" => pg }
|
|
943
|
+
h.op(opname: "list", path: "/w", ctrl: ctrl)
|
|
944
|
+
assert_equal 1, pg["page"]
|
|
945
|
+
assert_equal 2, pg["nextPage"]
|
|
946
|
+
assert_equal true, pg["hasMore"]
|
|
947
|
+
h.op(opname: "list", path: "/w", ctrl: ctrl)
|
|
948
|
+
assert_match(/[?&]page=2(&|\z)/, calls[1]["url"])
|
|
949
|
+
assert_equal false, pg["hasMore"]
|
|
950
|
+
end
|
|
951
|
+
|
|
914
952
|
def test_paging_non_list_op_is_not_paged
|
|
915
953
|
skip_unless_feature("paging")
|
|
916
954
|
server, calls = H.recording_server
|
|
@@ -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
|
-
|
|
16
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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 =
|
|
150
|
+
let point = pick_point(&points);
|
|
97
151
|
|
|
98
|
-
//
|
|
99
|
-
|
|
100
|
-
let reqd_params =
|
|
101
|
-
let
|
|
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
|
|
120
|
-
|
|
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
|
|
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
|
-
|
|
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![]))
|