@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 CHANGED
@@ -8,7 +8,7 @@ const { Shape, One, Skip } = require('shape')
8
8
 
9
9
  const { SdkGen } = require('../dist/sdkgen.js')
10
10
 
11
- const VERSION = '4.0.1'
11
+ const VERSION = '4.1.0'
12
12
  const KONSOLE = console
13
13
 
14
14
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@voxgig/sdkgen",
3
- "version": "4.0.1",
3
+ "version": "4.1.0",
4
4
  "main": "dist/sdkgen.js",
5
5
  "type": "commonjs",
6
6
  "engines": {
@@ -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
- Value respond(int status, const Value& data, const Value& extra) {
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
- static Dictionary<string, object?> Respond(int status, object? data,
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?>)(() => data),
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
- defp respond(status, data, extra \\ nil) do
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 -> data end,
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
- 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.
@@ -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
 
@@ -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
  )),
@@ -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
- private def respond(status: Int, data: Object, extra: JMap[String, Object]): JMap[String, Object] = {
44
- val js: Supplier[Object] = () => data
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
- private func testRespond(_ status: Int, _ data: Value, _ extra: VMap?) -> Value {
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
- fn respond(status: i64, data: Value, extra: []const h.Pair) Value {
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 /
@@ -3,7 +3,7 @@
3
3
  "package": 1
4
4
  },
5
5
  "name": "@voxgig/sdkgen",
6
- "version": "4.0.1",
6
+ "version": "4.1.0",
7
7
  "provides": {
8
8
  "target": [
9
9
  "c",