@voxgig/sdkgen 0.45.0 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/voxgig-sdkgen +1 -1
- package/package.json +3 -3
- package/project/.sdk/src/cmp/go/Entity_go.ts +2 -2
- package/project/.sdk/src/cmp/go/Main_go.ts +8 -4
- package/project/.sdk/src/cmp/go/Package_go.ts +2 -2
- package/project/.sdk/src/cmp/go/ReadmeExplanation_go.ts +2 -2
- package/project/.sdk/src/cmp/go/ReadmeHowto_go.ts +2 -2
- package/project/.sdk/src/cmp/go/ReadmeInstall_go.ts +2 -2
- package/project/.sdk/src/cmp/go/ReadmeModel_go.ts +2 -2
- package/project/.sdk/src/cmp/go/ReadmeQuick_go.ts +2 -2
- package/project/.sdk/src/cmp/go/ReadmeTopQuick_go.ts +2 -2
- package/project/.sdk/src/cmp/go/ReadmeTopTest_go.ts +2 -2
- package/project/.sdk/src/cmp/go/TestDirect_go.ts +204 -33
- package/project/.sdk/src/cmp/go/TestEntity_go.ts +37 -6
- package/project/.sdk/src/cmp/go/Test_go.ts +2 -2
- package/project/.sdk/src/cmp/go/fragment/Main.fragment.go +21 -4
- package/project/.sdk/src/cmp/lua/Package_lua.ts +9 -2
- package/project/.sdk/src/cmp/lua/TestDirect_lua.ts +154 -21
- package/project/.sdk/src/cmp/lua/TestEntity_lua.ts +25 -1
- package/project/.sdk/src/cmp/lua/fragment/Main.fragment.lua +20 -4
- package/project/.sdk/src/cmp/php/Package_php.ts +7 -1
- package/project/.sdk/src/cmp/php/TestDirect_php.ts +153 -20
- package/project/.sdk/src/cmp/php/TestEntity_php.ts +25 -1
- package/project/.sdk/src/cmp/php/fragment/Main.fragment.php +17 -3
- package/project/.sdk/src/cmp/py/Package_py.ts +8 -1
- package/project/.sdk/src/cmp/py/TestDirect_py.ts +146 -19
- package/project/.sdk/src/cmp/py/TestEntity_py.ts +25 -1
- package/project/.sdk/src/cmp/py/fragment/Main.fragment.py +19 -4
- package/project/.sdk/src/cmp/rb/Package_rb.ts +9 -2
- package/project/.sdk/src/cmp/rb/TestDirect_rb.ts +154 -21
- package/project/.sdk/src/cmp/rb/TestEntity_rb.ts +25 -1
- package/project/.sdk/src/cmp/rb/fragment/Main.fragment.rb +19 -3
- package/project/.sdk/src/cmp/ts/Package_ts.ts +1 -1
- package/project/.sdk/src/cmp/ts/TestDirect_ts.ts +145 -22
- package/project/.sdk/src/cmp/ts/TestEntity_ts.ts +59 -1
- package/project/.sdk/src/cmp/ts/fragment/Direct.test.fragment.ts +8 -1
- package/project/.sdk/src/cmp/ts/fragment/Entity.test.fragment.ts +8 -2
- package/project/.sdk/src/cmp/ts/fragment/Main.fragment.ts +21 -1
- package/project/.sdk/tm/go/feature/test_feature.go +51 -3
- package/project/.sdk/tm/go/test/runner_test.go +106 -6
- package/project/.sdk/tm/go/test/sdk-test-control.json +19 -0
- package/project/.sdk/tm/go/utility/fetcher.go +10 -0
- package/project/.sdk/tm/go/utility/make_url.go +12 -0
- package/project/.sdk/tm/lua/feature/test_feature.lua +41 -3
- package/project/.sdk/tm/lua/test/runner.lua +74 -0
- package/project/.sdk/tm/lua/test/sdk-test-control.json +19 -0
- package/project/.sdk/tm/lua/utility/fetcher.lua +13 -0
- package/project/.sdk/tm/lua/utility/make_url.lua +16 -0
- package/project/.sdk/tm/php/feature/TestFeature.php +185 -43
- package/project/.sdk/tm/php/test/Runner.php +62 -0
- package/project/.sdk/tm/php/test/sdk-test-control.json +19 -0
- package/project/.sdk/tm/php/utility/Fetcher.php +132 -9
- package/project/.sdk/tm/php/utility/MakeUrl.php +16 -0
- package/project/.sdk/tm/py/feature/test_feature.py +35 -3
- package/project/.sdk/tm/py/test/runner.py +60 -0
- package/project/.sdk/tm/py/test/sdk-test-control.json +19 -0
- package/project/.sdk/tm/py/utility/fetcher.py +13 -0
- package/project/.sdk/tm/py/utility/make_url.py +13 -0
- package/project/.sdk/tm/rb/feature/test_feature.rb +36 -3
- package/project/.sdk/tm/rb/test/runner.rb +46 -0
- package/project/.sdk/tm/rb/test/sdk-test-control.json +19 -0
- package/project/.sdk/tm/rb/utility/fetcher.rb +49 -28
- package/project/.sdk/tm/rb/utility/make_url.rb +16 -0
- package/project/.sdk/tm/ts/test/sdk-test-control.json +19 -0
- package/project/.sdk/tm/ts/test/utility.ts +120 -2
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 1,
|
|
3
|
+
"_doc": "Per-SDK test control. Lists tests/operations to skip in unit and live modes; tunes per-test pacing. Edit by hand. Loaded by go test runner.",
|
|
4
|
+
"test": {
|
|
5
|
+
"skip": {
|
|
6
|
+
"live": {
|
|
7
|
+
"direct": [],
|
|
8
|
+
"entityOp": []
|
|
9
|
+
},
|
|
10
|
+
"unit": {
|
|
11
|
+
"direct": [],
|
|
12
|
+
"entityOp": []
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"live": {
|
|
16
|
+
"delayMs": 500
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -27,13 +27,23 @@ func defaultHTTPFetch(fullurl string, fetchdef map[string]any) (map[string]any,
|
|
|
27
27
|
return nil, err
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
hasUA := false
|
|
30
31
|
if headers, ok := fetchdef["headers"].(map[string]any); ok {
|
|
31
32
|
for k, v := range headers {
|
|
32
33
|
if sv, ok := v.(string); ok {
|
|
34
|
+
if strings.EqualFold(k, "user-agent") {
|
|
35
|
+
hasUA = true
|
|
36
|
+
}
|
|
33
37
|
req.Header.Set(k, sv)
|
|
34
38
|
}
|
|
35
39
|
}
|
|
36
40
|
}
|
|
41
|
+
// Default User-Agent — Go's net/http defaults to "Go-http-client/1.1"
|
|
42
|
+
// which some CDNs block. Use a Mozilla-shaped UA unless the caller
|
|
43
|
+
// already set one.
|
|
44
|
+
if !hasUA {
|
|
45
|
+
req.Header.Set("User-Agent", "Mozilla/5.0 (compatible; ProjectNameSDK/1.0)")
|
|
46
|
+
}
|
|
37
47
|
|
|
38
48
|
resp, err := http.DefaultClient.Do(req)
|
|
39
49
|
if err != nil {
|
|
@@ -35,6 +35,18 @@ func makeUrlUtil(ctx *core.Context) (string, error) {
|
|
|
35
35
|
}
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
// Append query string from spec.Query.
|
|
39
|
+
qsep := "?"
|
|
40
|
+
for _, item := range vs.Items(spec.Query) {
|
|
41
|
+
key, _ := item[0].(string)
|
|
42
|
+
val := item[1]
|
|
43
|
+
if val != nil {
|
|
44
|
+
url += qsep + vs.EscUrl(key) + "=" + vs.EscUrl(vs.Stringify(val))
|
|
45
|
+
qsep = "&"
|
|
46
|
+
resmatch[key] = val
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
38
50
|
result.Resmatch = resmatch
|
|
39
51
|
|
|
40
52
|
return url, nil
|
|
@@ -62,8 +62,27 @@ function TestFeature:init(ctx, options)
|
|
|
62
62
|
entmap = {}
|
|
63
63
|
end
|
|
64
64
|
|
|
65
|
+
-- For single-entity ops (load, remove) with an empty explicit match, fall
|
|
66
|
+
-- back to the id the entity client already knows from a prior create/load
|
|
67
|
+
-- (in fctx.match / fctx.data). Mirrors the TS mock where param() resolves
|
|
68
|
+
-- the id from that accumulated state.
|
|
69
|
+
local function resolve_match(explicit)
|
|
70
|
+
if type(explicit) == "table" and next(explicit) ~= nil then
|
|
71
|
+
return explicit
|
|
72
|
+
end
|
|
73
|
+
local function id_of(src)
|
|
74
|
+
if src == nil then return nil end
|
|
75
|
+
local v = vs.getprop(src, "id")
|
|
76
|
+
if v ~= nil and v ~= "__UNDEFINED__" then return v end
|
|
77
|
+
return nil
|
|
78
|
+
end
|
|
79
|
+
local v = id_of(fctx.match) or id_of(fctx.data)
|
|
80
|
+
if v ~= nil then return { id = v } end
|
|
81
|
+
return {}
|
|
82
|
+
end
|
|
83
|
+
|
|
65
84
|
if op.name == "load" then
|
|
66
|
-
local args = test_self:build_args(fctx, op, fctx.reqmatch)
|
|
85
|
+
local args = test_self:build_args(fctx, op, resolve_match(fctx.reqmatch))
|
|
67
86
|
local found = vs.select(entmap, args)
|
|
68
87
|
local ent = vs.getelem(found, 0)
|
|
69
88
|
if ent == nil then
|
|
@@ -88,9 +107,28 @@ function TestFeature:init(ctx, options)
|
|
|
88
107
|
return respond(200, out, nil)
|
|
89
108
|
|
|
90
109
|
elseif op.name == "update" then
|
|
91
|
-
|
|
110
|
+
-- Match the existing entity by id only (or its alias). reqdata also
|
|
111
|
+
-- contains the new field values, which would otherwise cause select
|
|
112
|
+
-- to filter out the entity we want to update. Falls back to first
|
|
113
|
+
-- entity when no match found, mirroring the TS mock.
|
|
114
|
+
local update_match = {}
|
|
115
|
+
if type(fctx.reqdata) == "table" then
|
|
116
|
+
if fctx.reqdata["id"] ~= nil then update_match["id"] = fctx.reqdata["id"] end
|
|
117
|
+
if op.alias_map ~= nil then
|
|
118
|
+
local alias_id = vs.getprop(op.alias_map, "id")
|
|
119
|
+
if alias_id ~= nil and fctx.reqdata[alias_id] ~= nil then
|
|
120
|
+
update_match[alias_id] = fctx.reqdata[alias_id]
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
local args = test_self:build_args(fctx, op, update_match)
|
|
92
125
|
local found = vs.select(entmap, args)
|
|
93
126
|
local ent = vs.getelem(found, 0)
|
|
127
|
+
if ent == nil and type(entmap) == "table" then
|
|
128
|
+
for _, e in pairs(entmap) do
|
|
129
|
+
if type(e) == "table" then ent = e; break end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
94
132
|
if ent == nil then
|
|
95
133
|
return respond(404, nil, { statusText = "Not found" })
|
|
96
134
|
end
|
|
@@ -107,7 +145,7 @@ function TestFeature:init(ctx, options)
|
|
|
107
145
|
return respond(200, out, nil)
|
|
108
146
|
|
|
109
147
|
elseif op.name == "remove" then
|
|
110
|
-
local args = test_self:build_args(fctx, op, fctx.reqmatch)
|
|
148
|
+
local args = test_self:build_args(fctx, op, resolve_match(fctx.reqmatch))
|
|
111
149
|
local found = vs.select(entmap, args)
|
|
112
150
|
local ent = vs.getelem(found, 0)
|
|
113
151
|
if ent == nil then
|
|
@@ -83,4 +83,78 @@ function runner.entity_list_to_data(list)
|
|
|
83
83
|
end
|
|
84
84
|
|
|
85
85
|
|
|
86
|
+
-- Load sdk-test-control.json from this test dir; cache. Returns an
|
|
87
|
+
-- empty-skip default if the file is missing or invalid.
|
|
88
|
+
runner._test_control = nil
|
|
89
|
+
|
|
90
|
+
function runner.load_test_control()
|
|
91
|
+
if runner._test_control ~= nil then
|
|
92
|
+
return runner._test_control
|
|
93
|
+
end
|
|
94
|
+
local this_dir = debug.getinfo(1, "S").source:match("^@(.+/)") or "./"
|
|
95
|
+
local ctrl_path = this_dir .. "sdk-test-control.json"
|
|
96
|
+
local f = io.open(ctrl_path, "r")
|
|
97
|
+
if f == nil then
|
|
98
|
+
runner._test_control = {
|
|
99
|
+
version = 1,
|
|
100
|
+
test = {
|
|
101
|
+
skip = {
|
|
102
|
+
live = { direct = {}, entityOp = {} },
|
|
103
|
+
unit = { direct = {}, entityOp = {} },
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
}
|
|
107
|
+
return runner._test_control
|
|
108
|
+
end
|
|
109
|
+
local content = f:read("*a")
|
|
110
|
+
f:close()
|
|
111
|
+
local parsed = json.decode(content)
|
|
112
|
+
if parsed == nil then
|
|
113
|
+
runner._test_control = {
|
|
114
|
+
version = 1,
|
|
115
|
+
test = {
|
|
116
|
+
skip = {
|
|
117
|
+
live = { direct = {}, entityOp = {} },
|
|
118
|
+
unit = { direct = {}, entityOp = {} },
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
}
|
|
122
|
+
else
|
|
123
|
+
runner._test_control = parsed
|
|
124
|
+
end
|
|
125
|
+
return runner._test_control
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
-- Check sdk-test-control.json for a skip entry. Returns (skip, reason).
|
|
130
|
+
function runner.is_control_skipped(kind, name, mode)
|
|
131
|
+
local ctrl = runner.load_test_control()
|
|
132
|
+
local skip = (((ctrl.test or {}).skip or {})[mode] or {})
|
|
133
|
+
local items = skip[kind] or {}
|
|
134
|
+
for _, item in ipairs(items) do
|
|
135
|
+
if kind == "direct" and item.test == name then
|
|
136
|
+
return true, item.reason
|
|
137
|
+
end
|
|
138
|
+
if kind == "entityOp" then
|
|
139
|
+
local key = (item.entity or "") .. "." .. (item.op or "")
|
|
140
|
+
if key == name then
|
|
141
|
+
return true, item.reason
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
return false, nil
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
-- Per-test live pacing delay (ms); default 500.
|
|
150
|
+
function runner.live_delay_ms()
|
|
151
|
+
local ctrl = runner.load_test_control()
|
|
152
|
+
local v = ((ctrl.test or {}).live or {}).delayMs
|
|
153
|
+
if type(v) == "number" and v >= 0 then
|
|
154
|
+
return v
|
|
155
|
+
end
|
|
156
|
+
return 500
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
|
|
86
160
|
return runner
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 1,
|
|
3
|
+
"_doc": "Per-SDK test control. Lists tests/operations to skip in unit and live modes; tunes per-test pacing. Edit by hand. Loaded by lua test runner.",
|
|
4
|
+
"test": {
|
|
5
|
+
"skip": {
|
|
6
|
+
"live": {
|
|
7
|
+
"direct": [],
|
|
8
|
+
"entityOp": []
|
|
9
|
+
},
|
|
10
|
+
"unit": {
|
|
11
|
+
"direct": [],
|
|
12
|
+
"entityOp": []
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"live": {
|
|
16
|
+
"delayMs": 500
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -25,6 +25,19 @@ local function default_http_fetch(fullurl, fetchdef)
|
|
|
25
25
|
headers["content-length"] = tostring(#body_str)
|
|
26
26
|
end
|
|
27
27
|
|
|
28
|
+
-- Default User-Agent — many CDNs reject requests with no UA. Set a
|
|
29
|
+
-- Mozilla-shaped UA unless the caller already set one.
|
|
30
|
+
local has_ua = false
|
|
31
|
+
for k, _ in pairs(headers) do
|
|
32
|
+
if string.lower(k) == "user-agent" then
|
|
33
|
+
has_ua = true
|
|
34
|
+
break
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
if not has_ua then
|
|
38
|
+
headers["User-Agent"] = "Mozilla/5.0 (compatible; ProjectNameSDK/1.0)"
|
|
39
|
+
end
|
|
40
|
+
|
|
28
41
|
local response_body = {}
|
|
29
42
|
local res, code, response_headers = http.request({
|
|
30
43
|
url = fullurl,
|
|
@@ -38,6 +38,22 @@ local function make_url_util(ctx)
|
|
|
38
38
|
end
|
|
39
39
|
end
|
|
40
40
|
|
|
41
|
+
-- Append query string from spec.query.
|
|
42
|
+
local qsep = "?"
|
|
43
|
+
local query_items = vs.items(spec.query)
|
|
44
|
+
if query_items ~= nil then
|
|
45
|
+
for _, item in ipairs(query_items) do
|
|
46
|
+
local key = item[1]
|
|
47
|
+
local val = item[2]
|
|
48
|
+
if val ~= nil and type(key) == "string" then
|
|
49
|
+
local val_str = type(val) == "string" and val or tostring(val)
|
|
50
|
+
url = url .. qsep .. vs.escurl(key) .. "=" .. vs.escurl(val_str)
|
|
51
|
+
qsep = "&"
|
|
52
|
+
resmatch[key] = val
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
41
57
|
result.resmatch = resmatch
|
|
42
58
|
|
|
43
59
|
return url, nil
|
|
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
|
|
4
4
|
// ProjectName SDK test feature
|
|
5
5
|
|
|
6
6
|
require_once __DIR__ . '/BaseFeature.php';
|
|
7
|
+
require_once __DIR__ . '/../utility/Param.php';
|
|
7
8
|
|
|
8
9
|
class ProjectNameTestFeature extends ProjectNameBaseFeature
|
|
9
10
|
{
|
|
@@ -45,7 +46,7 @@ class ProjectNameTestFeature extends ProjectNameBaseFeature
|
|
|
45
46
|
$entity->data = $entity_data;
|
|
46
47
|
|
|
47
48
|
$test_fetcher = function (ProjectNameContext $fctx, string $_fullurl, array $_fetchdef) use ($entity): array {
|
|
48
|
-
$respond = function (int $status, mixed $data, ?array $extra): array {
|
|
49
|
+
$respond = function (int $status, mixed $data, ?array $extra = null): array {
|
|
49
50
|
$out = [
|
|
50
51
|
'status' => $status,
|
|
51
52
|
'statusText' => 'OK',
|
|
@@ -64,53 +65,115 @@ class ProjectNameTestFeature extends ProjectNameBaseFeature
|
|
|
64
65
|
$entname = $op->entity;
|
|
65
66
|
$entmap = is_array($entity->data[$entname] ?? null) ? $entity->data[$entname] : [];
|
|
66
67
|
|
|
67
|
-
//
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
68
|
+
// PHP-portable equivalent of TS buildArgs+select: a flat-key
|
|
69
|
+
// filter that matches by exact-equality on each provided key,
|
|
70
|
+
// with alias fallback. Empty match matches all entries — load
|
|
71
|
+
// with empty match returns the first fixture entry (or last
|
|
72
|
+
// create), list returns all entries.
|
|
73
|
+
$find_first = function (array $entmap, $match, $alias) {
|
|
74
|
+
if (!is_array($match) || empty($match)) {
|
|
75
|
+
foreach ($entmap as $e) {
|
|
76
|
+
if (is_array($e)) return $e;
|
|
77
|
+
}
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
foreach ($entmap as $e) {
|
|
81
|
+
if (!is_array($e)) continue;
|
|
82
|
+
$ok = true;
|
|
83
|
+
foreach ($match as $k => $v) {
|
|
84
|
+
if ($v === null || $v === '__UNDEFINED__') continue;
|
|
85
|
+
$ev = $e[$k] ?? null;
|
|
86
|
+
if ($ev !== $v) {
|
|
87
|
+
// Try alias key if any
|
|
88
|
+
$ka = is_array($alias) ? ($alias[$k] ?? null) : null;
|
|
89
|
+
$aliased = ($ka !== null) ? ($e[$ka] ?? null) : null;
|
|
90
|
+
if ($aliased !== $v) {
|
|
91
|
+
$ok = false;
|
|
92
|
+
break;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
73
95
|
}
|
|
96
|
+
if ($ok) return $e;
|
|
74
97
|
}
|
|
75
98
|
return null;
|
|
76
99
|
};
|
|
77
100
|
|
|
78
|
-
|
|
79
|
-
$
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
101
|
+
$find_all = function (array $entmap, $match, $alias) use ($find_first) {
|
|
102
|
+
if (!is_array($match) || empty($match)) {
|
|
103
|
+
return array_values(array_filter($entmap, 'is_array'));
|
|
104
|
+
}
|
|
105
|
+
$out = [];
|
|
106
|
+
foreach ($entmap as $e) {
|
|
107
|
+
if (!is_array($e)) continue;
|
|
108
|
+
$ok = true;
|
|
109
|
+
foreach ($match as $k => $v) {
|
|
110
|
+
if ($v === null || $v === '__UNDEFINED__') continue;
|
|
111
|
+
$ev = $e[$k] ?? null;
|
|
112
|
+
if ($ev !== $v) {
|
|
113
|
+
$ka = is_array($alias) ? ($alias[$k] ?? null) : null;
|
|
114
|
+
$aliased = ($ka !== null) ? ($e[$ka] ?? null) : null;
|
|
115
|
+
if ($aliased !== $v) { $ok = false; break; }
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
if ($ok) $out[] = $e;
|
|
119
|
+
}
|
|
120
|
+
return $out;
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
$alias = is_object($op) ? ($op->alias ?? null) : \Voxgig\Struct\Struct::getprop($op, 'alias');
|
|
124
|
+
|
|
125
|
+
// For single-entity ops (load, remove) with an empty explicit
|
|
126
|
+
// match, fall back to the id the entity client already knows from a
|
|
127
|
+
// prior create/load (carried in $fctx->match / $fctx->data). This
|
|
128
|
+
// mirrors the TS mock where param() resolves the id from that
|
|
129
|
+
// accumulated state — e.g. `create()` then `remove([])` removes the
|
|
130
|
+
// just-created entity, not an arbitrary fixture.
|
|
131
|
+
$resolve_match = function ($explicit) use ($fctx) {
|
|
132
|
+
if (is_array($explicit) && !empty($explicit)) return $explicit;
|
|
133
|
+
foreach ([$fctx->match, $fctx->data] as $src) {
|
|
134
|
+
$arr = is_array($src) ? $src : (is_object($src) ? (array) $src : []);
|
|
135
|
+
if (isset($arr['id']) && $arr['id'] !== null && $arr['id'] !== '__UNDEFINED__') {
|
|
136
|
+
return ['id' => $arr['id']];
|
|
85
137
|
}
|
|
86
138
|
}
|
|
87
|
-
|
|
139
|
+
return [];
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
if ($op->name === 'load') {
|
|
143
|
+
$ent = $find_first($entmap, $resolve_match($fctx->reqmatch), $alias);
|
|
144
|
+
if ($ent === null) {
|
|
88
145
|
return $respond(404, null, ['statusText' => 'Not found']);
|
|
89
146
|
}
|
|
147
|
+
if (is_array($ent)) unset($ent['$KEY']);
|
|
90
148
|
$out = \Voxgig\Struct\Struct::clone($ent);
|
|
91
|
-
|
|
92
|
-
return $respond(200, $out, null);
|
|
149
|
+
return $respond(200, $out);
|
|
93
150
|
|
|
94
151
|
} elseif ($op->name === 'list') {
|
|
95
|
-
$
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
$out[] = $copy;
|
|
101
|
-
}
|
|
152
|
+
$found = $find_all($entmap, $fctx->reqmatch, $alias);
|
|
153
|
+
$cleaned = [];
|
|
154
|
+
foreach ($found as $e) {
|
|
155
|
+
if (is_array($e)) unset($e['$KEY']);
|
|
156
|
+
$cleaned[] = $e;
|
|
102
157
|
}
|
|
103
|
-
|
|
158
|
+
$out = \Voxgig\Struct\Struct::clone($cleaned);
|
|
159
|
+
return $respond(200, $out);
|
|
104
160
|
|
|
105
161
|
} elseif ($op->name === 'update') {
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
162
|
+
// Match the existing entity by id only (or its alias). reqdata
|
|
163
|
+
// also contains the new field values, which would otherwise
|
|
164
|
+
// cause find_first to filter out the entity we want to update.
|
|
165
|
+
$update_match = [];
|
|
166
|
+
if (is_array($fctx->reqdata)) {
|
|
167
|
+
if (array_key_exists('id', $fctx->reqdata)) {
|
|
168
|
+
$update_match['id'] = $fctx->reqdata['id'];
|
|
169
|
+
}
|
|
170
|
+
$id_alias = is_array($alias) ? ($alias['id'] ?? null) : null;
|
|
171
|
+
if ($id_alias !== null && array_key_exists($id_alias, $fctx->reqdata)) {
|
|
172
|
+
$update_match[$id_alias] = $fctx->reqdata[$id_alias];
|
|
111
173
|
}
|
|
112
174
|
}
|
|
113
|
-
|
|
175
|
+
$ent = $find_first($entmap, $update_match, $alias);
|
|
176
|
+
if ($ent === null) {
|
|
114
177
|
return $respond(404, null, ['statusText' => 'Not found']);
|
|
115
178
|
}
|
|
116
179
|
if (is_array($fctx->reqdata)) {
|
|
@@ -118,33 +181,42 @@ class ProjectNameTestFeature extends ProjectNameBaseFeature
|
|
|
118
181
|
$ent[$k] = $v;
|
|
119
182
|
}
|
|
120
183
|
}
|
|
121
|
-
$
|
|
122
|
-
$
|
|
184
|
+
$id = is_array($ent) ? ($ent['id'] ?? null) : null;
|
|
185
|
+
if ($id !== null) {
|
|
186
|
+
$entmap[$id] = $ent;
|
|
187
|
+
$entity->data[$entname] = $entmap;
|
|
188
|
+
}
|
|
189
|
+
if (is_array($ent)) unset($ent['$KEY']);
|
|
123
190
|
$out = \Voxgig\Struct\Struct::clone($ent);
|
|
124
|
-
|
|
125
|
-
return $respond(200, $out, null);
|
|
191
|
+
return $respond(200, $out);
|
|
126
192
|
|
|
127
193
|
} elseif ($op->name === 'remove') {
|
|
128
|
-
$
|
|
129
|
-
if ($
|
|
194
|
+
$ent = $find_first($entmap, $resolve_match($fctx->reqmatch), $alias);
|
|
195
|
+
if ($ent === null) {
|
|
196
|
+
return $respond(404, null, ['statusText' => 'Not found']);
|
|
197
|
+
}
|
|
198
|
+
$id = is_array($ent) ? ($ent['id'] ?? null) : null;
|
|
199
|
+
if ($id !== null) {
|
|
130
200
|
unset($entmap[$id]);
|
|
131
201
|
$entity->data[$entname] = $entmap;
|
|
132
202
|
}
|
|
133
|
-
return $respond(200, null
|
|
203
|
+
return $respond(200, null);
|
|
134
204
|
|
|
135
205
|
} elseif ($op->name === 'create') {
|
|
136
|
-
$id = $
|
|
137
|
-
if ($id === null) {
|
|
138
|
-
$id = sprintf('%04x%04x%04x%04x',
|
|
206
|
+
$id = ProjectNameParam::call($fctx, 'id');
|
|
207
|
+
if ($id === null || $id === '__UNDEFINED__') {
|
|
208
|
+
$id = sprintf('%04x%04x%04x%04x',
|
|
209
|
+
random_int(0, 0xFFFF), random_int(0, 0xFFFF),
|
|
210
|
+
random_int(0, 0xFFFF), random_int(0, 0xFFFF));
|
|
139
211
|
}
|
|
140
212
|
|
|
141
213
|
$ent = is_array($fctx->reqdata) ? $fctx->reqdata : [];
|
|
142
214
|
$ent['id'] = $id;
|
|
143
215
|
$entmap[$id] = $ent;
|
|
144
216
|
$entity->data[$entname] = $entmap;
|
|
217
|
+
if (is_array($ent)) unset($ent['$KEY']);
|
|
145
218
|
$out = \Voxgig\Struct\Struct::clone($ent);
|
|
146
|
-
|
|
147
|
-
return $respond(200, $out, null);
|
|
219
|
+
return $respond(200, $out);
|
|
148
220
|
|
|
149
221
|
} else {
|
|
150
222
|
return $respond(404, null, ['statusText' => 'Unknown operation']);
|
|
@@ -153,4 +225,74 @@ class ProjectNameTestFeature extends ProjectNameBaseFeature
|
|
|
153
225
|
|
|
154
226
|
$ctx->utility->fetcher = $test_fetcher;
|
|
155
227
|
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Build a structured `$AND` query from the request match/data dict,
|
|
231
|
+
* matching the TS test feature's buildArgs. Mirrors ts/src/feature/test/TestFeature.ts:158-204.
|
|
232
|
+
*
|
|
233
|
+
* For each key in $args that is 'id' OR a required-param key on the
|
|
234
|
+
* current operation point, emit a `$OR` clause matching the key (and
|
|
235
|
+
* its alias, if any) against the supplied value.
|
|
236
|
+
*/
|
|
237
|
+
public function buildArgs(ProjectNameContext $ctx, $op, $args): array
|
|
238
|
+
{
|
|
239
|
+
// If args is empty/missing, return an empty $AND so select() matches
|
|
240
|
+
// every entry — the TS test feature relies on this for empty-match
|
|
241
|
+
// load against fixture entries.
|
|
242
|
+
$keys = is_array($args) ? \Voxgig\Struct\Struct::keysof($args) : [];
|
|
243
|
+
if (empty($keys)) {
|
|
244
|
+
return ['$AND' => []];
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
$opname = is_object($op) ? ($op->name ?? null) : (\Voxgig\Struct\Struct::getprop($op, 'name'));
|
|
248
|
+
$entityName = null;
|
|
249
|
+
if (isset($ctx->entity)) {
|
|
250
|
+
$entityName = is_object($ctx->entity)
|
|
251
|
+
? ($ctx->entity->name ?? null)
|
|
252
|
+
: (is_array($ctx->entity) ? ($ctx->entity['name'] ?? null) : null);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Resolve required-param names from the op's last point. Defensive:
|
|
256
|
+
// any missing piece falls back to "no required params".
|
|
257
|
+
$reqd_names = [];
|
|
258
|
+
if (is_string($opname) && is_string($entityName) && isset($ctx->config)) {
|
|
259
|
+
$points = \Voxgig\Struct\Struct::getpath(
|
|
260
|
+
['entity', $entityName, 'op', $opname, 'points'],
|
|
261
|
+
$ctx->config
|
|
262
|
+
);
|
|
263
|
+
$point = \Voxgig\Struct\Struct::getelem($points, -1);
|
|
264
|
+
$params = is_array($point) ? ($point['args']['params'] ?? null) : null;
|
|
265
|
+
if (is_array($params)) {
|
|
266
|
+
foreach ($params as $p) {
|
|
267
|
+
if (is_array($p) && (($p['reqd'] ?? false) === true)) {
|
|
268
|
+
$n = $p['name'] ?? null;
|
|
269
|
+
if ($n !== null) {
|
|
270
|
+
$reqd_names[] = $n;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
$alias = is_object($op) ? ($op->alias ?? null) : \Voxgig\Struct\Struct::getprop($op, 'alias');
|
|
278
|
+
$qand = [];
|
|
279
|
+
|
|
280
|
+
foreach ($keys as $k) {
|
|
281
|
+
$is_id = ($k === 'id');
|
|
282
|
+
$in_reqd = in_array($k, $reqd_names, true);
|
|
283
|
+
if ($is_id || $in_reqd) {
|
|
284
|
+
$v = ProjectNameParam::call($ctx, $k);
|
|
285
|
+
$ka = \Voxgig\Struct\Struct::getprop($alias, $k);
|
|
286
|
+
|
|
287
|
+
$qor = [[$k => $v]];
|
|
288
|
+
if ($ka !== null) {
|
|
289
|
+
$qor[] = [$ka => $v];
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
$qand[] = ['$OR' => $qor];
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
return ['$AND' => $qand];
|
|
297
|
+
}
|
|
156
298
|
}
|
|
@@ -86,6 +86,68 @@ class ProjectNameTestRunner
|
|
|
86
86
|
}
|
|
87
87
|
return $out;
|
|
88
88
|
}
|
|
89
|
+
|
|
90
|
+
private static $test_control = null;
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Load sdk-test-control.json from this test dir; cache. Returns an
|
|
94
|
+
* empty-skip default if the file is missing or invalid.
|
|
95
|
+
*/
|
|
96
|
+
public static function load_test_control(): array
|
|
97
|
+
{
|
|
98
|
+
if (self::$test_control !== null) {
|
|
99
|
+
return self::$test_control;
|
|
100
|
+
}
|
|
101
|
+
$ctrl_path = __DIR__ . '/sdk-test-control.json';
|
|
102
|
+
$default = [
|
|
103
|
+
'version' => 1,
|
|
104
|
+
'test' => ['skip' => [
|
|
105
|
+
'live' => ['direct' => [], 'entityOp' => []],
|
|
106
|
+
'unit' => ['direct' => [], 'entityOp' => []],
|
|
107
|
+
]],
|
|
108
|
+
];
|
|
109
|
+
if (!file_exists($ctrl_path)) {
|
|
110
|
+
self::$test_control = $default;
|
|
111
|
+
return self::$test_control;
|
|
112
|
+
}
|
|
113
|
+
$content = file_get_contents($ctrl_path);
|
|
114
|
+
$parsed = json_decode($content, true);
|
|
115
|
+
self::$test_control = is_array($parsed) ? $parsed : $default;
|
|
116
|
+
return self::$test_control;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Check sdk-test-control.json for a skip entry. Returns [skip, reason].
|
|
121
|
+
*/
|
|
122
|
+
public static function is_control_skipped(string $kind, string $name, string $mode): array
|
|
123
|
+
{
|
|
124
|
+
$ctrl = self::load_test_control();
|
|
125
|
+
$skip = $ctrl['test']['skip'][$mode] ?? [];
|
|
126
|
+
$items = $skip[$kind] ?? [];
|
|
127
|
+
foreach ($items as $item) {
|
|
128
|
+
if ($kind === 'direct' && ($item['test'] ?? null) === $name) {
|
|
129
|
+
return [true, $item['reason'] ?? null];
|
|
130
|
+
}
|
|
131
|
+
if ($kind === 'entityOp') {
|
|
132
|
+
$key = ($item['entity'] ?? '') . '.' . ($item['op'] ?? '');
|
|
133
|
+
if ($key === $name) {
|
|
134
|
+
return [true, $item['reason'] ?? null];
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return [false, null];
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** Per-test live pacing delay (ms); default 500. */
|
|
142
|
+
public static function live_delay_ms(): int
|
|
143
|
+
{
|
|
144
|
+
$ctrl = self::load_test_control();
|
|
145
|
+
$v = $ctrl['test']['live']['delayMs'] ?? null;
|
|
146
|
+
if (is_int($v) && $v >= 0) {
|
|
147
|
+
return $v;
|
|
148
|
+
}
|
|
149
|
+
return 500;
|
|
150
|
+
}
|
|
89
151
|
}
|
|
90
152
|
|
|
91
153
|
// Aliases for test convenience.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 1,
|
|
3
|
+
"_doc": "Per-SDK test control. Lists tests/operations to skip in unit and live modes; tunes per-test pacing. Edit by hand. Loaded by php test runner.",
|
|
4
|
+
"test": {
|
|
5
|
+
"skip": {
|
|
6
|
+
"live": {
|
|
7
|
+
"direct": [],
|
|
8
|
+
"entityOp": []
|
|
9
|
+
},
|
|
10
|
+
"unit": {
|
|
11
|
+
"direct": [],
|
|
12
|
+
"entityOp": []
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"live": {
|
|
16
|
+
"delayMs": 500
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|