@voxgig/sdkgen 3.4.3 → 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.
Files changed (34) hide show
  1. package/dist/cmp/ReadmeTop.js +10 -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 +10 -2
  7. package/dist/sdkgen.js.map +1 -1
  8. package/dist/tsconfig.tsbuildinfo +1 -1
  9. package/dist/utility.d.ts +12 -1
  10. package/dist/utility.js +206 -1
  11. package/dist/utility.js.map +1 -1
  12. package/model/sdkgen.aontu +11 -0
  13. package/package.json +1 -1
  14. package/project/.sdk/src/cmp/c/Config_c.ts +45 -0
  15. package/project/.sdk/src/cmp/c/utility_c.ts +36 -0
  16. package/project/.sdk/src/cmp/go/Config_go.ts +86 -2
  17. package/project/.sdk/src/cmp/js/Config_js.ts +49 -1
  18. package/project/.sdk/src/cmp/js/fragment/Config.data.fragment.js +50 -0
  19. package/project/.sdk/src/cmp/js/fragment/Config.fragment.js +1 -1
  20. package/project/.sdk/src/cmp/lua/Config_lua.ts +51 -1
  21. package/project/.sdk/src/cmp/lua/utility_lua.ts +21 -0
  22. package/project/.sdk/src/cmp/php/Config_php.ts +86 -0
  23. package/project/.sdk/src/cmp/py/Config_py.ts +47 -1
  24. package/project/.sdk/src/cmp/rb/Config_rb.ts +46 -2
  25. package/project/.sdk/src/cmp/rust/Config_rust.ts +50 -35
  26. package/project/.sdk/src/cmp/rust/utility_rust.ts +17 -0
  27. package/project/.sdk/src/cmp/ts/Config_ts.ts +49 -1
  28. package/project/.sdk/src/cmp/ts/fragment/Config.data.fragment.ts +50 -0
  29. package/project/.sdk/src/cmp/ts/fragment/Config.fragment.ts +1 -1
  30. package/project/.sdk/src/cmp/zig/Config_zig.ts +47 -40
  31. package/src/cmp/ReadmeTop.ts +10 -1
  32. package/src/helpers/opExample.ts +11 -1
  33. package/src/sdkgen.ts +11 -1
  34. package/src/utility.ts +226 -1
@@ -3,9 +3,11 @@ import {
3
3
  Content,
4
4
  File,
5
5
  cmp,
6
+ configDefinition,
7
+ configReprSetting,
6
8
  each,
7
9
  isAuthActive,
8
- resolveAuthPrefix,
10
+ isConfigData,
9
11
  } from '@voxgig/sdkgen'
10
12
 
11
13
 
