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