@voxgig/sdkgen 3.4.5 → 3.4.6

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/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
+ }