@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.
@@ -90,11 +90,45 @@ public class TestFeature extends BaseFeature {
90
90
  }
91
91
  }
92
92
 
93
- private Map<String, Object> respond(int status, Object data, Map<String, Object> extra) {
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>) () -> data);
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
- private fun respond(status: Int, data: Any?, extra: MutableMap<String, Any?>?): MutableMap<String, Any?> {
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?> { data }
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", out), ("headers", ← emptyMap)]
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
- let respond status data extra =
984
- let out = jo [("status", vint_of status); ("statusText", Str "OK"); ("json", json_thunk data); ("body", Str "not-used")] in
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 { $data },
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
- fn respond(status: i64, data: Value, extra: Vec<(&str, Value)>) -> Value {
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(data)),
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
  )),