@voxgig/sdkgen 3.4.5 → 3.4.7

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.
Files changed (35) hide show
  1. package/dist/cmp/ReadmeTop.js +5 -1
  2. package/dist/cmp/ReadmeTop.js.map +1 -1
  3. package/dist/helpers/opExample.js +10 -1
  4. package/dist/helpers/opExample.js.map +1 -1
  5. package/dist/sdkgen.d.ts +2 -2
  6. package/dist/sdkgen.js +6 -2
  7. package/dist/sdkgen.js.map +1 -1
  8. package/dist/tsconfig.tsbuildinfo +1 -1
  9. package/dist/utility.d.ts +8 -1
  10. package/dist/utility.js +144 -0
  11. package/dist/utility.js.map +1 -1
  12. package/package.json +1 -1
  13. package/project/.sdk/src/cmp/c/Config_c.ts +45 -0
  14. package/project/.sdk/src/cmp/c/utility_c.ts +36 -0
  15. package/project/.sdk/src/cmp/dart/Config_dart.ts +15 -0
  16. package/project/.sdk/src/cmp/dart/fragment/Config.fragment.dart +1 -1
  17. package/project/.sdk/src/cmp/go/Config_go.ts +9 -52
  18. package/project/.sdk/src/cmp/js/Config_js.ts +49 -1
  19. package/project/.sdk/src/cmp/js/fragment/Config.data.fragment.js +50 -0
  20. package/project/.sdk/src/cmp/js/fragment/Config.fragment.js +1 -1
  21. package/project/.sdk/src/cmp/lua/Config_lua.ts +51 -1
  22. package/project/.sdk/src/cmp/lua/utility_lua.ts +21 -0
  23. package/project/.sdk/src/cmp/php/Config_php.ts +86 -0
  24. package/project/.sdk/src/cmp/py/Config_py.ts +47 -1
  25. package/project/.sdk/src/cmp/rb/Config_rb.ts +46 -2
  26. package/project/.sdk/src/cmp/rust/Config_rust.ts +50 -35
  27. package/project/.sdk/src/cmp/rust/utility_rust.ts +17 -0
  28. package/project/.sdk/src/cmp/ts/Config_ts.ts +10 -50
  29. package/project/.sdk/src/cmp/zig/Config_zig.ts +47 -40
  30. package/project/.sdk/src/cmp/zig/fragment/Main.fragment.zig +28 -17
  31. package/project/.sdk/tm/zig/test/gotcha_test.zig +8 -8
  32. package/src/cmp/ReadmeTop.ts +6 -1
  33. package/src/helpers/opExample.ts +11 -1
  34. package/src/sdkgen.ts +6 -1
  35. package/src/utility.ts +157 -1
