@voxgig/sdkgen 4.0.1 → 4.1.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 +1 -1
- package/package.json +1 -1
- package/project/.sdk/tm/c/feature/test.c +50 -12
- package/project/.sdk/tm/cpp/feature/test.hpp +34 -11
- package/project/.sdk/tm/csharp/feature/TestFeature.cs +28 -2
- package/project/.sdk/tm/elixir/lib/projectname/feature/test.ex +39 -12
- package/project/.sdk/tm/java/feature/TestFeature.java +46 -12
- package/project/.sdk/tm/kotlin/feature/TestFeature.kt +29 -11
- package/project/.sdk/tm/perl/feature/test_feature.pm +34 -12
- package/project/.sdk/tm/rust/feature/test.rs +41 -16
- package/project/.sdk/tm/scala/feature/TestFeature.scala +35 -12
- package/project/.sdk/tm/swift/Sources/ProjectNameSDK/feature/TestFeature.swift +33 -12
- package/project/.sdk/tm/zig/feature/test.zig +39 -12
- package/project/sdkgen-package.json +1 -1
package/bin/voxgig-sdkgen
CHANGED
package/package.json
CHANGED
|
@@ -10,10 +10,48 @@
|
|
|
10
10
|
#include <stdlib.h>
|
|
11
11
|
#include <string.h>
|
|
12
12
|
|
|
13
|
+
// THE MOCK HAS TO AGREE WITH THE MODEL.
|
|
14
|
+
//
|
|
15
|
+
// A point carrying `transform.res: `body.item`` describes an API that answers
|
|
16
|
+
// {"item": {...}}, and the response transform unwraps that key on the way
|
|
17
|
+
// back. Handing back the bare payload means the transform unwraps a property
|
|
18
|
+
// that is not there and the caller gets nothing — a mock that only ever
|
|
19
|
+
// simulates APIs whose responses happen to be unwrapped.
|
|
20
|
+
//
|
|
21
|
+
// univec's list op declares `body.data`, so every list returned zero items
|
|
22
|
+
// while the fixture plainly held two. Mirrors the go/ts/lua/php mocks, which
|
|
23
|
+
// already wrap; rust, c and zig were the three that did not.
|
|
24
|
+
static voxgig_value* envelope(Context* ctx, voxgig_value* data) {
|
|
25
|
+
if (v_is_noval(data) || v_is_null(data)) {
|
|
26
|
+
return data;
|
|
27
|
+
}
|
|
28
|
+
voxgig_value* tm = getp(ctx->point, "transform");
|
|
29
|
+
const char* spec = get_str(tm, "res");
|
|
30
|
+
if (NULL == spec) {
|
|
31
|
+
return data;
|
|
32
|
+
}
|
|
33
|
+
// Exactly `body.<key>`; a deeper path is not an envelope this mock can
|
|
34
|
+
// synthesise, so it is left alone rather than guessed at.
|
|
35
|
+
size_t n = strlen(spec);
|
|
36
|
+
if (n < 8 || 0 != strncmp(spec, "`body.", 6) || '`' != spec[n - 1]) {
|
|
37
|
+
return data;
|
|
38
|
+
}
|
|
39
|
+
size_t inner_len = n - 7;
|
|
40
|
+
if (0 == inner_len || NULL != memchr(spec + 6, '.', inner_len)) {
|
|
41
|
+
return data;
|
|
42
|
+
}
|
|
43
|
+
char* inner = (char*)malloc(inner_len + 1);
|
|
44
|
+
memcpy(inner, spec + 6, inner_len);
|
|
45
|
+
inner[inner_len] = '\0';
|
|
46
|
+
voxgig_value* out = cmap(1, inner, data);
|
|
47
|
+
free(inner);
|
|
48
|
+
return out;
|
|
49
|
+
}
|
|
50
|
+
|
|
13
51
|
// respond builds a transport-shaped response the result pipeline understands.
|
|
14
|
-
static voxgig_value* respond(int64_t status, voxgig_value* data) {
|
|
52
|
+
static voxgig_value* respond(Context* ctx, int64_t status, voxgig_value* data) {
|
|
15
53
|
return cmap(4, "status", v_num((double)status), "statusText", v_str("OK"), "json",
|
|
16
|
-
json_thunk(data), "body", v_str("not-used"));
|
|
54
|
+
json_thunk(envelope(ctx, data)), "body", v_str("not-used"));
|
|
17
55
|
}
|
|
18
56
|
|
|
19
57
|
// For single-entity ops (load, remove) with an empty explicit match, fall
|
|
@@ -109,19 +147,19 @@ static voxgig_value* test_fetch(voxgig_value* entity, Context* ctx, const char*
|
|
|
109
147
|
voxgig_value* found = voxgig_select(entmap, args);
|
|
110
148
|
voxgig_value* ent = voxgig_getelem(found, v_int(0), NULL);
|
|
111
149
|
if (v_is_noval(ent) || v_is_null(ent)) {
|
|
112
|
-
voxgig_value* r = respond(404, v_undef());
|
|
150
|
+
voxgig_value* r = respond(ctx, 404, v_undef());
|
|
113
151
|
setp(r, "statusText", v_str("Not found"));
|
|
114
152
|
return r;
|
|
115
153
|
}
|
|
116
154
|
voxgig_delprop(ent, v_str("$KEY"));
|
|
117
|
-
return respond(200, voxgig_clone(ent));
|
|
155
|
+
return respond(ctx, 200, voxgig_clone(ent));
|
|
118
156
|
}
|
|
119
157
|
|
|
120
158
|
if (strcmp(opname, "list") == 0) {
|
|
121
159
|
voxgig_value* args = build_args(ctx, ctx->reqmatch);
|
|
122
160
|
voxgig_value* found = voxgig_select(entmap, args);
|
|
123
161
|
if (v_is_noval(found) || v_is_null(found)) {
|
|
124
|
-
voxgig_value* r = respond(404, v_undef());
|
|
162
|
+
voxgig_value* r = respond(ctx, 404, v_undef());
|
|
125
163
|
setp(r, "statusText", v_str("Not found"));
|
|
126
164
|
return r;
|
|
127
165
|
}
|
|
@@ -131,7 +169,7 @@ static voxgig_value* test_fetch(voxgig_value* entity, Context* ctx, const char*
|
|
|
131
169
|
voxgig_delprop(l->items[i], v_str("$KEY"));
|
|
132
170
|
}
|
|
133
171
|
}
|
|
134
|
-
return respond(200, voxgig_clone(found));
|
|
172
|
+
return respond(ctx, 200, voxgig_clone(found));
|
|
135
173
|
}
|
|
136
174
|
|
|
137
175
|
if (strcmp(opname, "update") == 0) {
|
|
@@ -168,7 +206,7 @@ static voxgig_value* test_fetch(voxgig_value* entity, Context* ctx, const char*
|
|
|
168
206
|
}
|
|
169
207
|
}
|
|
170
208
|
if (v_is_noval(ent) || v_is_null(ent)) {
|
|
171
|
-
voxgig_value* r = respond(404, v_undef());
|
|
209
|
+
voxgig_value* r = respond(ctx, 404, v_undef());
|
|
172
210
|
setp(r, "statusText", v_str("Not found"));
|
|
173
211
|
return r;
|
|
174
212
|
}
|
|
@@ -179,7 +217,7 @@ static voxgig_value* test_fetch(voxgig_value* entity, Context* ctx, const char*
|
|
|
179
217
|
}
|
|
180
218
|
}
|
|
181
219
|
voxgig_delprop(ent, v_str("$KEY"));
|
|
182
|
-
return respond(200, voxgig_clone(ent));
|
|
220
|
+
return respond(ctx, 200, voxgig_clone(ent));
|
|
183
221
|
}
|
|
184
222
|
|
|
185
223
|
if (strcmp(opname, "remove") == 0) {
|
|
@@ -192,7 +230,7 @@ static voxgig_value* test_fetch(voxgig_value* entity, Context* ctx, const char*
|
|
|
192
230
|
voxgig_value* id = getp(ent, "id");
|
|
193
231
|
voxgig_delprop(entmap, id);
|
|
194
232
|
}
|
|
195
|
-
return respond(200, v_undef());
|
|
233
|
+
return respond(ctx, 200, v_undef());
|
|
196
234
|
}
|
|
197
235
|
|
|
198
236
|
if (strcmp(opname, "create") == 0) {
|
|
@@ -216,12 +254,12 @@ static voxgig_value* test_fetch(voxgig_value* entity, Context* ctx, const char*
|
|
|
216
254
|
setp(entmap, voxgig_as_string(id), ent);
|
|
217
255
|
}
|
|
218
256
|
voxgig_delprop(ent, v_str("$KEY"));
|
|
219
|
-
return respond(200, voxgig_clone(ent));
|
|
257
|
+
return respond(ctx, 200, voxgig_clone(ent));
|
|
220
258
|
}
|
|
221
|
-
return respond(200, ent);
|
|
259
|
+
return respond(ctx, 200, ent);
|
|
222
260
|
}
|
|
223
261
|
|
|
224
|
-
voxgig_value* r = respond(404, v_undef());
|
|
262
|
+
voxgig_value* r = respond(ctx, 404, v_undef());
|
|
225
263
|
setp(r, "statusText", v_str("Unknown operation"));
|
|
226
264
|
return r;
|
|
227
265
|
}
|
|
@@ -57,11 +57,34 @@ public:
|
|
|
57
57
|
}
|
|
58
58
|
|
|
59
59
|
private:
|
|
60
|
-
|
|
60
|
+
// THE MOCK HAS TO AGREE WITH THE MODEL. A point carrying
|
|
61
|
+
// `transform.res: `body.item`` describes an API that answers {"item": {...}}
|
|
62
|
+
// and the response transform unwraps that key on the way back. Returning the
|
|
63
|
+
// bare payload means the transform unwraps a property that is not there and
|
|
64
|
+
// the caller gets nothing. Mirrors the go/ts/lua/php mocks.
|
|
65
|
+
Value envelope(CtxPtr ctx, const Value& data) {
|
|
66
|
+
if (is_nullish(data) || !ctx) return data;
|
|
67
|
+
Value tm = getp(ctx->point, "transform");
|
|
68
|
+
Value restf = getp(tm, "res");
|
|
69
|
+
if (!restf.is_string()) return data;
|
|
70
|
+
std::string spec = restf.as_string();
|
|
71
|
+
// Exactly `body.<key>`; a deeper path is not an envelope this mock can
|
|
72
|
+
// synthesise, so it is left alone rather than guessed at.
|
|
73
|
+
if (spec.size() < 8) return data;
|
|
74
|
+
if (0 != spec.compare(0, 6, "`body.")) return data;
|
|
75
|
+
if ('`' != spec[spec.size() - 1]) return data;
|
|
76
|
+
std::string inner = spec.substr(6, spec.size() - 7);
|
|
77
|
+
if (inner.empty() || std::string::npos != inner.find('.')) return data;
|
|
78
|
+
Value wrapped = vmap();
|
|
79
|
+
map_put(wrapped, inner, data);
|
|
80
|
+
return wrapped;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
Value respond(CtxPtr ctx, int status, const Value& data, const Value& extra) {
|
|
61
84
|
Value out = vmap();
|
|
62
85
|
map_put(out, "status", Value(status));
|
|
63
86
|
map_put(out, "statusText", Value("OK"));
|
|
64
|
-
map_put(out, "json", json_thunk(data));
|
|
87
|
+
map_put(out, "json", json_thunk(envelope(ctx, data)));
|
|
65
88
|
map_put(out, "body", Value("not-used"));
|
|
66
89
|
if (extra.is_map()) {
|
|
67
90
|
for (const auto& kv : *extra.as_map()) map_put(out, kv.first, kv.second);
|
|
@@ -100,10 +123,10 @@ private:
|
|
|
100
123
|
Value args = buildArgs(ctx, op, resolveMatch(ctx, ctx->reqmatch));
|
|
101
124
|
std::vector<Value> found = Struct::select(entmap, args);
|
|
102
125
|
Value ent = found.empty() ? Value::undef() : found[0];
|
|
103
|
-
if (is_nullish(ent)) return respond(404, Value(nullptr), extra1("statusText", Value("Not found")));
|
|
126
|
+
if (is_nullish(ent)) return respond(ctx, 404, Value(nullptr), extra1("statusText", Value("Not found")));
|
|
104
127
|
Struct::delprop(ent, Value("$KEY"));
|
|
105
128
|
Value out = Struct::clone(ent);
|
|
106
|
-
return respond(200, out, Value::undef());
|
|
129
|
+
return respond(ctx, 200, out, Value::undef());
|
|
107
130
|
} else if (op->name == "list") {
|
|
108
131
|
Value args = buildArgs(ctx, op, ctx->reqmatch);
|
|
109
132
|
std::vector<Value> found = Struct::select(entmap, args);
|
|
@@ -113,7 +136,7 @@ private:
|
|
|
113
136
|
outlist.as_list()->push_back(item);
|
|
114
137
|
}
|
|
115
138
|
Value out = Struct::clone(outlist);
|
|
116
|
-
return respond(200, out, Value::undef());
|
|
139
|
+
return respond(ctx, 200, out, Value::undef());
|
|
117
140
|
} else if (op->name == "update") {
|
|
118
141
|
Value updateMatch = vmap();
|
|
119
142
|
if (ctx->reqdata.is_map()) {
|
|
@@ -141,13 +164,13 @@ private:
|
|
|
141
164
|
if (kv.second.is_map()) { ent = kv.second; break; }
|
|
142
165
|
}
|
|
143
166
|
}
|
|
144
|
-
if (is_nullish(ent)) return respond(404, Value(nullptr), extra1("statusText", Value("Not found")));
|
|
167
|
+
if (is_nullish(ent)) return respond(ctx, 404, Value(nullptr), extra1("statusText", Value("Not found")));
|
|
145
168
|
if (ent.is_map() && ctx->reqdata.is_map()) {
|
|
146
169
|
for (const auto& kv : *ctx->reqdata.as_map()) map_put(ent, kv.first, kv.second);
|
|
147
170
|
}
|
|
148
171
|
Struct::delprop(ent, Value("$KEY"));
|
|
149
172
|
Value out = Struct::clone(ent);
|
|
150
|
-
return respond(200, out, Value::undef());
|
|
173
|
+
return respond(ctx, 200, out, Value::undef());
|
|
151
174
|
} else if (op->name == "remove") {
|
|
152
175
|
Value args = buildArgs(ctx, op, resolveMatch(ctx, ctx->reqmatch));
|
|
153
176
|
std::vector<Value> found = Struct::select(entmap, args);
|
|
@@ -156,7 +179,7 @@ private:
|
|
|
156
179
|
Value id = getp(ent, "id", Value(nullptr));
|
|
157
180
|
Struct::delprop(entmap, id);
|
|
158
181
|
}
|
|
159
|
-
return respond(200, Value(nullptr), Value::undef());
|
|
182
|
+
return respond(ctx, 200, Value(nullptr), Value::undef());
|
|
160
183
|
} else if (op->name == "create") {
|
|
161
184
|
buildArgs(ctx, op, ctx->reqdata);
|
|
162
185
|
Value id = ctx->utility->param(ctx, Value("id"));
|
|
@@ -177,12 +200,12 @@ private:
|
|
|
177
200
|
if (id.is_string()) map_put(entmap, id.as_string(), ent);
|
|
178
201
|
Struct::delprop(ent, Value("$KEY"));
|
|
179
202
|
Value out = Struct::clone(ent);
|
|
180
|
-
return respond(200, out, Value::undef());
|
|
203
|
+
return respond(ctx, 200, out, Value::undef());
|
|
181
204
|
}
|
|
182
|
-
return respond(200, ent, Value::undef());
|
|
205
|
+
return respond(ctx, 200, ent, Value::undef());
|
|
183
206
|
}
|
|
184
207
|
|
|
185
|
-
return respond(404, Value(nullptr), extra1("statusText", Value("Unknown operation")));
|
|
208
|
+
return respond(ctx, 404, Value(nullptr), extra1("statusText", Value("Unknown operation")));
|
|
186
209
|
}
|
|
187
210
|
|
|
188
211
|
// ---- net simulation over the mock -----------------------------------
|
|
@@ -45,14 +45,40 @@ public class TestFeature : BaseFeature
|
|
|
45
45
|
|
|
46
46
|
FetcherFunc testFetcher = (ctx2, _fullurl, _fetchdef) =>
|
|
47
47
|
{
|
|
48
|
-
|
|
48
|
+
// THE MOCK HAS TO AGREE WITH THE MODEL. A point carrying
|
|
49
|
+
// `transform.res: `body.item`` describes an API that answers
|
|
50
|
+
// {"item": {...}}, and the response transform unwraps that key on
|
|
51
|
+
// the way back. Returning the bare payload means the transform
|
|
52
|
+
// unwraps a property that is not there and the caller gets
|
|
53
|
+
// nothing. Mirrors the go/ts/lua/php mocks.
|
|
54
|
+
//
|
|
55
|
+
// NOT `static`: it closes over ctx2 to read the point, which is
|
|
56
|
+
// how go's mock does it too.
|
|
57
|
+
object? Envelope(object? data)
|
|
58
|
+
{
|
|
59
|
+
if (data == null || ctx2.Point == null) { return data; }
|
|
60
|
+
var tm = StructUtils.GetProp(ctx2.Point, "transform");
|
|
61
|
+
if (StructUtils.GetProp(tm, "res") is not string spec) { return data; }
|
|
62
|
+
// Exactly `body.<key>`; a deeper path is not an envelope this
|
|
63
|
+
// mock can synthesise, so it is left alone.
|
|
64
|
+
if (!spec.StartsWith("`body.") || !spec.EndsWith("`") || spec.Length < 8)
|
|
65
|
+
{
|
|
66
|
+
return data;
|
|
67
|
+
}
|
|
68
|
+
var inner = spec.Substring(6, spec.Length - 7);
|
|
69
|
+
if (inner.Length == 0 || inner.Contains('.')) { return data; }
|
|
70
|
+
return new Dictionary<string, object?> { [inner] = data };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
Dictionary<string, object?> Respond(int status, object? data,
|
|
49
74
|
Dictionary<string, object?>? extra)
|
|
50
75
|
{
|
|
76
|
+
var payload = Envelope(data);
|
|
51
77
|
var res = new Dictionary<string, object?>
|
|
52
78
|
{
|
|
53
79
|
["status"] = status,
|
|
54
80
|
["statusText"] = "OK",
|
|
55
|
-
["json"] = (Func<object?>)(() =>
|
|
81
|
+
["json"] = (Func<object?>)(() => payload),
|
|
56
82
|
["body"] = "not-used",
|
|
57
83
|
};
|
|
58
84
|
if (extra != null)
|
|
@@ -47,12 +47,39 @@ defmodule ProjectName.Feature.Test do
|
|
|
47
47
|
nil
|
|
48
48
|
end
|
|
49
49
|
|
|
50
|
-
|
|
50
|
+
# THE MOCK HAS TO AGREE WITH THE MODEL. A point carrying
|
|
51
|
+
# `transform.res: `body.item`` describes an API that answers {"item": {...}}
|
|
52
|
+
# and the response transform unwraps that key on the way back. Returning the
|
|
53
|
+
# bare payload means the transform unwraps a property that is not there and
|
|
54
|
+
# the caller gets nothing. Mirrors the go/ts/lua/php mocks.
|
|
55
|
+
defp envelope(fctx, data) do
|
|
56
|
+
spec = S.getprop(S.getprop(S.getprop(fctx, "point"), "transform"), "res")
|
|
57
|
+
|
|
58
|
+
# Exactly `body.<key>`; a deeper path is not an envelope this mock can
|
|
59
|
+
# synthesise, so it is left alone rather than guessed at.
|
|
60
|
+
case {data, spec} do
|
|
61
|
+
{nil, _} ->
|
|
62
|
+
data
|
|
63
|
+
|
|
64
|
+
{_, s} when is_binary(s) ->
|
|
65
|
+
case Regex.run(~r/^`body\.([^`.]+)`$/, s) do
|
|
66
|
+
[_, inner] -> S.jm([inner, data])
|
|
67
|
+
_ -> data
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
_ ->
|
|
71
|
+
data
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
defp respond(fctx, status, data, extra \\ nil) do
|
|
76
|
+
payload = envelope(fctx, data)
|
|
77
|
+
|
|
51
78
|
out =
|
|
52
79
|
S.jm([
|
|
53
80
|
"status", status,
|
|
54
81
|
"statusText", "OK",
|
|
55
|
-
"json", fn ->
|
|
82
|
+
"json", fn -> payload end,
|
|
56
83
|
"body", "not-used"
|
|
57
84
|
])
|
|
58
85
|
|
|
@@ -87,10 +114,10 @@ defmodule ProjectName.Feature.Test do
|
|
|
87
114
|
ent = S.getelem(found, 0)
|
|
88
115
|
|
|
89
116
|
if ent == nil do
|
|
90
|
-
respond(404, nil, S.jm(["statusText", "Not found"]))
|
|
117
|
+
respond(fctx, 404, nil, S.jm(["statusText", "Not found"]))
|
|
91
118
|
else
|
|
92
119
|
S.delprop(ent, "$KEY")
|
|
93
|
-
respond(200, S.clone(ent))
|
|
120
|
+
respond(fctx, 200, S.clone(ent))
|
|
94
121
|
end
|
|
95
122
|
|
|
96
123
|
"list" ->
|
|
@@ -98,13 +125,13 @@ defmodule ProjectName.Feature.Test do
|
|
|
98
125
|
found = S.select(entmap, args)
|
|
99
126
|
|
|
100
127
|
if found == nil do
|
|
101
|
-
respond(404, nil, S.jm(["statusText", "Not found"]))
|
|
128
|
+
respond(fctx, 404, nil, S.jm(["statusText", "Not found"]))
|
|
102
129
|
else
|
|
103
130
|
if S.islist(found) and S.size(found) > 0 do
|
|
104
131
|
Enum.each(0..(S.size(found) - 1), fn i -> S.delprop(S.getelem(found, i), "$KEY") end)
|
|
105
132
|
end
|
|
106
133
|
|
|
107
|
-
respond(200, S.clone(found))
|
|
134
|
+
respond(fctx, 200, S.clone(found))
|
|
108
135
|
end
|
|
109
136
|
|
|
110
137
|
"update" ->
|
|
@@ -143,14 +170,14 @@ defmodule ProjectName.Feature.Test do
|
|
|
143
170
|
end
|
|
144
171
|
|
|
145
172
|
if ent == nil do
|
|
146
|
-
respond(404, nil, S.jm(["statusText", "Not found"]))
|
|
173
|
+
respond(fctx, 404, nil, S.jm(["statusText", "Not found"]))
|
|
147
174
|
else
|
|
148
175
|
if S.ismap(ent) and reqdata != nil do
|
|
149
176
|
Enum.each(H.entries(reqdata), fn {k, v} -> S.setprop(ent, k, v) end)
|
|
150
177
|
end
|
|
151
178
|
|
|
152
179
|
S.delprop(ent, "$KEY")
|
|
153
|
-
respond(200, S.clone(ent))
|
|
180
|
+
respond(fctx, 200, S.clone(ent))
|
|
154
181
|
end
|
|
155
182
|
|
|
156
183
|
"remove" ->
|
|
@@ -163,7 +190,7 @@ defmodule ProjectName.Feature.Test do
|
|
|
163
190
|
S.delprop(entmap, eid)
|
|
164
191
|
end
|
|
165
192
|
|
|
166
|
-
respond(200, nil)
|
|
193
|
+
respond(fctx, 200, nil)
|
|
167
194
|
|
|
168
195
|
"create" ->
|
|
169
196
|
build_args(f, fctx, op, S.getprop(fctx, "reqdata"))
|
|
@@ -175,13 +202,13 @@ defmodule ProjectName.Feature.Test do
|
|
|
175
202
|
S.setprop(ent, "id", eid)
|
|
176
203
|
if is_binary(eid), do: S.setprop(entmap, eid, ent)
|
|
177
204
|
S.delprop(ent, "$KEY")
|
|
178
|
-
respond(200, S.clone(ent))
|
|
205
|
+
respond(fctx, 200, S.clone(ent))
|
|
179
206
|
else
|
|
180
|
-
respond(200, ent)
|
|
207
|
+
respond(fctx, 200, ent)
|
|
181
208
|
end
|
|
182
209
|
|
|
183
210
|
_ ->
|
|
184
|
-
respond(404, nil, S.jm(["statusText", "Unknown operation"]))
|
|
211
|
+
respond(fctx, 404, nil, S.jm(["statusText", "Unknown operation"]))
|
|
185
212
|
end
|
|
186
213
|
end
|
|
187
214
|
|
|
@@ -90,11 +90,45 @@ public class TestFeature extends BaseFeature {
|
|
|
90
90
|
}
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
-
|
|
93
|
+
// THE MOCK HAS TO AGREE WITH THE MODEL.
|
|
94
|
+
//
|
|
95
|
+
// A point carrying `transform.res: `body.item`` describes an API that
|
|
96
|
+
// answers {"item": {...}}, and the response transform unwraps that key on
|
|
97
|
+
// the way back. Handing back the bare payload means the transform unwraps a
|
|
98
|
+
// property that is not there and the caller gets nothing — a mock that only
|
|
99
|
+
// ever simulates APIs whose responses happen to be unwrapped.
|
|
100
|
+
//
|
|
101
|
+
// Mirrors the go/ts/lua/php mocks, which already wrap.
|
|
102
|
+
private Object envelope(Context ctx, Object data) {
|
|
103
|
+
if (null == data || null == ctx || null == ctx.point) {
|
|
104
|
+
return data;
|
|
105
|
+
}
|
|
106
|
+
Object tm = Struct.getprop(ctx.point, "transform");
|
|
107
|
+
Object restf = Struct.getprop(tm, "res");
|
|
108
|
+
if (!(restf instanceof String)) {
|
|
109
|
+
return data;
|
|
110
|
+
}
|
|
111
|
+
String spec = (String) restf;
|
|
112
|
+
// Exactly `body.<key>`; a deeper path is not an envelope this mock can
|
|
113
|
+
// synthesise, so it is left alone rather than guessed at.
|
|
114
|
+
if (!spec.startsWith("`body.") || !spec.endsWith("`") || spec.length() < 8) {
|
|
115
|
+
return data;
|
|
116
|
+
}
|
|
117
|
+
String inner = spec.substring(6, spec.length() - 1);
|
|
118
|
+
if (inner.isEmpty() || inner.contains(".")) {
|
|
119
|
+
return data;
|
|
120
|
+
}
|
|
121
|
+
Map<String, Object> wrapped = new LinkedHashMap<>();
|
|
122
|
+
wrapped.put(inner, data);
|
|
123
|
+
return wrapped;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
private Map<String, Object> respond(Context ctx, int status, Object data, Map<String, Object> extra) {
|
|
127
|
+
Object payload = envelope(ctx, data);
|
|
94
128
|
Map<String, Object> out = new LinkedHashMap<>();
|
|
95
129
|
out.put("status", status);
|
|
96
130
|
out.put("statusText", "OK");
|
|
97
|
-
out.put("json", (Supplier<Object>) () ->
|
|
131
|
+
out.put("json", (Supplier<Object>) () -> payload);
|
|
98
132
|
out.put("body", "not-used");
|
|
99
133
|
if (extra != null) {
|
|
100
134
|
out.putAll(extra);
|
|
@@ -142,23 +176,23 @@ public class TestFeature extends BaseFeature {
|
|
|
142
176
|
List<Object> found = Struct.select(entmap, args);
|
|
143
177
|
Object ent = Struct.getelem(found, 0);
|
|
144
178
|
if (ent == null) {
|
|
145
|
-
return respond(404, null, extra("statusText", "Not found"));
|
|
179
|
+
return respond(ctx, 404, null, extra("statusText", "Not found"));
|
|
146
180
|
}
|
|
147
181
|
Struct.delprop(ent, "$KEY");
|
|
148
182
|
Object out = Struct.clone(ent);
|
|
149
|
-
return respond(200, out, null);
|
|
183
|
+
return respond(ctx, 200, out, null);
|
|
150
184
|
}
|
|
151
185
|
else if ("list".equals(op.name)) {
|
|
152
186
|
Object args = buildArgs(ctx, op, ctx.reqmatch);
|
|
153
187
|
List<Object> found = Struct.select(entmap, args);
|
|
154
188
|
if (found == null) {
|
|
155
|
-
return respond(404, null, extra("statusText", "Not found"));
|
|
189
|
+
return respond(ctx, 404, null, extra("statusText", "Not found"));
|
|
156
190
|
}
|
|
157
191
|
for (Object item : found) {
|
|
158
192
|
Struct.delprop(item, "$KEY");
|
|
159
193
|
}
|
|
160
194
|
Object out = Struct.clone(found);
|
|
161
|
-
return respond(200, out, null);
|
|
195
|
+
return respond(ctx, 200, out, null);
|
|
162
196
|
}
|
|
163
197
|
else if ("update".equals(op.name)) {
|
|
164
198
|
// Match the existing entity by id only (or its alias). Reqdata
|
|
@@ -198,14 +232,14 @@ public class TestFeature extends BaseFeature {
|
|
|
198
232
|
}
|
|
199
233
|
}
|
|
200
234
|
if (ent == null) {
|
|
201
|
-
return respond(404, null, extra("statusText", "Not found"));
|
|
235
|
+
return respond(ctx, 404, null, extra("statusText", "Not found"));
|
|
202
236
|
}
|
|
203
237
|
if (ent instanceof Map && ctx.reqdata != null) {
|
|
204
238
|
((Map<String, Object>) ent).putAll(ctx.reqdata);
|
|
205
239
|
}
|
|
206
240
|
Struct.delprop(ent, "$KEY");
|
|
207
241
|
Object out = Struct.clone(ent);
|
|
208
|
-
return respond(200, out, null);
|
|
242
|
+
return respond(ctx, 200, out, null);
|
|
209
243
|
}
|
|
210
244
|
else if ("remove".equals(op.name)) {
|
|
211
245
|
Object args = buildArgs(ctx, op, resolveMatch(ctx, ctx.reqmatch));
|
|
@@ -217,7 +251,7 @@ public class TestFeature extends BaseFeature {
|
|
|
217
251
|
Object id = Struct.getprop(ent, "id", null);
|
|
218
252
|
Struct.delprop(entmap, id);
|
|
219
253
|
}
|
|
220
|
-
return respond(200, null, null);
|
|
254
|
+
return respond(ctx, 200, null, null);
|
|
221
255
|
}
|
|
222
256
|
else if ("create".equals(op.name)) {
|
|
223
257
|
buildArgs(ctx, op, ctx.reqdata);
|
|
@@ -238,12 +272,12 @@ public class TestFeature extends BaseFeature {
|
|
|
238
272
|
}
|
|
239
273
|
Struct.delprop(entm, "$KEY");
|
|
240
274
|
Object out = Struct.clone(entm);
|
|
241
|
-
return respond(200, out, null);
|
|
275
|
+
return respond(ctx, 200, out, null);
|
|
242
276
|
}
|
|
243
|
-
return respond(200, ent, null);
|
|
277
|
+
return respond(ctx, 200, ent, null);
|
|
244
278
|
}
|
|
245
279
|
|
|
246
|
-
return respond(404, null, extra("statusText", "Unknown operation"));
|
|
280
|
+
return respond(ctx, 404, null, extra("statusText", "Unknown operation"));
|
|
247
281
|
}
|
|
248
282
|
|
|
249
283
|
// makeNetsim wraps a transport with simulated network conditions: latency
|
|
@@ -54,11 +54,29 @@ class TestFeature : BaseFeature("test", "0.0.1", true) {
|
|
|
54
54
|
}
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
-
|
|
57
|
+
// THE MOCK HAS TO AGREE WITH THE MODEL. A point carrying
|
|
58
|
+
// `transform.res: `body.item`` describes an API that answers {"item": {...}}
|
|
59
|
+
// and the response transform unwraps that key on the way back. Returning the
|
|
60
|
+
// bare payload means the transform unwraps a property that is not there and
|
|
61
|
+
// the caller gets nothing. Mirrors the go/ts/lua/php mocks.
|
|
62
|
+
private fun envelope(ctx: Context?, data: Any?): Any? {
|
|
63
|
+
if (null == data || null == ctx) return data
|
|
64
|
+
val tm = Struct.getprop(ctx.point, "transform")
|
|
65
|
+
val restf = Struct.getprop(tm, "res") as? String ?: return data
|
|
66
|
+
// Exactly `body.<key>`; a deeper path is not an envelope this mock can
|
|
67
|
+
// synthesise, so it is left alone rather than guessed at.
|
|
68
|
+
if (!restf.startsWith("`body.") || !restf.endsWith("`") || restf.length < 8) return data
|
|
69
|
+
val inner = restf.substring(6, restf.length - 1)
|
|
70
|
+
if (inner.isEmpty() || inner.contains(".")) return data
|
|
71
|
+
return linkedMapOf<String, Any?>(inner to data)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
private fun respond(ctx: Context?, status: Int, data: Any?, extra: MutableMap<String, Any?>?): MutableMap<String, Any?> {
|
|
75
|
+
val payload = envelope(ctx, data)
|
|
58
76
|
val out = linkedMapOf<String, Any?>()
|
|
59
77
|
out["status"] = status
|
|
60
78
|
out["statusText"] = "OK"
|
|
61
|
-
out["json"] = Supplier<Any?> {
|
|
79
|
+
out["json"] = Supplier<Any?> { payload }
|
|
62
80
|
out["body"] = "not-used"
|
|
63
81
|
if (extra != null) {
|
|
64
82
|
out.putAll(extra)
|
|
@@ -102,11 +120,11 @@ class TestFeature : BaseFeature("test", "0.0.1", true) {
|
|
|
102
120
|
val found = Struct.select(entmap, args)
|
|
103
121
|
val ent = Struct.getelem(found, 0, null)
|
|
104
122
|
if (ent == null) {
|
|
105
|
-
return respond(404, null, extra("statusText", "Not found"))
|
|
123
|
+
return respond(ctx, 404, null, extra("statusText", "Not found"))
|
|
106
124
|
}
|
|
107
125
|
Struct.delprop(ent, "\$KEY")
|
|
108
126
|
val out = Struct.clone(ent)
|
|
109
|
-
return respond(200, out, null)
|
|
127
|
+
return respond(ctx, 200, out, null)
|
|
110
128
|
}
|
|
111
129
|
"list" -> {
|
|
112
130
|
val args = buildArgs(ctx, op, ctx.reqmatch)
|
|
@@ -115,7 +133,7 @@ class TestFeature : BaseFeature("test", "0.0.1", true) {
|
|
|
115
133
|
Struct.delprop(item, "\$KEY")
|
|
116
134
|
}
|
|
117
135
|
val out = Struct.clone(found)
|
|
118
|
-
return respond(200, out, null)
|
|
136
|
+
return respond(ctx, 200, out, null)
|
|
119
137
|
}
|
|
120
138
|
"update" -> {
|
|
121
139
|
// Match the existing entity by id only (or its alias).
|
|
@@ -148,14 +166,14 @@ class TestFeature : BaseFeature("test", "0.0.1", true) {
|
|
|
148
166
|
}
|
|
149
167
|
}
|
|
150
168
|
if (ent == null) {
|
|
151
|
-
return respond(404, null, extra("statusText", "Not found"))
|
|
169
|
+
return respond(ctx, 404, null, extra("statusText", "Not found"))
|
|
152
170
|
}
|
|
153
171
|
if (ent is MutableMap<*, *>) {
|
|
154
172
|
(ent as MutableMap<String, Any?>).putAll(reqdata)
|
|
155
173
|
}
|
|
156
174
|
Struct.delprop(ent, "\$KEY")
|
|
157
175
|
val out = Struct.clone(ent)
|
|
158
|
-
return respond(200, out, null)
|
|
176
|
+
return respond(ctx, 200, out, null)
|
|
159
177
|
}
|
|
160
178
|
"remove" -> {
|
|
161
179
|
val args = buildArgs(ctx, op, resolveMatch(ctx, ctx.reqmatch))
|
|
@@ -166,7 +184,7 @@ class TestFeature : BaseFeature("test", "0.0.1", true) {
|
|
|
166
184
|
val id = Struct.getprop(ent, "id", null)
|
|
167
185
|
Struct.delprop(entmap, id)
|
|
168
186
|
}
|
|
169
|
-
return respond(200, null, null)
|
|
187
|
+
return respond(ctx, 200, null, null)
|
|
170
188
|
}
|
|
171
189
|
"create" -> {
|
|
172
190
|
buildArgs(ctx, op, ctx.reqdata)
|
|
@@ -189,13 +207,13 @@ class TestFeature : BaseFeature("test", "0.0.1", true) {
|
|
|
189
207
|
}
|
|
190
208
|
Struct.delprop(entm, "\$KEY")
|
|
191
209
|
val out = Struct.clone(entm)
|
|
192
|
-
return respond(200, out, null)
|
|
210
|
+
return respond(ctx, 200, out, null)
|
|
193
211
|
}
|
|
194
|
-
return respond(200, ent, null)
|
|
212
|
+
return respond(ctx, 200, ent, null)
|
|
195
213
|
}
|
|
196
214
|
}
|
|
197
215
|
|
|
198
|
-
return respond(404, null, extra("statusText", "Unknown operation"))
|
|
216
|
+
return respond(ctx, 404, null, extra("statusText", "Unknown operation"))
|
|
199
217
|
}
|
|
200
218
|
|
|
201
219
|
// makeNetsim wraps a transport with simulated network conditions.
|
|
@@ -51,12 +51,34 @@ sub init {
|
|
|
51
51
|
|
|
52
52
|
my $test_self = $self;
|
|
53
53
|
|
|
54
|
+
# THE MOCK HAS TO AGREE WITH THE MODEL. A point carrying
|
|
55
|
+
# `transform.res: `body.item`` describes an API that answers {"item": {...}}
|
|
56
|
+
# and the response transform unwraps that key on the way back. Returning the
|
|
57
|
+
# bare payload means the transform unwraps a property that is not there and
|
|
58
|
+
# the caller gets nothing. Mirrors the go/ts/lua/php mocks.
|
|
59
|
+
#
|
|
60
|
+
# Takes the PER-REQUEST context: the point is resolved per call, so the
|
|
61
|
+
# init-time $ctx this closure would otherwise capture is the wrong one.
|
|
62
|
+
my $envelope = sub {
|
|
63
|
+
my ($fctx, $data) = @_;
|
|
64
|
+
return $data unless defined $data;
|
|
65
|
+
return $data unless defined $fctx && defined $fctx->{point};
|
|
66
|
+
my $tm = ProjectNameHelpers::gp($fctx->{point}, 'transform');
|
|
67
|
+
my $spec = ProjectNameHelpers::gp($tm, 'res');
|
|
68
|
+
return $data unless defined $spec && !ref($spec);
|
|
69
|
+
# Exactly `body.<key>`; a deeper path is not an envelope this mock can
|
|
70
|
+
# synthesise, so it is left alone rather than guessed at.
|
|
71
|
+
return $data unless $spec =~ /^`body\.([^`.]+)`$/;
|
|
72
|
+
return { $1 => $data };
|
|
73
|
+
};
|
|
74
|
+
|
|
54
75
|
my $respond = sub {
|
|
55
|
-
my ($status, $data, $extra) = @_;
|
|
76
|
+
my ($fctx, $status, $data, $extra) = @_;
|
|
77
|
+
my $payload = $envelope->($fctx, $data);
|
|
56
78
|
my $out = {
|
|
57
79
|
'status' => $status,
|
|
58
80
|
'statusText' => 'OK',
|
|
59
|
-
'json' => sub { $
|
|
81
|
+
'json' => sub { $payload },
|
|
60
82
|
'body' => 'not-used',
|
|
61
83
|
};
|
|
62
84
|
if (Voxgig::Struct::ismap($extra)) {
|
|
@@ -92,22 +114,22 @@ sub init {
|
|
|
92
114
|
my $args = $test_self->build_args($fctx, $op, $resolve_match->($fctx->{reqmatch}));
|
|
93
115
|
my $found = Voxgig::Struct::select($entmap, $args);
|
|
94
116
|
my $ent = ProjectNameHelpers::ge($found, 0);
|
|
95
|
-
return $respond->(404, undef, { 'statusText' => 'Not found' })
|
|
117
|
+
return $respond->($fctx, 404, undef, { 'statusText' => 'Not found' })
|
|
96
118
|
unless ProjectNameHelpers::rb_truthy($ent);
|
|
97
119
|
Voxgig::Struct::delprop($ent, '$KEY');
|
|
98
120
|
my $out = Voxgig::Struct::clone($ent);
|
|
99
|
-
return $respond->(200, $out, undef);
|
|
121
|
+
return $respond->($fctx, 200, $out, undef);
|
|
100
122
|
}
|
|
101
123
|
elsif ('list' eq $op->{name}) {
|
|
102
124
|
my $args = $test_self->build_args($fctx, $op, $fctx->{reqmatch});
|
|
103
125
|
my $found = Voxgig::Struct::select($entmap, $args);
|
|
104
|
-
return $respond->(404, undef, { 'statusText' => 'Not found' })
|
|
126
|
+
return $respond->($fctx, 404, undef, { 'statusText' => 'Not found' })
|
|
105
127
|
unless defined $found && !Voxgig::Struct::is_none($found);
|
|
106
128
|
if (Voxgig::Struct::islist($found)) {
|
|
107
129
|
Voxgig::Struct::delprop($_, '$KEY') for @$found;
|
|
108
130
|
}
|
|
109
131
|
my $out = Voxgig::Struct::clone($found);
|
|
110
|
-
return $respond->(200, $out, undef);
|
|
132
|
+
return $respond->($fctx, 200, $out, undef);
|
|
111
133
|
}
|
|
112
134
|
elsif ('update' eq $op->{name}) {
|
|
113
135
|
# Match the existing entity by id only (or its alias). reqdata also
|
|
@@ -138,14 +160,14 @@ sub init {
|
|
|
138
160
|
}
|
|
139
161
|
}
|
|
140
162
|
}
|
|
141
|
-
return $respond->(404, undef, { 'statusText' => 'Not found' })
|
|
163
|
+
return $respond->($fctx, 404, undef, { 'statusText' => 'Not found' })
|
|
142
164
|
unless ProjectNameHelpers::rb_truthy($ent);
|
|
143
165
|
if (Voxgig::Struct::ismap($ent) && $fctx->{reqdata}) {
|
|
144
166
|
$ent->{$_} = $fctx->{reqdata}{$_} for keys %{ $fctx->{reqdata} };
|
|
145
167
|
}
|
|
146
168
|
Voxgig::Struct::delprop($ent, '$KEY');
|
|
147
169
|
my $out = Voxgig::Struct::clone($ent);
|
|
148
|
-
return $respond->(200, $out, undef);
|
|
170
|
+
return $respond->($fctx, 200, $out, undef);
|
|
149
171
|
}
|
|
150
172
|
elsif ('remove' eq $op->{name}) {
|
|
151
173
|
my $args = $test_self->build_args($fctx, $op, $resolve_match->($fctx->{reqmatch}));
|
|
@@ -157,7 +179,7 @@ sub init {
|
|
|
157
179
|
my $id = ProjectNameHelpers::gp($ent, 'id');
|
|
158
180
|
Voxgig::Struct::delprop($entmap, $id);
|
|
159
181
|
}
|
|
160
|
-
return $respond->(200, undef, undef);
|
|
182
|
+
return $respond->($fctx, 200, undef, undef);
|
|
161
183
|
}
|
|
162
184
|
elsif ('create' eq $op->{name}) {
|
|
163
185
|
$test_self->build_args($fctx, $op, $fctx->{reqdata});
|
|
@@ -172,12 +194,12 @@ sub init {
|
|
|
172
194
|
$entmap->{"$id"} = $ent if defined $id && !ref $id;
|
|
173
195
|
Voxgig::Struct::delprop($ent, '$KEY');
|
|
174
196
|
my $out = Voxgig::Struct::clone($ent);
|
|
175
|
-
return $respond->(200, $out, undef);
|
|
197
|
+
return $respond->($fctx, 200, $out, undef);
|
|
176
198
|
}
|
|
177
|
-
return $respond->(200, $ent, undef);
|
|
199
|
+
return $respond->($fctx, 200, $ent, undef);
|
|
178
200
|
}
|
|
179
201
|
else {
|
|
180
|
-
return $respond->(404, undef, { 'statusText' => 'Unknown operation' });
|
|
202
|
+
return $respond->($fctx, 404, undef, { 'statusText' => 'Unknown operation' });
|
|
181
203
|
}
|
|
182
204
|
};
|
|
183
205
|
|
|
@@ -33,11 +33,40 @@ impl TestFeature {
|
|
|
33
33
|
}
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
|
|
36
|
+
// THE MOCK HAS TO AGREE WITH THE MODEL.
|
|
37
|
+
//
|
|
38
|
+
// A point carrying `transform.res: `body.item`` describes an API that answers
|
|
39
|
+
// {"item": {...}}, and the response transform unwraps that key on the way
|
|
40
|
+
// back. Handing back the bare payload means the transform unwraps a property
|
|
41
|
+
// that is not there, and the caller gets nothing — a mock that only ever
|
|
42
|
+
// simulates APIs whose responses happen to be unwrapped.
|
|
43
|
+
//
|
|
44
|
+
// univec's list op declares `body.data`, so every list returned zero items
|
|
45
|
+
// while the fixture plainly held two. Mirrors the go/ts/lua/php mocks, which
|
|
46
|
+
// already wrap; rust, c and zig were the three that did not.
|
|
47
|
+
fn envelope(ctx: &Rc<Context>, data: Value) -> Value {
|
|
48
|
+
if data.is_noval() || data.is_null() {
|
|
49
|
+
return data;
|
|
50
|
+
}
|
|
51
|
+
let restf = crate::core::helpers::getpath(&["transform", "res"], &ctx.point.borrow());
|
|
52
|
+
if let Value::Str(spec) = restf {
|
|
53
|
+
// Exactly `body.<key>` — a deeper path is not an envelope this mock
|
|
54
|
+
// can synthesise, so it is left alone rather than guessed at.
|
|
55
|
+
if let Some(inner) = spec.strip_prefix("`body.").and_then(|r| r.strip_suffix('`')) {
|
|
56
|
+
if !inner.is_empty() && !inner.contains('.') {
|
|
57
|
+
return jo(vec![(inner, data)]);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
data
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
fn respond(ctx: &Rc<Context>, status: i64, data: Value, extra: Vec<(&str, Value)>) -> Value {
|
|
65
|
+
let payload = envelope(ctx, data);
|
|
37
66
|
let out = jo(vec![
|
|
38
67
|
("status", Value::Num(status as f64)),
|
|
39
68
|
("statusText", Value::str("OK")),
|
|
40
|
-
("json", json_thunk(
|
|
69
|
+
("json", json_thunk(payload)),
|
|
41
70
|
("body", Value::str("not-used")),
|
|
42
71
|
]);
|
|
43
72
|
for (k, v) in extra {
|
|
@@ -150,22 +179,20 @@ fn test_fetch(
|
|
|
150
179
|
let found = vs::select(&entmap, &args);
|
|
151
180
|
let ent = vs::get_elem(&found, &Value::Num(0.0), Value::Noval);
|
|
152
181
|
if ent.is_noval() || ent.is_null() {
|
|
153
|
-
return Ok(respond(
|
|
154
|
-
404,
|
|
182
|
+
return Ok(respond(ctx, 404,
|
|
155
183
|
Value::Noval,
|
|
156
184
|
vec![("statusText", Value::str("Not found"))],
|
|
157
185
|
));
|
|
158
186
|
}
|
|
159
187
|
vs::del_prop(ent.clone(), &Value::str("$KEY"));
|
|
160
|
-
Ok(respond(200, vs::clone(&ent), vec![]))
|
|
188
|
+
Ok(respond(ctx, 200, vs::clone(&ent), vec![]))
|
|
161
189
|
}
|
|
162
190
|
|
|
163
191
|
"list" => {
|
|
164
192
|
let args = build_args(ctx, &ctx.reqmatch.borrow().clone());
|
|
165
193
|
let found = vs::select(&entmap, &args);
|
|
166
194
|
if found.is_noval() || found.is_null() {
|
|
167
|
-
return Ok(respond(
|
|
168
|
-
404,
|
|
195
|
+
return Ok(respond(ctx, 404,
|
|
169
196
|
Value::Noval,
|
|
170
197
|
vec![("statusText", Value::str("Not found"))],
|
|
171
198
|
));
|
|
@@ -175,7 +202,7 @@ fn test_fetch(
|
|
|
175
202
|
vs::del_prop(item.clone(), &Value::str("$KEY"));
|
|
176
203
|
}
|
|
177
204
|
}
|
|
178
|
-
Ok(respond(200, vs::clone(&found), vec![]))
|
|
205
|
+
Ok(respond(ctx, 200, vs::clone(&found), vec![]))
|
|
179
206
|
}
|
|
180
207
|
|
|
181
208
|
"update" => {
|
|
@@ -214,8 +241,7 @@ fn test_fetch(
|
|
|
214
241
|
}
|
|
215
242
|
}
|
|
216
243
|
if ent.is_noval() || ent.is_null() {
|
|
217
|
-
return Ok(respond(
|
|
218
|
-
404,
|
|
244
|
+
return Ok(respond(ctx, 404,
|
|
219
245
|
Value::Noval,
|
|
220
246
|
vec![("statusText", Value::str("Not found"))],
|
|
221
247
|
));
|
|
@@ -228,7 +254,7 @@ fn test_fetch(
|
|
|
228
254
|
}
|
|
229
255
|
}
|
|
230
256
|
vs::del_prop(ent.clone(), &Value::str("$KEY"));
|
|
231
|
-
Ok(respond(200, vs::clone(&ent), vec![]))
|
|
257
|
+
Ok(respond(ctx, 200, vs::clone(&ent), vec![]))
|
|
232
258
|
}
|
|
233
259
|
|
|
234
260
|
"remove" => {
|
|
@@ -242,7 +268,7 @@ fn test_fetch(
|
|
|
242
268
|
let id = getp(&ent, "id");
|
|
243
269
|
vs::del_prop(entmap, &id);
|
|
244
270
|
}
|
|
245
|
-
Ok(respond(200, Value::Noval, vec![]))
|
|
271
|
+
Ok(respond(ctx, 200, Value::Noval, vec![]))
|
|
246
272
|
}
|
|
247
273
|
|
|
248
274
|
"create" => {
|
|
@@ -265,13 +291,12 @@ fn test_fetch(
|
|
|
265
291
|
setp(&entmap, id_str, ent.clone());
|
|
266
292
|
}
|
|
267
293
|
vs::del_prop(ent.clone(), &Value::str("$KEY"));
|
|
268
|
-
return Ok(respond(200, vs::clone(&ent), vec![]));
|
|
294
|
+
return Ok(respond(ctx, 200, vs::clone(&ent), vec![]));
|
|
269
295
|
}
|
|
270
|
-
Ok(respond(200, ent, vec![]))
|
|
296
|
+
Ok(respond(ctx, 200, ent, vec![]))
|
|
271
297
|
}
|
|
272
298
|
|
|
273
|
-
_ => Ok(respond(
|
|
274
|
-
404,
|
|
299
|
+
_ => Ok(respond(ctx, 404,
|
|
275
300
|
Value::Noval,
|
|
276
301
|
vec![("statusText", Value::str("Unknown operation"))],
|
|
277
302
|
)),
|
|
@@ -40,8 +40,31 @@ class TestFeature extends BaseFeature("test", "0.0.1", true) {
|
|
|
40
40
|
else ctx.utility.fetcher = makeNetsim(net, testFetcher)
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
|
|
44
|
-
|
|
43
|
+
// THE MOCK HAS TO AGREE WITH THE MODEL. A point carrying
|
|
44
|
+
// `transform.res: `body.item`` describes an API that answers {"item": {...}}
|
|
45
|
+
// and the response transform unwraps that key on the way back. Returning the
|
|
46
|
+
// bare payload means the transform unwraps a property that is not there and
|
|
47
|
+
// the caller gets nothing. Mirrors the go/ts/lua/php mocks.
|
|
48
|
+
private def envelope(ctx: Context, data: Object): Object = {
|
|
49
|
+
if (data == null || ctx == null || ctx.point == null) return data
|
|
50
|
+
val tm = Struct.getprop(ctx.point, "transform")
|
|
51
|
+
Struct.getprop(tm, "res") match {
|
|
52
|
+
case spec: String =>
|
|
53
|
+
// Exactly `body.<key>`; a deeper path is not an envelope this mock
|
|
54
|
+
// can synthesise, so it is left alone rather than guessed at.
|
|
55
|
+
if (!spec.startsWith("`body.") || !spec.endsWith("`") || spec.length < 8) return data
|
|
56
|
+
val inner = spec.substring(6, spec.length - 1)
|
|
57
|
+
if (inner.isEmpty || inner.contains(".")) return data
|
|
58
|
+
val wrapped = new java.util.LinkedHashMap[String, Object]()
|
|
59
|
+
wrapped.put(inner, data)
|
|
60
|
+
wrapped
|
|
61
|
+
case _ => data
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
private def respond(ctx: Context, status: Int, data: Object, extra: JMap[String, Object]): JMap[String, Object] = {
|
|
66
|
+
val payload = envelope(ctx, data)
|
|
67
|
+
val js: Supplier[Object] = () => payload
|
|
45
68
|
val out = new LinkedHashMap[String, Object]()
|
|
46
69
|
out.put("status", java.lang.Integer.valueOf(status))
|
|
47
70
|
out.put("statusText", "OK")
|
|
@@ -85,18 +108,18 @@ class TestFeature extends BaseFeature("test", "0.0.1", true) {
|
|
|
85
108
|
val args = buildArgs(ctx, op, resolveMatch(ctx, ctx.reqmatch))
|
|
86
109
|
val found = Struct.select(entmap, args)
|
|
87
110
|
val ent = Struct.getelem(found, java.lang.Integer.valueOf(0))
|
|
88
|
-
if (ent == null) return respond(404, null, extra("statusText", "Not found"))
|
|
111
|
+
if (ent == null) return respond(ctx, 404, null, extra("statusText", "Not found"))
|
|
89
112
|
Struct.delprop(ent, "$KEY")
|
|
90
113
|
val out = Struct.clone(ent)
|
|
91
|
-
respond(200, out, null)
|
|
114
|
+
respond(ctx, 200, out, null)
|
|
92
115
|
} else if ("list" == op.name) {
|
|
93
116
|
val args = buildArgs(ctx, op, ctx.reqmatch)
|
|
94
117
|
val found = Struct.select(entmap, args)
|
|
95
|
-
if (found == null) return respond(404, null, extra("statusText", "Not found"))
|
|
118
|
+
if (found == null) return respond(ctx, 404, null, extra("statusText", "Not found"))
|
|
96
119
|
val it = found.iterator()
|
|
97
120
|
while (it.hasNext) Struct.delprop(it.next(), "$KEY")
|
|
98
121
|
val out = Struct.clone(found)
|
|
99
|
-
respond(200, out, null)
|
|
122
|
+
respond(ctx, 200, out, null)
|
|
100
123
|
} else if ("update" == op.name) {
|
|
101
124
|
var updateMatch = new LinkedHashMap[String, Object]()
|
|
102
125
|
if (ctx.reqdata != null) {
|
|
@@ -119,11 +142,11 @@ class TestFeature extends BaseFeature("test", "0.0.1", true) {
|
|
|
119
142
|
vit.next() match { case e: JMap[_, _] => ent = e; brk = true; case _ => }
|
|
120
143
|
}
|
|
121
144
|
}
|
|
122
|
-
if (ent == null) return respond(404, null, extra("statusText", "Not found"))
|
|
145
|
+
if (ent == null) return respond(ctx, 404, null, extra("statusText", "Not found"))
|
|
123
146
|
ent match { case m: JMap[_, _] if ctx.reqdata != null => m.asInstanceOf[JMap[String, Object]].putAll(ctx.reqdata); case _ => }
|
|
124
147
|
Struct.delprop(ent, "$KEY")
|
|
125
148
|
val out = Struct.clone(ent)
|
|
126
|
-
respond(200, out, null)
|
|
149
|
+
respond(ctx, 200, out, null)
|
|
127
150
|
} else if ("remove" == op.name) {
|
|
128
151
|
val args = buildArgs(ctx, op, resolveMatch(ctx, ctx.reqmatch))
|
|
129
152
|
val found = Struct.select(entmap, args)
|
|
@@ -134,7 +157,7 @@ class TestFeature extends BaseFeature("test", "0.0.1", true) {
|
|
|
134
157
|
Struct.delprop(entmap, id)
|
|
135
158
|
case _ =>
|
|
136
159
|
}
|
|
137
|
-
respond(200, null, null)
|
|
160
|
+
respond(ctx, 200, null, null)
|
|
138
161
|
} else if ("create" == op.name) {
|
|
139
162
|
buildArgs(ctx, op, ctx.reqdata)
|
|
140
163
|
var id = ctx.utility.param(ctx, "id")
|
|
@@ -153,11 +176,11 @@ class TestFeature extends BaseFeature("test", "0.0.1", true) {
|
|
|
153
176
|
id match { case s: String => entmap.put(s, entm); case _ => }
|
|
154
177
|
Struct.delprop(entm, "$KEY")
|
|
155
178
|
val out = Struct.clone(entm)
|
|
156
|
-
respond(200, out, null)
|
|
157
|
-
case _ => respond(200, ent, null)
|
|
179
|
+
respond(ctx, 200, out, null)
|
|
180
|
+
case _ => respond(ctx, 200, ent, null)
|
|
158
181
|
}
|
|
159
182
|
} else {
|
|
160
|
-
respond(404, null, extra("statusText", "Unknown operation"))
|
|
183
|
+
respond(ctx, 404, null, extra("statusText", "Unknown operation"))
|
|
161
184
|
}
|
|
162
185
|
}
|
|
163
186
|
|
|
@@ -5,11 +5,32 @@
|
|
|
5
5
|
|
|
6
6
|
import Foundation
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
// THE MOCK HAS TO AGREE WITH THE MODEL. A point carrying
|
|
9
|
+
// `transform.res: `body.item`` describes an API that answers {"item": {...}},
|
|
10
|
+
// and the response transform unwraps that key on the way back. Returning the
|
|
11
|
+
// bare payload means the transform unwraps a property that is not there and
|
|
12
|
+
// the caller gets nothing. Mirrors the go/ts/lua/php mocks.
|
|
13
|
+
private func testEnvelope(_ ctx: Context, _ data: Value) -> Value {
|
|
14
|
+
if case .noval = data { return data }
|
|
15
|
+
if case .null = data { return data }
|
|
16
|
+
guard let point = ctx.point else { return data }
|
|
17
|
+
guard case .map(let tm)? = point.entries["transform"] else { return data }
|
|
18
|
+
guard case .string(let spec)? = tm.entries["res"] else { return data }
|
|
19
|
+
// Exactly `body.<key>`; a deeper path is not an envelope this mock can
|
|
20
|
+
// synthesise, so it is left alone rather than guessed at.
|
|
21
|
+
guard spec.hasPrefix("`body."), spec.hasSuffix("`"), spec.count >= 8 else { return data }
|
|
22
|
+
let inner = String(spec.dropFirst(6).dropLast(1))
|
|
23
|
+
guard !inner.isEmpty, !inner.contains(".") else { return data }
|
|
24
|
+
let wrapped = VMap()
|
|
25
|
+
wrapped.entries[inner] = data
|
|
26
|
+
return .map(wrapped)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
private func testRespond(_ ctx: Context, _ status: Int, _ data: Value, _ extra: VMap?) -> Value {
|
|
9
30
|
let res = VMap()
|
|
10
31
|
res.entries["status"] = .int(Int64(status))
|
|
11
32
|
res.entries["statusText"] = .string("OK")
|
|
12
|
-
let captured = data
|
|
33
|
+
let captured = testEnvelope(ctx, data)
|
|
13
34
|
res.entries["json"] = .nat({ () -> Value in captured } as NativeCall0)
|
|
14
35
|
res.entries["body"] = .string("not-used")
|
|
15
36
|
if let extra = extra {
|
|
@@ -151,22 +172,22 @@ public final class TestFeature: BaseFeature {
|
|
|
151
172
|
if isNil(ent) {
|
|
152
173
|
let extra = VMap()
|
|
153
174
|
extra.entries["statusText"] = .string("Not found")
|
|
154
|
-
return testRespond(404, .noval, extra)
|
|
175
|
+
return testRespond(ctx, 404, .noval, extra)
|
|
155
176
|
}
|
|
156
177
|
delprop(ent, .string("$KEY"))
|
|
157
|
-
return testRespond(200, clone(ent), nil)
|
|
178
|
+
return testRespond(ctx, 200, clone(ent), nil)
|
|
158
179
|
} else if op.name == "list" {
|
|
159
180
|
let args = testBuildArgs(ctx2, op, ctx2.reqmatch)
|
|
160
181
|
let found = select(.map(entmap), args)
|
|
161
182
|
if isNil(found) {
|
|
162
183
|
let extra = VMap()
|
|
163
184
|
extra.entries["statusText"] = .string("Not found")
|
|
164
|
-
return testRespond(404, .noval, extra)
|
|
185
|
+
return testRespond(ctx, 404, .noval, extra)
|
|
165
186
|
}
|
|
166
187
|
if let fl = found.asList {
|
|
167
188
|
for item in fl.items { delprop(item, .string("$KEY")) }
|
|
168
189
|
}
|
|
169
|
-
return testRespond(200, clone(found), nil)
|
|
190
|
+
return testRespond(ctx, 200, clone(found), nil)
|
|
170
191
|
} else if op.name == "update" {
|
|
171
192
|
var updateMatch = VMap()
|
|
172
193
|
if let idv = ctx2.reqdata.entries["id"] {
|
|
@@ -188,13 +209,13 @@ public final class TestFeature: BaseFeature {
|
|
|
188
209
|
if isNil(ent) {
|
|
189
210
|
let extra = VMap()
|
|
190
211
|
extra.entries["statusText"] = .string("Not found")
|
|
191
|
-
return testRespond(404, .noval, extra)
|
|
212
|
+
return testRespond(ctx, 404, .noval, extra)
|
|
192
213
|
}
|
|
193
214
|
if let entm = ent.asMap {
|
|
194
215
|
for (k, v) in ctx2.reqdata.entries { entm.entries[k] = v }
|
|
195
216
|
}
|
|
196
217
|
delprop(ent, .string("$KEY"))
|
|
197
|
-
return testRespond(200, clone(ent), nil)
|
|
218
|
+
return testRespond(ctx, 200, clone(ent), nil)
|
|
198
219
|
} else if op.name == "remove" {
|
|
199
220
|
let args = testBuildArgs(ctx2, op, testResolveMatch(ctx2, ctx2.reqmatch))
|
|
200
221
|
let found = select(.map(entmap), args)
|
|
@@ -203,7 +224,7 @@ public final class TestFeature: BaseFeature {
|
|
|
203
224
|
let id = gp(entm2, "id")
|
|
204
225
|
delprop(.map(entmap), id)
|
|
205
226
|
}
|
|
206
|
-
return testRespond(200, .noval, nil)
|
|
227
|
+
return testRespond(ctx, 200, .noval, nil)
|
|
207
228
|
} else if op.name == "create" {
|
|
208
229
|
_ = testBuildArgs(ctx2, op, ctx2.reqdata)
|
|
209
230
|
var id = ctx2.utility!.param(ctx2, .string("id"))
|
|
@@ -218,14 +239,14 @@ public final class TestFeature: BaseFeature {
|
|
|
218
239
|
entm.entries["id"] = id
|
|
219
240
|
if let idStr = id.asString { entmap.entries[idStr] = .map(entm) }
|
|
220
241
|
delprop(.map(entm), .string("$KEY"))
|
|
221
|
-
return testRespond(200, clone(.map(entm)), nil)
|
|
242
|
+
return testRespond(ctx, 200, clone(.map(entm)), nil)
|
|
222
243
|
}
|
|
223
|
-
return testRespond(200, ent, nil)
|
|
244
|
+
return testRespond(ctx, 200, ent, nil)
|
|
224
245
|
}
|
|
225
246
|
|
|
226
247
|
let extra = VMap()
|
|
227
248
|
extra.entries["statusText"] = .string("Unknown operation")
|
|
228
|
-
return testRespond(404, .noval, extra)
|
|
249
|
+
return testRespond(ctx, 404, .noval, extra)
|
|
229
250
|
}
|
|
230
251
|
|
|
231
252
|
// Optional network behaviour simulation over the mock transport.
|
|
@@ -93,11 +93,38 @@ fn fixIds(_: Allocator, key: ?[]const u8, val: Value, _: Value, path: []const []
|
|
|
93
93
|
return val;
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
-
|
|
96
|
+
// THE MOCK HAS TO AGREE WITH THE MODEL.
|
|
97
|
+
//
|
|
98
|
+
// A point carrying `transform.res: `body.item`` describes an API that answers
|
|
99
|
+
// {"item": {...}}, and the response transform unwraps that key on the way
|
|
100
|
+
// back. Handing back the bare payload means the transform unwraps a property
|
|
101
|
+
// that is not there and the caller gets nothing — a mock that only ever
|
|
102
|
+
// simulates APIs whose responses happen to be unwrapped.
|
|
103
|
+
//
|
|
104
|
+
// univec's list op declares `body.data`, so every list returned zero items
|
|
105
|
+
// while the fixture plainly held two. Mirrors the go/ts/lua/php mocks, which
|
|
106
|
+
// already wrap; rust, c and zig were the three that did not.
|
|
107
|
+
fn envelope(ctx: *Context, data: Value) Value {
|
|
108
|
+
if (data == .undef or data == .null) return data;
|
|
109
|
+
const restf = h.getpath(&.{ "transform", "res" }, ctx.point);
|
|
110
|
+
if (restf != .string) return data;
|
|
111
|
+
const spec = restf.string;
|
|
112
|
+
// Exactly `body.<key>`; a deeper path is not an envelope this mock can
|
|
113
|
+
// synthesise, so it is left alone rather than guessed at.
|
|
114
|
+
if (spec.len < 8) return data;
|
|
115
|
+
if (!std.mem.startsWith(u8, spec, "`body.")) return data;
|
|
116
|
+
if (!std.mem.endsWith(u8, spec, "`")) return data;
|
|
117
|
+
const inner = spec[6 .. spec.len - 1];
|
|
118
|
+
if (inner.len == 0) return data;
|
|
119
|
+
if (std.mem.indexOfScalar(u8, inner, '.') != null) return data;
|
|
120
|
+
return h.jo(&.{.{ inner, data }});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
fn respond(ctx: *Context, status: i64, data: Value, extra: []const h.Pair) Value {
|
|
97
124
|
const out = h.jo(&.{
|
|
98
125
|
.{ "status", h.vnum(status) },
|
|
99
126
|
.{ "statusText", h.vstr("OK") },
|
|
100
|
-
.{ "json", h.json_thunk(data) },
|
|
127
|
+
.{ "json", h.json_thunk(envelope(ctx, data)) },
|
|
101
128
|
.{ "body", h.vstr("not-used") },
|
|
102
129
|
});
|
|
103
130
|
for (extra) |kv| h.setp(out, kv[0], kv[1]);
|
|
@@ -182,20 +209,20 @@ fn test_fetch(entity: Value, ctx: *Context, _: []const u8, _: Value) err.E!Value
|
|
|
182
209
|
const found = vs.select(h.A(), entmap, args) catch h.olist();
|
|
183
210
|
const ent = h.get_elem(found, h.vnum(0), h.vnull());
|
|
184
211
|
if (h.is_noval(ent)) {
|
|
185
|
-
return respond(404, h.vnull(), &.{.{ "statusText", h.vstr("Not found") }});
|
|
212
|
+
return respond(ctx, 404, h.vnull(), &.{.{ "statusText", h.vstr("Not found") }});
|
|
186
213
|
}
|
|
187
214
|
h.del_prop(ent, h.vstr("$KEY"));
|
|
188
|
-
return respond(200, h.clone(ent), &.{});
|
|
215
|
+
return respond(ctx, 200, h.clone(ent), &.{});
|
|
189
216
|
} else if (std.mem.eql(u8, op.name, "list")) {
|
|
190
217
|
const args = build_args(ctx, ctx.reqmatch);
|
|
191
218
|
const found = vs.select(h.A(), entmap, args) catch h.olist();
|
|
192
219
|
if (h.is_noval(found)) {
|
|
193
|
-
return respond(404, h.vnull(), &.{.{ "statusText", h.vstr("Not found") }});
|
|
220
|
+
return respond(ctx, 404, h.vnull(), &.{.{ "statusText", h.vstr("Not found") }});
|
|
194
221
|
}
|
|
195
222
|
if (found == .array) {
|
|
196
223
|
for (found.array.data.items) |item| h.del_prop(item, h.vstr("$KEY"));
|
|
197
224
|
}
|
|
198
|
-
return respond(200, h.clone(found), &.{});
|
|
225
|
+
return respond(ctx, 200, h.clone(found), &.{});
|
|
199
226
|
} else if (std.mem.eql(u8, op.name, "update")) {
|
|
200
227
|
const reqdata = ctx.reqdata;
|
|
201
228
|
var update_match = h.omap();
|
|
@@ -221,14 +248,14 @@ fn test_fetch(entity: Value, ctx: *Context, _: []const u8, _: Value) err.E!Value
|
|
|
221
248
|
}
|
|
222
249
|
}
|
|
223
250
|
if (h.is_noval(ent)) {
|
|
224
|
-
return respond(404, h.vnull(), &.{.{ "statusText", h.vstr("Not found") }});
|
|
251
|
+
return respond(ctx, 404, h.vnull(), &.{.{ "statusText", h.vstr("Not found") }});
|
|
225
252
|
}
|
|
226
253
|
if (ent == .object and reqdata == .object) {
|
|
227
254
|
var it = reqdata.object.iterator();
|
|
228
255
|
while (it.next()) |kv| h.setp(ent, kv.key_ptr.*, kv.value_ptr.*);
|
|
229
256
|
}
|
|
230
257
|
h.del_prop(ent, h.vstr("$KEY"));
|
|
231
|
-
return respond(200, h.clone(ent), &.{});
|
|
258
|
+
return respond(ctx, 200, h.clone(ent), &.{});
|
|
232
259
|
} else if (std.mem.eql(u8, op.name, "remove")) {
|
|
233
260
|
const m = resolve_match(ctx, ctx.reqmatch);
|
|
234
261
|
const args = build_args(ctx, m);
|
|
@@ -238,7 +265,7 @@ fn test_fetch(entity: Value, ctx: *Context, _: []const u8, _: Value) err.E!Value
|
|
|
238
265
|
const id = h.getp(ent, "id");
|
|
239
266
|
h.del_prop(entmap, id);
|
|
240
267
|
}
|
|
241
|
-
return respond(200, h.vnull(), &.{});
|
|
268
|
+
return respond(ctx, 200, h.vnull(), &.{});
|
|
242
269
|
} else if (std.mem.eql(u8, op.name, "create")) {
|
|
243
270
|
_ = build_args(ctx, ctx.reqdata);
|
|
244
271
|
var id = ctx.util().param(ctx, h.vstr("id"));
|
|
@@ -255,12 +282,12 @@ fn test_fetch(entity: Value, ctx: *Context, _: []const u8, _: Value) err.E!Value
|
|
|
255
282
|
h.setp(ent, "id", id);
|
|
256
283
|
if (id == .string) h.setp(entmap, id.string, ent);
|
|
257
284
|
h.del_prop(ent, h.vstr("$KEY"));
|
|
258
|
-
return respond(200, h.clone(ent), &.{});
|
|
285
|
+
return respond(ctx, 200, h.clone(ent), &.{});
|
|
259
286
|
}
|
|
260
|
-
return respond(200, ent, &.{});
|
|
287
|
+
return respond(ctx, 200, ent, &.{});
|
|
261
288
|
}
|
|
262
289
|
|
|
263
|
-
return respond(404, h.vnull(), &.{.{ "statusText", h.vstr("Unknown operation") }});
|
|
290
|
+
return respond(ctx, 404, h.vnull(), &.{.{ "statusText", h.vstr("Unknown operation") }});
|
|
264
291
|
}
|
|
265
292
|
|
|
266
293
|
// make_netsim (test-local): counter-driven latency / first-N failures /
|