@@ -17,7 +19,6 @@ import {
17
19
 
18
20
 
19
21
  import {
20
- clean,
21
22
  formatZigValue,
22
23
  } from './utility_zig'
23
24
 
@@ -31,52 +32,57 @@ const Config = cmp(async function Config(props: any) {
31
32
 
32
33
  const model: Model = ctx$.model
33
34
 
34
- const entity = getModelPath(model, `main.${KIT}.entity`)
35
35
  const feature = getModelPath(model, `main.${KIT}.feature`)
36
36
 
37
- const headers = getModelPath(model, `main.${KIT}.config.headers`) || {}
37
+ // The canonical config OBJECT and its JSON, from the shared helper. Both
38
+ // representations render from the same `def`, so they cannot describe
39
+ // different configs - and this target picks up `options.server` (the
40
+ // OpenAPI server-variable defaults), which the hand-rolled build here
41
+ // omitted entirely.
42
+ const { def: config, json: configJson } = configDefinition(model)
43
+ const asData = isConfigData(configJson, configReprSetting(model))
38
44
 
39
- const authActive = isAuthActive(model)
40
- const authPrefix = resolveAuthPrefix(model)
45
+ File({ name: 'config.' + target.ext }, () => {
41
46
 
42
- let baseUrl = ''
43
- try { baseUrl = getModelPath(model, `main.${KIT}.info.servers.0.url`) } catch (_e) { }
47
+ // ABOVE THE THRESHOLD: emit the model as DATA.
48
+ //
49
+ // The literal is one nested expression that Zig's comptime evaluator has
50
+ // to walk in full at every build; a string constant is one token, and
51
+ // `json_parse` (std.json at the boundary, then fromStdJson) builds the
52
+ // same Value at runtime.
53
+ //
54
+ // The escaping is JSON.stringify's, which is valid Zig: it escapes every
55
+ // backslash, so the JSON's own `\uXXXX` reaches the file as `\\uXXXX` and
56
+ // no Zig escape sequence is ever formed from it.
57
+ if (asData) {
58
+ Content(`// Generated API configuration (mirrors go/rust core/config).
44
59
 
45
- const featureConfig: any = {}
46
- each(feature, (f: any) => {
47
- featureConfig[f.name] = f.config || {}
48
- })
60
+ const std = @import("std");
61
+ const h = @import("helpers.zig");
62
+ const types = @import("types.zig");
63
+ const jsonparse = @import("../utility/jsonparse.zig");
64
+ const Value = h.Value;
65
+ const Feature = types.Feature;
49
66
 
50
- const entityOptions: any = {}
51
- each(entity, (ent: any) => {
52
- entityOptions[ent.name] = {}
53
- })
67
+ /// THE API MODEL, EMBEDDED AS DATA (sdkgen rung L1).
68
+ ///
69
+ /// Emitted only above a size threshold, or when \`main.kit.config.repr\` pins
70
+ /// it: for a small model the literal is smaller and far easier to read when
71
+ /// debugging.
72
+ const CONFIG_DATA: []const u8 = ${JSON.stringify(configJson)};
54
73
 
55
- const options: any = {
56
- base: baseUrl,
57
- headers,
58
- entity: entityOptions,
59
- }
60
- if (authActive) {
61
- options.auth = { prefix: authPrefix }
62
- }
63
-
64
- const entityConfig = Object.values(entity || {}).reduce((a: any, n: any) => (
65
- a[n.name] = clean({
66
- fields: n.fields,
67
- name: n.name,
68
- op: n.op,
69
- relations: n.relations,
70
- }, true), a), {})
71
-
72
- const config = {
73
- main: { name: model.const.Name },
74
- feature: featureConfig,
75
- options,
76
- entity: entityConfig,
77
- }
74
+ pub fn make_config() Value {
75
+ // Unreachable on error: the constant is generated by a JSON serialiser and
76
+ // never edited by hand. Panicking beats returning an empty config, which
77
+ // would fail far from the cause.
78
+ return jsonparse.json_parse(CONFIG_DATA) catch
79
+ @panic("${model.const.Name}: embedded config is not valid JSON");
80
+ }
78
81
 
79
- File({ name: 'config.' + target.ext }, () => {
82
+ pub fn make_feature(name: []const u8) Feature {
83
+ `)
84
+ }
85
+ else {
80
86
 
81
87
  Content(`// Generated API configuration (mirrors go/rust core/config).
82
88
 
@@ -92,6 +98,7 @@ pub fn make_config() Value {
92
98
 
93
99
  pub fn make_feature(name: []const u8) Feature {
94
100
  `)
101
+ }
95
102
 
96
103
  // The factory must be able to instantiate any built-in feature by name,
97
104
  // not just the ones the current API model configures — a caller can enable
@@ -577,7 +577,16 @@ The OpenAPI spec(s) this SDK was generated from are kept in the
577
577
 
578
578
  `)
579
579
  if (upstreamUrl) {
580
- Content(`- Upstream API: [${upstreamUrl}](${upstreamUrl})
580
+ // A per-tenant server URL carries an OpenAPI server variable —
581
+ // `https://{instance}.dreamapply.com/api` — and is the right thing to
582
+ // SHOW, because it tells the reader the host is theirs to fill in. It is
583
+ // not a thing to LINK: the braces are not a resolvable address, so a
584
+ // markdown link renders as clickable and dead. Show it as code instead.
585
+ const templated = /[{}]/.test(upstreamUrl)
586
+ Content(templated
587
+ ? `- Upstream API: \`${upstreamUrl}\`
588
+ `
589
+ : `- Upstream API: [${upstreamUrl}](${upstreamUrl})
581
590
  `)
582
591
  }
583
592
  if (docsUrl && docsUrl !== upstreamUrl) {
@@ -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
@@ -19,7 +19,9 @@ import type {
19
19
  ActionResult,
20
20
  } from './types'
21
21
 
22
- import { SdkGenError, requirePath, isAuthActive, resolveAuthPrefix } from './utility'
22
+ import { SdkGenError, requirePath, isAuthActive, resolveAuthPrefix,
23
+ CONFIG_DATA_THRESHOLD, CONFIG_REPR_VALUES, isConfigData, configRepr,
24
+ configReprSetting, configDefinition, clean, rawStringLiteral } from './utility'
23
25
 
24
26
  import { Main } from './cmp/Main'
25
27
  import { ExternalTarget } from './cmp/ExternalTarget'
@@ -776,6 +778,14 @@ export {
776
778
  requirePath,
777
779
  isAuthActive,
778
780
  resolveAuthPrefix,
781
+ CONFIG_DATA_THRESHOLD,
782
+ CONFIG_REPR_VALUES,
783
+ isConfigData,
784
+ configRepr,
785
+ configReprSetting,
786
+ configDefinition,
787
+ clean,
788
+ rawStringLiteral,
779
789
 
780
790
  // Scaffold components need this to fail a generation with an actionable
781
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
  //
@@ -96,4 +98,227 @@ export {
96
98
  isAuthActive,
97
99
  resolveAuthPrefix,
98
100
  SdkGenError,
101
+ CONFIG_DATA_THRESHOLD,
102
+ CONFIG_REPR_VALUES,
103
+ isConfigData,
104
+ configRepr,
105
+ configReprSetting,
106
+ configDefinition,
107
+ clean,
108
+ rawStringLiteral,
109
+ }
110
+
111
+
112
+ // CONFIG REPRESENTATION (design rung L1, threshold from design Q7).
113
+ //
114
+ // Above a size threshold the API model is emitted as DATA - a JSON string
115
+ // constant parsed once - rather than as a composite literal. Below it the
116
+ // literal stays, because for a small model the literal is smaller, simpler,
117
+ // faster to load and far easier to debug, and a symbol table would be pure
118
+ // complexity.
119
+ //
120
+ // The threshold is on the JSON, not the emitted source, because the emitted
121
+ // source size varies by language while the model does not. It is measured in
122
+ // UTF-8 BYTES rather than string length: `.length` counts UTF-16 code units,
123
+ // so a CJK-heavy model would read as roughly a third of its real size and
124
+ // stay on the expensive literal path well past the point where it hurts.
125
+ //
126
+ // Measured on the real gitlab model (923.5 KB of JSON), Go, cold cache,
127
+ // recompiling only the config package:
128
+ //
129
+ // composite literal JSON string constant
130
+ // compile+link wall 30.80 s 0.34 s 91x faster
131
+ // peak compiler RSS 2.49 GB 0.06 GB 39x less
132
+ // binary 7.44 MB 3.51 MB 2.1x smaller
133
+ //
134
+ // The reader side is unchanged either way: make_config returns the same map,
135
+ // so nothing downstream can tell which representation it got.
136
+ const CONFIG_DATA_THRESHOLD = 256 * 1024
137
+
138
+
139
+ // Should this model be emitted as data rather than as a literal?
140
+ //
141
+ // `repr` is the per-SDK override from `main.kit.config.repr`: 'auto' (the
142
+ // default) decides by size, 'data' and 'literal' pin it. The override is what
143
+ // lets a small fixture exercise the data path - by size alone no test model
144
+ // comes near the threshold, so the branch every large SDK depends on would
145
+ // never be generated, compiled or run in CI.
146
+ const CONFIG_REPR_VALUES = ['auto', 'data', 'literal']
147
+
148
+ function isConfigData(configJson: string, repr?: string): boolean {
149
+ // An unknown value is REJECTED, not ignored. The aontu declaration
150
+ // documents the closed set but does not enforce it here, and silently
151
+ // treating `repr: 'date'` as `auto` would quietly restore the compile cost
152
+ // this exists to remove - the failure mode being a slow build nobody
153
+ // connects to a typo.
154
+ if (null != repr && '' !== repr && !CONFIG_REPR_VALUES.includes(repr)) {
155
+ throw new SdkGenError(
156
+ 'sdkgen: main.kit.config.repr must be one of ' +
157
+ CONFIG_REPR_VALUES.join(', ') + ' (got: ' + repr + ')', {})
158
+ }
159
+ if ('data' === repr) {
160
+ return true
161
+ }
162
+ if ('literal' === repr) {
163
+ return false
164
+ }
165
+ return CONFIG_DATA_THRESHOLD < Buffer.byteLength(configJson, 'utf8')
166
+ }
167
+
168
+
169
+ // The chosen representation, as a word - for generation logs and for the
170
+ // per-SDK reporting the fleet regen needs, so a model crossing the threshold
171
+ // is visible rather than showing up as an unexplained whole-file diff.
172
+ function configRepr(configJson: string, repr?: string): string {
173
+ return isConfigData(configJson, repr) ? 'data' : 'literal'
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) }
99
324
  }