@voxgig/sdkgen 4.0.1 → 4.2.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/src/cmp/scala/TestDirect_scala.ts +1 -1
- package/project/.sdk/src/cmp/scala/TestEntity_scala.ts +1 -1
- package/project/.sdk/tm/c/feature/test.c +50 -12
- package/project/.sdk/tm/clojure/src/sdk/features.clj +21 -3
- package/project/.sdk/tm/cpp/Makefile +11 -1
- package/project/.sdk/tm/cpp/feature/test.hpp +34 -11
- package/project/.sdk/tm/csharp/feature/TestFeature.cs +28 -2
- package/project/.sdk/tm/dart/lib/feature/test/TestFeature.dart +38 -10
- package/project/.sdk/tm/elixir/lib/projectname/feature/test.ex +40 -13
- package/project/.sdk/tm/java/feature/TestFeature.java +46 -12
- package/project/.sdk/tm/kotlin/feature/TestFeature.kt +29 -11
- package/project/.sdk/tm/lean/src/SdkRuntime.lean +19 -1
- package/project/.sdk/tm/lean/src/SdkUtility.lean +2 -2
- package/project/.sdk/tm/ocaml/sdk_features.ml +31 -12
- package/project/.sdk/tm/perl/feature/test_feature.pm +34 -12
- package/project/.sdk/tm/py-data/test/conftest.py +24 -0
- 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
|
@@ -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.
|
|
@@ -205,6 +205,23 @@ def liveFetcher : SdkFeature.Fetcher := fun _ctx url fetchdef => do
|
|
|
205
205
|
("body", json), ("headers", ← emptyMap)]
|
|
206
206
|
pure (resp, none)
|
|
207
207
|
|
|
208
|
+
/-- THE MOCK HAS TO AGREE WITH THE MODEL. A point carrying
|
|
209
|
+
`transform.res: `body.item`` describes an API that answers {"item": {...}}
|
|
210
|
+
and the response transform unwraps that key on the way back. Returning the
|
|
211
|
+
bare payload means the transform unwraps a property that is not there and
|
|
212
|
+
the caller gets nothing. Mirrors the go/ts/lua/php mocks. -/
|
|
213
|
+
def mockEnvelope (ctx : Value) (data : Value) : SIO Value := do
|
|
214
|
+
if isNv data then pure data else do
|
|
215
|
+
let tm ← gp (← gp ctx "point") "transform"
|
|
216
|
+
let spec := asStr (← gp tm "res")
|
|
217
|
+
-- Exactly `body.<key>`; a deeper path is not an envelope this mock can
|
|
218
|
+
-- synthesise, so it is left alone rather than guessed at.
|
|
219
|
+
if spec.startsWith "`body." && spec.endsWith "`" && spec.length > 7 then
|
|
220
|
+
let inner := ((spec.drop 6).dropRight 1).toString
|
|
221
|
+
if inner.isEmpty || inner.contains '.' then pure data
|
|
222
|
+
else newMap #[(inner, data)]
|
|
223
|
+
else pure data
|
|
224
|
+
|
|
208
225
|
/-- The base transport in test mode: answer from the seeded store. -/
|
|
209
226
|
def testFetcher : SdkFeature.Fetcher := fun ctx _url _fetchdef => do
|
|
210
227
|
let client ← gp ctx "client"
|
|
@@ -213,8 +230,9 @@ def testFetcher : SdkFeature.Fetcher := fun ctx _url _fetchdef => do
|
|
|
213
230
|
let matchV ← gp ctx "reqmatch"
|
|
214
231
|
let dataV ← gp ctx "reqdata"
|
|
215
232
|
let out ← mockOp client entityName opName matchV dataV
|
|
233
|
+
let payload ← mockEnvelope ctx out
|
|
216
234
|
let resp ← newMap #[("status", .num 200.0), ("statusText", .str "OK"),
|
|
217
|
-
("body",
|
|
235
|
+
("body", payload), ("headers", ← emptyMap)]
|
|
218
236
|
pure (resp, none)
|
|
219
237
|
|
|
220
238
|
/-- Merge config.feature and options.feature into the client's feature options. -/
|
|
@@ -595,8 +595,6 @@ def makeResult (ctx : Value) : SIO Value := do
|
|
|
595
595
|
sp ctx "result" res
|
|
596
596
|
pure res
|
|
597
597
|
|
|
598
|
-
/-- Select the endpoint for this operation: the single point, else the first
|
|
599
|
-
whose `select.exist` keys are all present and whose `$action` agrees. -/
|
|
600
598
|
/-- How many path segments a point has, and whether its path ends in a
|
|
601
599
|
parameter. A record route ends in the record's identifier (/boards/{id});
|
|
602
600
|
a cross-reference that also returns the entity ends in the relationship's
|
|
@@ -611,6 +609,8 @@ def pointShape (pt : Value) : SIO (Nat × Bool) := do
|
|
|
611
609
|
else pure (items.size, (vs items[items.size - 1]!).startsWith "{")
|
|
612
610
|
| _ => pure (0, false)
|
|
613
611
|
|
|
612
|
+
/-- Select the endpoint for this operation: the single point, else the first
|
|
613
|
+
whose `select.exist` keys are all present and whose `$action` agrees. -/
|
|
614
614
|
def makePoint (ctx : Value) : SIO Value := do
|
|
615
615
|
let op ← gp ctx "op"
|
|
616
616
|
let matchV ← gp ctx "reqmatch"
|
|
@@ -980,8 +980,27 @@ let netsim_feature () : feature =
|
|
|
980
980
|
let test_feature () : feature =
|
|
981
981
|
let f = { f_name = "test"; f_version = "0.0.1"; f_active = true; f_options = Noval;
|
|
982
982
|
f_init = (fun _ _ -> ()); f_hook = (fun _ _ -> ()) } in
|
|
983
|
-
|
|
984
|
-
|
|
983
|
+
(* THE MOCK HAS TO AGREE WITH THE MODEL. A point carrying
|
|
984
|
+
`transform.res: `body.item`` describes an API that answers {"item": {...}}
|
|
985
|
+
and the response transform unwraps that key on the way back. Returning the
|
|
986
|
+
bare payload means the transform unwraps a property that is not there and
|
|
987
|
+
the caller gets nothing. Mirrors the go/ts/lua/php mocks. *)
|
|
988
|
+
let envelope ctx data =
|
|
989
|
+
if is_nullish data then data
|
|
990
|
+
else
|
|
991
|
+
match getp (getp ctx.c_point "transform") "res" with
|
|
992
|
+
| Str spec ->
|
|
993
|
+
let n = String.length spec in
|
|
994
|
+
(* Exactly `body.<key>`; a deeper path is not an envelope this mock
|
|
995
|
+
can synthesise, so it is left alone rather than guessed at. *)
|
|
996
|
+
if n > 7 && String.sub spec 0 6 = "`body." && spec.[n - 1] = '`' then
|
|
997
|
+
let inner = String.sub spec 6 (n - 7) in
|
|
998
|
+
if String.length inner = 0 || String.contains inner '.' then data
|
|
999
|
+
else jo [(inner, data)]
|
|
1000
|
+
else data
|
|
1001
|
+
| _ -> data in
|
|
1002
|
+
let respond ctx status data extra =
|
|
1003
|
+
let out = jo [("status", vint_of status); ("statusText", Str "OK"); ("json", json_thunk (envelope ctx data)); ("body", Str "not-used")] in
|
|
985
1004
|
(match extra with Some (Map _ as e) -> List.iter (fun k -> setp out k (getp e k)) (keysof e) | _ -> ());
|
|
986
1005
|
(out, None) in
|
|
987
1006
|
let build_args ctx (op : operation) args =
|
|
@@ -1027,15 +1046,15 @@ let test_feature () : feature =
|
|
|
1027
1046
|
| "load" ->
|
|
1028
1047
|
let args = build_args fctx op (resolve_match fctx fctx.c_reqmatch) in
|
|
1029
1048
|
let ent = getelem (select entmap args) (Num 0.0) in
|
|
1030
|
-
if is_nullish ent then respond 404 Noval (Some (jo [("statusText", Str "Not found")]))
|
|
1031
|
-
else (ignore (delprop ent (Str "$KEY")); respond 200 (clone ent) None)
|
|
1049
|
+
if is_nullish ent then respond fctx 404 Noval (Some (jo [("statusText", Str "Not found")]))
|
|
1050
|
+
else (ignore (delprop ent (Str "$KEY")); respond fctx 200 (clone ent) None)
|
|
1032
1051
|
| "list" ->
|
|
1033
1052
|
let args = build_args fctx op fctx.c_reqmatch in
|
|
1034
1053
|
let found = select entmap args in
|
|
1035
|
-
if is_nullish found then respond 404 Noval (Some (jo [("statusText", Str "Not found")]))
|
|
1054
|
+
if is_nullish found then respond fctx 404 Noval (Some (jo [("statusText", Str "Not found")]))
|
|
1036
1055
|
else begin
|
|
1037
1056
|
(match found with List r -> List.iter (fun item -> ignore (delprop item (Str "$KEY"))) !r | _ -> ());
|
|
1038
|
-
respond 200 (clone found) None
|
|
1057
|
+
respond fctx 200 (clone found) None
|
|
1039
1058
|
end
|
|
1040
1059
|
| "update" ->
|
|
1041
1060
|
let update_match = empty_map () in
|
|
@@ -1046,17 +1065,17 @@ let test_feature () : feature =
|
|
|
1046
1065
|
(if is_nullish !ent then match entmap with
|
|
1047
1066
|
| Map m -> (try (match List.find (fun (_, v) -> match v with Map _ -> true | _ -> false) m.entries with (_, v) -> ent := v) with Not_found -> ())
|
|
1048
1067
|
| _ -> ());
|
|
1049
|
-
if is_nullish !ent then respond 404 Noval (Some (jo [("statusText", Str "Not found")]))
|
|
1068
|
+
if is_nullish !ent then respond fctx 404 Noval (Some (jo [("statusText", Str "Not found")]))
|
|
1050
1069
|
else begin
|
|
1051
1070
|
(match !ent with Map _ -> (match fctx.c_reqdata with Map _ -> List.iter (fun k -> setp !ent k (getp fctx.c_reqdata k)) (keysof fctx.c_reqdata) | _ -> ()) | _ -> ());
|
|
1052
1071
|
ignore (delprop !ent (Str "$KEY"));
|
|
1053
|
-
respond 200 (clone !ent) None
|
|
1072
|
+
respond fctx 200 (clone !ent) None
|
|
1054
1073
|
end
|
|
1055
1074
|
| "remove" ->
|
|
1056
1075
|
let args = build_args fctx op (resolve_match fctx fctx.c_reqmatch) in
|
|
1057
1076
|
let ent = getelem (select entmap args) (Num 0.0) in
|
|
1058
1077
|
(match ent with Map _ -> ignore (delprop entmap (getp ent "id")) | _ -> ());
|
|
1059
|
-
respond 200 Noval None
|
|
1078
|
+
respond fctx 200 Noval None
|
|
1060
1079
|
| "create" ->
|
|
1061
1080
|
ignore (build_args fctx op fctx.c_reqdata);
|
|
1062
1081
|
let eid = let v = (cu fctx).u_param fctx (Str "id") in if is_nullish v then Str (random_id16 ()) else v in
|
|
@@ -1066,9 +1085,9 @@ let test_feature () : feature =
|
|
|
1066
1085
|
setp ent "id" eid;
|
|
1067
1086
|
(match eid with Str s -> setp entmap s ent | _ -> ());
|
|
1068
1087
|
ignore (delprop ent (Str "$KEY"));
|
|
1069
|
-
respond 200 (clone ent) None
|
|
1070
|
-
| _ -> respond 200 ent None)
|
|
1071
|
-
| _ -> respond 404 Noval (Some (jo [("statusText", Str "Unknown operation")]))) in
|
|
1088
|
+
respond fctx 200 (clone ent) None
|
|
1089
|
+
| _ -> respond fctx 200 ent None)
|
|
1090
|
+
| _ -> respond fctx 404 Noval (Some (jo [("statusText", Str "Unknown operation")]))) in
|
|
1072
1091
|
let make_netsim net inner =
|
|
1073
1092
|
let netcalls = ref 0 in
|
|
1074
1093
|
let pick_latency () =
|
|
@@ -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
|
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Make the suite runnable with a bare `make test`, the way every other
|
|
2
|
+
# target's is.
|
|
3
|
+
#
|
|
4
|
+
# py-data is the one target that CONSUMES a sibling: its package imports the
|
|
5
|
+
# ProjectName SDK generated into ../py of the same repo. The Makefile's `dev`
|
|
6
|
+
# target pip-installs both editable, but `test` does not depend on it, so
|
|
7
|
+
# `make test` on a fresh clone died in collection with
|
|
8
|
+
# "ModuleNotFoundError: No module named 'projectname_sdk'" — before a single
|
|
9
|
+
# assertion ran.
|
|
10
|
+
#
|
|
11
|
+
# Requiring an install step would also make this target the only one that
|
|
12
|
+
# cannot be tested from a clean checkout. Putting the two source roots on
|
|
13
|
+
# sys.path keeps it self-contained; an installed copy still wins, because
|
|
14
|
+
# these entries are appended only when the import is not already satisfiable.
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import sys
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
|
|
20
|
+
_here = Path(__file__).resolve().parent
|
|
21
|
+
for _root in (_here.parent, _here.parent.parent / "py"):
|
|
22
|
+
_p = str(_root)
|
|
23
|
+
if _root.is_dir() and _p not in sys.path:
|
|
24
|
+
sys.path.insert(0, _p)
|
|
@@ -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
|
)),
|