@@ -22,7 +22,20 @@ const Spec = spec_mod.Spec;
22
22
  pub const ProjectNameSDK = struct {
23
23
  mode: []const u8 = "live",
24
24
  options: Value = .{ .null = {} },
25
- utility: *Utility,
25
+ // NOT named `utility`, and NOT a name `zigVarName` can produce.
26
+ //
27
+ // Entity accessors are generated as methods on this same struct
28
+ // (MainEntity_zig), so an API with an entity called `utility` collided
29
+ // twice: Zig rejects a local binding that shadows the declaration, and
30
+ // `sdk.utility` resolves to the FIELD, which made the generated accessor
31
+ // unreachable.
32
+ //
33
+ // `zigVarName` lowercases every name it is given, so a field spelling with
34
+ // an uppercase letter is one no entity name can ever reach - which is why
35
+ // this is `sdkUtility` and not, say, `util_rt`, a name an entity called
36
+ // `util-rt` would map straight onto. The public reader is `get_utility()`,
37
+ // so this field is internal and free to be spelled this way.
38
+ sdkUtility: *Utility,
26
39
  features: std.ArrayList(Feature),
27
40
  rootctx: ?*Context = null,
28
41
 
@@ -31,22 +44,22 @@ pub const ProjectNameSDK = struct {
31
44
  sdk.* = .{
32
45
  .mode = "live",
33
46
  .options = h.vnull(),
34
- .utility = Utility.new(),
47
+ .sdkUtility = Utility.new(),
35
48
  .features = std.ArrayList(Feature).init(h.A()),
36
49
  .rootctx = null,
37
50
  };
38
51
 
39
52
  const cfg = config.make_config();
40
53
 
41
- const rootctx = sdk.utility.make_context(CtxSpec{
54
+ const rootctx = sdk.sdkUtility.make_context(CtxSpec{
42
55
  .client = sdk,
43
- .utility = sdk.utility,
56
+ .utility = sdk.sdkUtility,
44
57
  .config = cfg,
45
58
  .options = options,
46
59
  .shared = h.omap(),
47
60
  }, null);
48
61
 
49
- const opts = sdk.utility.make_options(rootctx);
62
+ const opts = sdk.sdkUtility.make_options(rootctx);
50
63
  sdk.options = opts;
51
64
 
52
65
  if (h.veq(h.getpath(&.{ "feature", "test", "active" }, opts), h.vbool(true))) {
@@ -71,7 +84,7 @@ pub const ProjectNameSDK = struct {
71
84
  const fopts = h.getp(feature_opts, fname);
72
85
  if (fopts == .object) {
73
86
  if (h.get_bool(fopts, "active") orelse false) {
74
- sdk.utility.feature_add(rootctx, config.make_feature(fname));
87
+ sdk.sdkUtility.feature_add(rootctx, config.make_feature(fname));
75
88
  }
76
89
  }
77
90
  }
@@ -80,9 +93,9 @@ pub const ProjectNameSDK = struct {
80
93
  // Initialize features.
81
94
  var snap = std.ArrayList(Feature).init(h.A());
82
95
  for (sdk.features.items) |f| snap.append(f) catch {};
83
- for (snap.items) |f| sdk.utility.feature_init(rootctx, f);
96
+ for (snap.items) |f| sdk.sdkUtility.feature_init(rootctx, f);
84
97
 
85
- sdk.utility.feature_hook(rootctx, "PostConstruct");
98
+ sdk.sdkUtility.feature_hook(rootctx, "PostConstruct");
86
99
 
87
100
  return sdk;
88
101
  }
@@ -92,7 +105,7 @@ pub const ProjectNameSDK = struct {
92
105
  }
93
106
 
94
107
  pub fn get_utility(self: *ProjectNameSDK) *Utility {
95
- return Utility.copy(self.utility);
108
+ return Utility.copy(self.sdkUtility);
96
109
  }
97
110
 
98
111
  pub fn get_root_ctx(self: *ProjectNameSDK) *Context {
@@ -100,7 +113,6 @@ pub const ProjectNameSDK = struct {
100
113
  }
101
114
 
102
115
  pub fn prepare(self: *ProjectNameSDK, fetchargs_in: Value) E!Value {
103
- const utility = self.utility;
104
116
 
105
117
  const fetchargs: Value = switch (fetchargs_in) {
106
118
  .object => fetchargs_in,
@@ -112,7 +124,7 @@ pub const ProjectNameSDK = struct {
112
124
  else => h.omap(),
113
125
  };
114
126
 
115
- const ctx = utility.make_context(CtxSpec{
127
+ const ctx = self.sdkUtility.make_context(CtxSpec{
116
128
  .opname = "prepare",
117
129
  .ctrl = ctrl,
118
130
  }, self.get_root_ctx());
@@ -134,7 +146,7 @@ pub const ProjectNameSDK = struct {
134
146
  else => h.omap(),
135
147
  };
136
148
 
137
- const headers = utility.prepare_headers(ctx);
149
+ const headers = self.sdkUtility.prepare_headers(ctx);
138
150
 
139
151
  const specmap = h.jo(&.{
140
152
  .{ "base", h.getp(options, "base") },
@@ -158,9 +170,9 @@ pub const ProjectNameSDK = struct {
158
170
  while (it.next()) |kv| h.setp(spec.headers, kv.key_ptr.*, kv.value_ptr.*);
159
171
  }
160
172
 
161
- _ = try utility.prepare_auth(ctx);
173
+ _ = try self.sdkUtility.prepare_auth(ctx);
162
174
 
163
- return utility.make_fetch_def(ctx);
175
+ return self.sdkUtility.make_fetch_def(ctx);
164
176
  }
165
177
 
166
178
  // Raw endpoint access is operator-controllable, like every entity op.
@@ -200,7 +212,6 @@ pub const ProjectNameSDK = struct {
200
212
  // a caller-supplied marker would let anyone opt straight back out of the
201
213
  // gate by passing it.
202
214
  fn raw_request(self: *ProjectNameSDK, fetchargs_in: Value) Value {
203
- const utility = self.utility;
204
215
 
205
216
  const fetchdef = self.prepare(fetchargs_in) catch {
206
217
  return h.jo(&.{
@@ -218,13 +229,13 @@ pub const ProjectNameSDK = struct {
218
229
  else => h.omap(),
219
230
  };
220
231
 
221
- const ctx = utility.make_context(CtxSpec{
232
+ const ctx = self.sdkUtility.make_context(CtxSpec{
222
233
  .opname = "direct",
223
234
  .ctrl = ctrl,
224
235
  }, self.get_root_ctx());
225
236
 
226
237
  const url = h.get_str(fetchdef, "url") orelse "";
227
- const fetched = utility.fetch(ctx, url, fetchdef) catch {
238
+ const fetched = self.sdkUtility.fetch(ctx, url, fetchdef) catch {
228
239
  return h.jo(&.{
229
240
  .{ "ok", h.vbool(false) },
230
241
  .{ "err", h.vstr(if (ctx.pending_err) |e| e.msg else "fetch failed") },
@@ -100,7 +100,7 @@ test "gotcha8: custom utility callable survives" {
100
100
  const fn_val = h.callable(@ptrCast(&probe_dummy), probeCall);
101
101
  const opts = h.jo(&.{.{ "utility", h.jo(&.{.{ "probe", fn_val }}) }});
102
102
  const client = sdk.SDK.new(opts);
103
- const stored = h.getp(client.utility.custom, "probe");
103
+ const stored = h.getp(client.sdkUtility.custom, "probe");
104
104
  try testing.expect(stored == .function);
105
105
  const r = h.call_vfn(stored, h.vstr("x"));
106
106
  try testing.expect(r == .string and std.mem.eql(u8, r.string, "x"));
@@ -139,10 +139,10 @@ test "gotcha3: many features" {
139
139
 
140
140
  test "gotcha2: make_point surfaces out.point error" {
141
141
  const client = sdk.new();
142
- const ctx = client.utility.make_context(.{ .opname = "list" }, client.get_root_ctx());
142
+ const ctx = client.sdkUtility.make_context(.{ .opname = "list" }, client.get_root_ctx());
143
143
  const e = ctx.make_error("rbac_denied", "denied");
144
144
  ctx.out_set("point", sdk.OutVal{ .err = e });
145
- try testing.expectError(error.Sdk, client.utility.make_point(ctx));
145
+ try testing.expectError(error.Sdk, client.sdkUtility.make_point(ctx));
146
146
  try testing.expect(ctx.pending_err != null);
147
147
  try testing.expect(std.mem.eql(u8, ctx.pending_err.?.code, "rbac_denied"));
148
148
  }
@@ -152,8 +152,8 @@ test "gotcha2: make_point surfaces out.point error" {
152
152
  test "gotcha4: featureAdd before ordering" {
153
153
  const client = sdk.new();
154
154
  const ctx = client.get_root_ctx();
155
- client.utility.feature_add(ctx, OrderFeat.make("aaa", vnull()));
156
- client.utility.feature_add(ctx, OrderFeat.make("bbb", h.jo(&.{.{ "__before__", h.vstr("aaa") }})));
155
+ client.sdkUtility.feature_add(ctx, OrderFeat.make("aaa", vnull()));
156
+ client.sdkUtility.feature_add(ctx, OrderFeat.make("bbb", h.jo(&.{.{ "__before__", h.vstr("aaa") }})));
157
157
  var ai: usize = 999;
158
158
  var bi: usize = 999;
159
159
  for (client.features.items, 0..) |f, i| {
@@ -176,7 +176,7 @@ test "gotcha6: netsim offline error code" {
176
176
  });
177
177
  const client = sdk.SDK.new(opts);
178
178
  const ctx = client.get_root_ctx();
179
- const r = client.utility.fetch(ctx, "http://x", h.jo(&.{.{ "url", h.vstr("http://x") }}));
179
+ const r = client.sdkUtility.fetch(ctx, "http://x", h.jo(&.{.{ "url", h.vstr("http://x") }}));
180
180
  try testing.expectError(error.Sdk, r);
181
181
  try testing.expect(std.mem.eql(u8, ctx.pending_err.?.code, "netsim_offline"));
182
182
  }
@@ -192,7 +192,7 @@ test "gotcha6: netsim conn error code" {
192
192
  });
193
193
  const client = sdk.SDK.new(opts);
194
194
  const ctx = client.get_root_ctx();
195
- const r = client.utility.fetch(ctx, "http://x", h.jo(&.{.{ "url", h.vstr("http://x") }}));
195
+ const r = client.sdkUtility.fetch(ctx, "http://x", h.jo(&.{.{ "url", h.vstr("http://x") }}));
196
196
  try testing.expectError(error.Sdk, r);
197
197
  try testing.expect(std.mem.eql(u8, ctx.pending_err.?.code, "netsim_conn"));
198
198
  }
@@ -215,7 +215,7 @@ test "gotcha6: netsim seeded latency is deterministic" {
215
215
  const ctx = client.get_root_ctx();
216
216
  var i: usize = 0;
217
217
  while (i < 4) : (i += 1) {
218
- _ = client.utility.fetch(ctx, "http://x", h.jo(&.{.{ "url", h.vstr("http://x") }})) catch {};
218
+ _ = client.sdkUtility.fetch(ctx, "http://x", h.jo(&.{.{ "url", h.vstr("http://x") }})) catch {};
219
219
  }
220
220
  return sleep_log.toOwnedSlice() catch &.{};
221
221
  }
@@ -261,8 +261,13 @@ ${aboutMd.trim()}
261
261
  exCall = `const ${exLower} = await client.${ex}().load(${exLoadArg})`
262
262
  } else if ('create' === primaryOp || 'update' === primaryOp) {
263
263
  const exIdF = entityIdField(exEnt)
264
+ // Drop the id only when the request shape says it is OPTIONAL. It is
265
+ // server-assigned on a normal create, but an op whose id comes from a
266
+ // PATH PARAMETER requires it, and the typed CreateData then rejects a
267
+ // body without it. Same rule as dataArg in helpers/opExample.
264
268
  const shapeItems = opRequestShape(exEnt, primaryOp).items
265
- .filter((it: any) => it.name !== exIdF && it.name !== 'id')
269
+ .filter((it: any) =>
270
+ (it.name !== exIdF && it.name !== 'id') || !it.optional)
266
271
  const required = shapeItems.filter((it: any) => !it.optional)
267
272
  // ALL required fields must appear or the literal is not assignable to
268
273
  // the typed CreateData/UpdateData; cap only the optional fallback.
@@ -116,8 +116,18 @@ function matchArg(
116
116
  // `key: value` pairs (capped) in the target language's object syntax. Ensures
117
117
  // the body satisfies a typed CreateData/UpdateData (required fields present).
118
118
  function dataArg(lang: ExampleLang, ent: any, op: string, idF: string | null): string {
119
+ // The id is normally server-assigned on create, so it is dropped from the
120
+ // example body. But it is only safe to drop when the request shape says it is
121
+ // OPTIONAL: an op whose id comes from a PATH PARAMETER requires it, and a
122
+ // typed CreateData then rejects a body without it.
123
+ //
124
+ // Conecto's `/integrations/{slug}/actions/{action}/run/` is the case — the
125
+ // guide renames the `action` param to `id`, so ActionCreateData is
126
+ // `{id, slug, ok}` and the generated snippet emitted only `{slug, ok}`,
127
+ // failing to compile with "Property 'id' is missing".
119
128
  const items = opRequestShape(ent, op).items
120
- .filter((it: any) => it.name !== idF && it.name !== 'id')
129
+ .filter((it: any) =>
130
+ (it.name !== idF && it.name !== 'id') || !it.optional)
121
131
  const required = items.filter((it: any) => !it.optional)
122
132
  // ALL required fields must appear (a typed CreateData rejects a partial); cap
123
133
  // only the optional fallback used when the op declares no required field.
package/src/sdkgen.ts CHANGED
@@ -20,7 +20,8 @@ import type {
20
20
  } from './types'
21
21
 
22
22
  import { SdkGenError, requirePath, isAuthActive, resolveAuthPrefix,
23
- CONFIG_DATA_THRESHOLD, CONFIG_REPR_VALUES, isConfigData, configRepr } from './utility'
23
+ CONFIG_DATA_THRESHOLD, CONFIG_REPR_VALUES, isConfigData, configRepr,
24
+ configReprSetting, configDefinition, clean, rawStringLiteral } from './utility'
24
25
 
25
26
  import { Main } from './cmp/Main'
26
27
  import { ExternalTarget } from './cmp/ExternalTarget'
@@ -781,6 +782,10 @@ export {
781
782
  CONFIG_REPR_VALUES,
782
783
  isConfigData,
783
784
  configRepr,
785
+ configReprSetting,
786
+ configDefinition,
787
+ clean,
788
+ rawStringLiteral,
784
789
 
785
790
  // Scaffold components need this to fail a generation with an actionable
786
791
  // message rather than a bare Error (py-data guards on its sibling `py`).
package/src/utility.ts CHANGED
@@ -1,10 +1,12 @@
1
1
 
2
2
  import Path from 'node:path'
3
3
 
4
- import { JostracaResult } from 'jostraca'
4
+ import { JostracaResult, each } from 'jostraca'
5
5
 
6
6
  import { KIT, getModelPath } from '@voxgig/apidef'
7
7
 
8
+ import { serverVariables } from './helpers/serverVars'
9
+
8
10
 
9
11
  // Where a per-target component is loaded from: `<project>/.sdk/dist/<path>`.
10
12
  //
@@ -100,6 +102,10 @@ export {
100
102
  CONFIG_REPR_VALUES,
101
103
  isConfigData,
102
104
  configRepr,
105
+ configReprSetting,
106
+ configDefinition,
107
+ clean,
108
+ rawStringLiteral,
103
109
  }
104
110
 
105
111
 
@@ -166,3 +172,153 @@ function isConfigData(configJson: string, repr?: string): boolean {
166
172
  function configRepr(configJson: string, repr?: string): string {
167
173
  return isConfigData(configJson, repr) ? 'data' : 'literal'
168
174
  }
175
+
176
+
177
+ // The per-SDK override, or 'auto'. `main.kit.config.repr` is optional, and
178
+ // getModelPath throws rather than returning undefined for an absent path.
179
+ function configReprSetting(model: any): string {
180
+ try {
181
+ return getModelPath(model, `main.${KIT}.config.repr`) || 'auto'
182
+ }
183
+ catch (_e) {
184
+ return 'auto'
185
+ }
186
+ }
187
+
188
+
189
+ // L0 NORMALISATION: strip what the emitted config must not carry.
190
+ //
191
+ // Three kinds of noise, and the distinction between them matters:
192
+ //
193
+ // MODEL_META jostraca's `each` injects index$/key$/val$ into every node
194
+ // it iterates. Pure bookkeeping, and it leaked into every
195
+ // generated SDK for years (5,231 occurrences in gitlab
196
+ // alone) because this helper deleted keys during a walk that
197
+ // assigned them straight back.
198
+ //
199
+ // CONFIG_DEFAULT a key whose value equals the default the reader already
200
+ // applies. Emitting it is pure bulk. Only these three names
201
+ // are dropped, and only at their default value - `entity$`
202
+ // is real Seneca data, so no blanket suffix rule.
203
+ //
204
+ // PAYLOAD_KEYS the boundary. Under `default`/`example`/`examples` the
205
+ // value is API DATA, not config, and an example that happens
206
+ // to contain `active: true` must survive intact. Below one of
207
+ // these keys default-dropping stops; metadata stripping does
208
+ // not, because jostraca's bookkeeping is never payload.
209
+ const MODEL_META = ['index$', 'key$', 'val$']
210
+
211
+ const CONFIG_DEFAULT: Record<string, any> = {
212
+ active: true,
213
+ req: false,
214
+ reqd: false,
215
+ }
216
+
217
+ const PAYLOAD_KEYS = ['default', 'example', 'examples']
218
+
219
+ function clean(o: any, dropDefaults?: boolean): any {
220
+ // Rebuild rather than delete in place: the caller's model is shared with
221
+ // every other component, and mutating it here would strip metadata a later
222
+ // target still needs.
223
+ const prune = (node: any, defaults: boolean): any => {
224
+ if (Array.isArray(node)) return node.map((n: any) => prune(n, defaults))
225
+ if (null != node && 'object' === typeof node) {
226
+ const out: any = {}
227
+ for (const k of Object.keys(node)) {
228
+ if (MODEL_META.includes(k)) continue
229
+ // An ABSENT optional member, dropped rather than carried as undefined.
230
+ //
231
+ // Callers build `{fields, name, op, relations}` from an entity, and
232
+ // `op` and `relations` are optional - so the key exists with value
233
+ // undefined. JSON.stringify silently omits such a key, while the
234
+ // literal formatters emit it as None/nil/null, and the two
235
+ // representations would describe different configs for any entity
236
+ // without an `op`. Dropping it here fixes both branches at once,
237
+ // because both reach the emitter through this function.
238
+ if (undefined === node[k]) continue
239
+ if (defaults && k in CONFIG_DEFAULT && CONFIG_DEFAULT[k] === node[k]) continue
240
+ out[k] = prune(node[k], defaults && !PAYLOAD_KEYS.includes(k))
241
+ }
242
+ return out
243
+ }
244
+ return node
245
+ }
246
+ return prune(o, true === dropDefaults)
247
+ }
248
+
249
+
250
+ // The JSON as a source-level string literal, for a language whose SINGLE
251
+ // quoted literal neither interpolates nor processes escapes beyond the quote
252
+ // and the backslash - Ruby, PHP, Perl, Lua.
253
+ //
254
+ // Reproducing the JSON text VERBATIM is all that is needed, because the JSON
255
+ // already encodes control characters and non-ASCII itself. That is why this is
256
+ // preferred over the double-quoted form in those languages: Ruby and PHP
257
+ // interpolate (`#{...}`, `$var`) and Lua does not understand `\uXXXX` at all,
258
+ // so a double-quoted literal would need a language-specific escape table and
259
+ // would get it wrong for exactly the inputs nobody tests.
260
+ function rawStringLiteral(s: string): string {
261
+ return "'" + s.replace(/\\/g, '\\\\').replace(/'/g, "\\'") + "'"
262
+ }
263
+
264
+
265
+ // THE CANONICAL CONFIG OBJECT, and the JSON the threshold is measured on.
266
+ //
267
+ // Every target builds its config from this one function, so the literal a
268
+ // target emits and the data that replaces it above the threshold cannot
269
+ // describe different configs - which is the entire promise of rung L1. Before
270
+ // this existed each target assembled its own, and they had already drifted:
271
+ // `feature.<name>` came out as `{}` in Go and as nothing at all in ts when a
272
+ // feature declared no config.
273
+ //
274
+ // Key order is `each`'s order, which is sorted, so the JSON is byte-stable
275
+ // across runs exactly like the literal it replaces.
276
+ function configDefinition(model: any): { def: any, json: string } {
277
+ const entity = getModelPath(model, `main.${KIT}.entity`)
278
+ const feature = getModelPath(model, `main.${KIT}.feature`)
279
+ const headers = getModelPath(model, `main.${KIT}.config.headers`) || {}
280
+
281
+ const authActive = isAuthActive(model)
282
+ const authPrefix = resolveAuthPrefix(model)
283
+
284
+ let baseUrl = ''
285
+ try { baseUrl = getModelPath(model, `main.${KIT}.info.servers.0.url`) } catch (_e) { }
286
+
287
+ const svars = serverVariables(model)
288
+
289
+ const entityDefs: any = {}
290
+ const entityStubs: any = {}
291
+ each(entity, (e: any) => {
292
+ entityDefs[e.name] = clean({
293
+ fields: e.fields,
294
+ name: e.name,
295
+ op: e.op,
296
+ relations: e.relations,
297
+ }, true)
298
+ entityStubs[e.name] = {}
299
+ })
300
+
301
+ const featureDefs: any = {}
302
+ each(feature, (f: any) => {
303
+ featureDefs[f.name] = f.config || {}
304
+ })
305
+
306
+ const options: any = { base: baseUrl }
307
+ if (0 < svars.length) {
308
+ options.server = svars.reduce((a: any, v: any) => (a[v.name] = v.dflt, a), {})
309
+ }
310
+ if (authActive) {
311
+ options.auth = { prefix: authPrefix }
312
+ }
313
+ options.headers = headers
314
+ options.entity = entityStubs
315
+
316
+ const def = {
317
+ main: { name: model.const.Name },
318
+ feature: featureDefs,
319
+ options,
320
+ entity: entityDefs,
321
+ }
322
+
323
+ return { def, json: JSON.stringify(def) }
324
+ }