@voxgig/sdkgen 1.3.4 → 1.3.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 (90) hide show
  1. package/bin/voxgig-sdkgen +1 -1
  2. package/dist/cmp/ReadmeErrors.js +15 -15
  3. package/dist/cmp/ReadmeErrors.js.map +1 -1
  4. package/dist/cmp/ReadmeExplanation.js +37 -35
  5. package/dist/cmp/ReadmeExplanation.js.map +1 -1
  6. package/dist/cmp/ReadmeTop.js +58 -29
  7. package/dist/cmp/ReadmeTop.js.map +1 -1
  8. package/dist/helpers/naming.js +29 -2
  9. package/dist/helpers/naming.js.map +1 -1
  10. package/dist/helpers/opShape.d.ts +6 -1
  11. package/dist/helpers/opShape.js +78 -0
  12. package/dist/helpers/opShape.js.map +1 -1
  13. package/dist/sdkgen.d.ts +2 -2
  14. package/dist/sdkgen.js +3 -1
  15. package/dist/sdkgen.js.map +1 -1
  16. package/package.json +1 -1
  17. package/project/.sdk/src/cmp/go/EntityOperation_go.ts +8 -1
  18. package/project/.sdk/src/cmp/go/Entity_go.ts +20 -3
  19. package/project/.sdk/src/cmp/go/Main_go.ts +2 -1
  20. package/project/.sdk/src/cmp/go/ReadmeEntity_go.ts +5 -5
  21. package/project/.sdk/src/cmp/go/ReadmeHowto_go.ts +23 -12
  22. package/project/.sdk/src/cmp/go/ReadmeModel_go.ts +27 -17
  23. package/project/.sdk/src/cmp/go/ReadmeRef_go.ts +5 -5
  24. package/project/.sdk/src/cmp/go/ReadmeTopTest_go.ts +5 -6
  25. package/project/.sdk/src/cmp/go/fragment/Entity.fragment.go +11 -11
  26. package/project/.sdk/src/cmp/go/fragment/EntityCreateOp.fragment.go +2 -2
  27. package/project/.sdk/src/cmp/go/fragment/EntityListOp.fragment.go +2 -2
  28. package/project/.sdk/src/cmp/go/fragment/EntityLoadOp.fragment.go +2 -2
  29. package/project/.sdk/src/cmp/go/fragment/EntityRemoveOp.fragment.go +2 -2
  30. package/project/.sdk/src/cmp/go/fragment/EntityUpdateOp.fragment.go +2 -2
  31. package/project/.sdk/src/cmp/js/Entity_js.ts +18 -2
  32. package/project/.sdk/src/cmp/js/MainEntity_js.ts +12 -2
  33. package/project/.sdk/src/cmp/js/Main_js.ts +5 -2
  34. package/project/.sdk/src/cmp/js/ReadmeEntity_js.ts +5 -5
  35. package/project/.sdk/src/cmp/js/ReadmeHowto_js.ts +37 -26
  36. package/project/.sdk/src/cmp/js/ReadmeRef_js.ts +5 -5
  37. package/project/.sdk/src/cmp/js/ReadmeTopTest_js.ts +5 -6
  38. package/project/.sdk/src/cmp/js/fragment/Entity.fragment.js +3 -3
  39. package/project/.sdk/src/cmp/lua/EntityOperation_lua.ts +6 -1
  40. package/project/.sdk/src/cmp/lua/Entity_lua.ts +20 -2
  41. package/project/.sdk/src/cmp/lua/ReadmeEntity_lua.ts +8 -5
  42. package/project/.sdk/src/cmp/lua/ReadmeHowto_lua.ts +14 -7
  43. package/project/.sdk/src/cmp/lua/ReadmeQuick_lua.ts +9 -6
  44. package/project/.sdk/src/cmp/lua/ReadmeRef_lua.ts +5 -2
  45. package/project/.sdk/src/cmp/lua/ReadmeTopQuick_lua.ts +8 -5
  46. package/project/.sdk/src/cmp/lua/ReadmeTopTest_lua.ts +5 -6
  47. package/project/.sdk/src/cmp/lua/fragment/Entity.fragment.lua +13 -13
  48. package/project/.sdk/src/cmp/lua/fragment/EntityCreateOp.fragment.lua +1 -1
  49. package/project/.sdk/src/cmp/lua/fragment/EntityListOp.fragment.lua +1 -1
  50. package/project/.sdk/src/cmp/lua/fragment/EntityLoadOp.fragment.lua +1 -1
  51. package/project/.sdk/src/cmp/lua/fragment/EntityRemoveOp.fragment.lua +1 -1
  52. package/project/.sdk/src/cmp/lua/fragment/EntityUpdateOp.fragment.lua +1 -1
  53. package/project/.sdk/src/cmp/php/Entity_php.ts +17 -1
  54. package/project/.sdk/src/cmp/php/MainEntity_php.ts +12 -3
  55. package/project/.sdk/src/cmp/php/ReadmeHowto_php.ts +15 -8
  56. package/project/.sdk/src/cmp/php/ReadmeTopTest_php.ts +5 -6
  57. package/project/.sdk/src/cmp/php/fragment/Entity.fragment.php +2 -2
  58. package/project/.sdk/src/cmp/py/Entity_py.ts +18 -1
  59. package/project/.sdk/src/cmp/py/MainEntity_py.ts +14 -4
  60. package/project/.sdk/src/cmp/py/Main_py.ts +2 -1
  61. package/project/.sdk/src/cmp/py/ReadmeEntity_py.ts +8 -5
  62. package/project/.sdk/src/cmp/py/ReadmeHowto_py.ts +18 -8
  63. package/project/.sdk/src/cmp/py/ReadmeQuick_py.ts +10 -7
  64. package/project/.sdk/src/cmp/py/ReadmeRef_py.ts +7 -4
  65. package/project/.sdk/src/cmp/py/ReadmeTopQuick_py.ts +9 -6
  66. package/project/.sdk/src/cmp/py/ReadmeTopTest_py.ts +6 -7
  67. package/project/.sdk/src/cmp/py/fragment/Entity.fragment.py +2 -2
  68. package/project/.sdk/src/cmp/rb/Entity_rb.ts +17 -1
  69. package/project/.sdk/src/cmp/rb/MainEntity_rb.ts +11 -2
  70. package/project/.sdk/src/cmp/rb/ReadmeEntity_rb.ts +8 -5
  71. package/project/.sdk/src/cmp/rb/ReadmeHowto_rb.ts +20 -8
  72. package/project/.sdk/src/cmp/rb/ReadmeQuick_rb.ts +9 -6
  73. package/project/.sdk/src/cmp/rb/ReadmeRef_rb.ts +5 -2
  74. package/project/.sdk/src/cmp/rb/ReadmeTopQuick_rb.ts +8 -5
  75. package/project/.sdk/src/cmp/rb/ReadmeTopTest_rb.ts +10 -8
  76. package/project/.sdk/src/cmp/rb/fragment/Entity.fragment.rb +2 -2
  77. package/project/.sdk/src/cmp/ts/Entity_ts.ts +18 -2
  78. package/project/.sdk/src/cmp/ts/MainEntity_ts.ts +12 -2
  79. package/project/.sdk/src/cmp/ts/Main_ts.ts +5 -2
  80. package/project/.sdk/src/cmp/ts/ReadmeEntity_ts.ts +5 -5
  81. package/project/.sdk/src/cmp/ts/ReadmeHowto_ts.ts +36 -26
  82. package/project/.sdk/src/cmp/ts/ReadmeRef_ts.ts +4 -5
  83. package/project/.sdk/src/cmp/ts/ReadmeTopTest_ts.ts +5 -6
  84. package/project/.sdk/src/cmp/ts/fragment/Entity.fragment.ts +4 -4
  85. package/src/cmp/ReadmeErrors.ts +16 -17
  86. package/src/cmp/ReadmeExplanation.ts +37 -35
  87. package/src/cmp/ReadmeTop.ts +60 -31
  88. package/src/helpers/naming.ts +32 -2
  89. package/src/helpers/opShape.ts +89 -0
  90. package/src/sdkgen.ts +3 -1
@@ -6,7 +6,7 @@ import {
6
6
  getModelPath
7
7
  } from '../types'
8
8
 
9
- import { entityIdField, entityPrimaryOp } from '../helpers/opShape'
9
+ import { entityIdField, pickExampleEntity } from '../helpers/opShape'
10
10
  import { primaryOpCall } from '../helpers/opExample'
11
11
  import type { ExampleLang, PrimaryCall } from '../helpers/opExample'
12
12
  import { safeVarName } from '../helpers/naming'
@@ -224,29 +224,28 @@ const ReadmeErrors = cmp(function ReadmeErrors(props: any) {
224
224
 
225
225
  const lang = LANGS[target.name] || DEFAULT_LANG
226
226
 
227
- // Derive a real example entity from the model (the same way the sibling
228
- // Readme components do) so the snippet never references a phantom entity.
227
+ // Pick a real example entity WITH a real op (prefer a read op) so the
228
+ // error-handling snippet never references a phantom entity or fabricates a
229
+ // `.load()` on an op-less one (e.g. Cloudsmith's `Abort`). primaryOp is null
230
+ // only when NO entity exposes any op — then the entity example is skipped
231
+ // and only the direct() error handling (which every SDK has) is shown.
229
232
  const entity = getModelPath(model, `main.${KIT}.entity`, { only_active: false, required: false })
230
- const ex = Object.values(entity || {}).find((e: any) => e && e.active !== false) as any
231
- const eName = ex ? (ex.Name || (ex.name[0].toUpperCase() + ex.name.slice(1))) : 'Entity'
232
- // Sanitise the variable name against the target's reserved words (a `Delete`
233
- // entity must not bind `const delete = ...`).
234
- const eLower = safeVarName(eName.toLowerCase(), target.name)
235
-
236
- // The entity's id-like key field name, or null when it has none.
237
- const idF = entityIdField(ex)
238
- // The entity's PRIMARY op — an op it actually exposes (prefer list/load, else
239
- // create/update/remove). A create-only entity therefore never shows a
240
- // phantom `.load()`.
241
- const primaryOp = entityPrimaryOp(ex) || 'load'
242
- const call = primaryOpCall(target.name as ExampleLang, eName, eLower, primaryOp, idF, ex)
233
+ const { entity: ex, primaryOp } = pickExampleEntity(entity || {})
243
234
 
244
235
  Content(`
245
236
  ## Error handling
246
237
 
247
238
  `)
248
239
 
249
- Content(lang.entity(call, primaryOp))
240
+ if (ex && primaryOp) {
241
+ const eName = ex.Name || (ex.name[0].toUpperCase() + ex.name.slice(1))
242
+ // Sanitise the variable name against the target's reserved words (a
243
+ // `Delete` entity must not bind `const delete = ...`).
244
+ const eLower = safeVarName(eName.toLowerCase(), target.name)
245
+ const idF = entityIdField(ex)
246
+ const call = primaryOpCall(target.name as ExampleLang, eName, eLower, primaryOp, idF, ex)
247
+ Content(lang.entity(call, primaryOp))
248
+ }
250
249
 
251
250
  Content(lang.direct)
252
251
  })
@@ -8,7 +8,7 @@ import {
8
8
 
9
9
  import { requirePath } from '../utility'
10
10
 
11
- import { entityIdField, entityPrimaryOp } from '../helpers/opShape'
11
+ import { entityIdField, pickExampleEntity } from '../helpers/opShape'
12
12
  import { idLiteral, matchArg, dataArg } from '../helpers/opExample'
13
13
  import type { ExampleLang } from '../helpers/opExample'
14
14
  import { safeVarName } from '../helpers/naming'
@@ -225,39 +225,40 @@ const ReadmeExplanation = cmp(function ReadmeExplanation(props: any) {
225
225
  const feature = getModelPath(model, `main.${KIT}.feature`)
226
226
  const lang = LANGS[target.name] || DEFAULT_LANG
227
227
 
228
- // Derive a real example entity from the model (the same way the sibling
229
- // Readme components do) so the entity-state example never references a
230
- // phantom entity.
228
+ // Pick a real example entity WITH a real op (prefer a read op) so the
229
+ // entity-state example never references a phantom entity or fabricates a
230
+ // `.load()` on an op-less one (e.g. Cloudsmith's `Abort`). primaryOp is null
231
+ // only when NO entity exposes any op — then the entity-state section is
232
+ // skipped (a direct()-only SDK has no entity op to illustrate).
231
233
  const entity = getModelPath(model, `main.${KIT}.entity`, { only_active: false, required: false })
232
- const ex = Object.values(entity || {}).find((e: any) => e && e.active !== false) as any
233
- const eName = ex ? (ex.Name || (ex.name[0].toUpperCase() + ex.name.slice(1))) : 'Entity'
234
- // Sanitise against the target's reserved words (a `Delete` entity must not
235
- // bind `const delete = ...`).
236
- const eLower = safeVarName(eName.toLowerCase(), target.name)
234
+ const { entity: ex, primaryOp } = pickExampleEntity(entity || {})
237
235
  const lname = target.name as ExampleLang
238
- // The entity's id-like key field name, or null when it has none (a
239
- // response-wrapped spec can model an entity with no id). Drives whether the
240
- // state example keys on an id at all.
241
- const idF = entityIdField(ex)
242
- // The entity's PRIMARY op an op it actually exposes (never a hardcoded
243
- // `load` a create-only entity lacks).
244
- const primaryOp = entityPrimaryOp(ex) || 'load'
245
- const isMatchOp = 'load' === primaryOp || 'remove' === primaryOp
246
- // Type-correct example id literal (numeric when the id param is integer-typed),
247
- // derived from the OP's param type so an id carried only in the match compiles.
248
- const idLit = idLiteral(ex, primaryOp, idF)
249
- // Language-correct call argument for the primary op: a match for load/remove,
250
- // a required-field body for create/update, nothing for list.
251
- let stateArg: string
252
- if ('list' === primaryOp) {
253
- stateArg = 'go' === target.name ? 'nil' : ''
254
- } else if (isMatchOp) {
255
- stateArg = matchArg(lname, idF, idLit)
256
- } else {
257
- stateArg = dataArg(lname, ex, primaryOp, idF)
236
+ const hasEntityExample = !!(ex && primaryOp)
237
+
238
+ let eName = 'Entity', eLower = 'entity', stateArg = '', matchIdF: string | null = null, idLit = ''
239
+ if (hasEntityExample) {
240
+ eName = ex.Name || (ex.name[0].toUpperCase() + ex.name.slice(1))
241
+ // Sanitise against the target's reserved words (a `Delete` entity must
242
+ // not bind `const delete = ...`).
243
+ eLower = safeVarName(eName.toLowerCase(), target.name)
244
+ const idF = entityIdField(ex)
245
+ const isMatchOp = 'load' === primaryOp || 'remove' === primaryOp
246
+ // Type-correct example id literal (numeric when the id param is integer-
247
+ // typed), derived from the OP's param type so an id carried only in the
248
+ // match compiles.
249
+ idLit = idLiteral(ex, primaryOp as string, idF)
250
+ // Language-correct call argument for the primary op: a match for
251
+ // load/remove, a required-field body for create/update, nothing for list.
252
+ if ('list' === primaryOp) {
253
+ stateArg = 'go' === target.name ? 'nil' : ''
254
+ } else if (isMatchOp) {
255
+ stateArg = matchArg(lname, idF, idLit)
256
+ } else {
257
+ stateArg = dataArg(lname, ex, primaryOp as string, idF)
258
+ }
259
+ // Only a match op keys the `.match()` comment on `{ id: ... }`.
260
+ matchIdF = isMatchOp ? idF : null
258
261
  }
259
- // Only a match op keys the `.match()` comment on `{ id: ... }`.
260
- const matchIdF = isMatchOp ? idF : null
261
262
 
262
263
  Content(`
263
264
  ## Advanced
@@ -327,12 +328,13 @@ were added, so later features can override earlier ones.
327
328
  }
328
329
 
329
330
 
330
- // Entity state
331
- Content(`### Entity state
331
+ // Entity state — only when the SDK actually has an entity op to show.
332
+ if (hasEntityExample) {
333
+ Content(`### Entity state
332
334
 
333
335
  `)
334
-
335
- Content(lang.entityState(eName, eLower, primaryOp, stateArg, matchIdF, idLit))
336
+ Content(lang.entityState(eName, eLower, primaryOp as string, stateArg, matchIdF, idLit))
337
+ }
336
338
 
337
339
 
338
340
  // Direct vs entity access
@@ -25,6 +25,7 @@ import {
25
25
 
26
26
 
27
27
  const SDKGEN_REPO = 'https://github.com/voxgig/sdkgen'
28
+ const VOXGIG_SDK = 'https://voxgig.com/sdk/'
28
29
 
29
30
 
30
31
  // A type-correct TS example literal for a model field, keyed off its canonical
@@ -97,6 +98,16 @@ const ReadmeTop = cmp(function ReadmeTop(props: any) {
97
98
  `[${apiWebsite.replace(/^https?:\/\//, '').replace(/\/$/, '')}](${apiWebsite}).`
98
99
  : ''
99
100
 
101
+ // Attribution for API metadata sourced from a third-party catalogue (e.g.
102
+ // freepublicapis.com). Rendered only when the model carries an
103
+ // `info.meta_source` URL, so first-party SDKs never show it. The link points
104
+ // back to the catalogue that kindly supplied the metadata.
105
+ const metaSource = (info.meta_source || '').trim()
106
+ const metaSourceLine = metaSource
107
+ ? `Meta data kindly supplied by ` +
108
+ `[${metaSource.replace(/^https?:\/\//, '').replace(/\/$/, '')}](${metaSource}).`
109
+ : ''
110
+
100
111
  const entity = getModelPath(model, `main.${KIT}.entity`)
101
112
  const target = getModelPath(model, `main.${KIT}.target`)
102
113
  const feature = getModelPath(model, `main.${KIT}.feature`)
@@ -118,16 +129,25 @@ const ReadmeTop = cmp(function ReadmeTop(props: any) {
118
129
  const docsOrder: string[] = (getModelPath(model, `main.${KIT}.config.docs_order`,
119
130
  { only_active: false, required: false }) as any) || []
120
131
 
132
+ const orderOf = (name: string): number => {
133
+ const i = docsOrder.indexOf(name)
134
+ if (i !== -1) return i
135
+ // Unlisted targets go after the languages; keep the CLI then MCP last.
136
+ if (name === 'go-cli') return docsOrder.length + 1
137
+ if (name === 'go-mcp') return docsOrder.length + 2
138
+ return docsOrder.length
139
+ }
140
+
121
141
  const sdkTargets = activeTargets
122
142
  .filter((t: any) => t.name !== 'go-cli' && t.name !== 'go-mcp')
123
143
  .slice()
124
- .sort((a: any, b: any) => {
125
- const ai = docsOrder.indexOf(a.name)
126
- const bi = docsOrder.indexOf(b.name)
127
- const av = ai === -1 ? docsOrder.length : ai
128
- const bv = bi === -1 ? docsOrder.length : bi
129
- return av - bv
130
- })
144
+ .sort((a: any, b: any) => orderOf(a.name) - orderOf(b.name))
145
+
146
+ // Every installable port for the Packages table — including the go-cli and
147
+ // go-mcp binaries, which publish as Go modules via git tags.
148
+ const pkgTargets = activeTargets
149
+ .slice()
150
+ .sort((a: any, b: any) => orderOf(a.name) - orderOf(b.name))
131
151
 
132
152
  const langList = sdkTargets.map((t: any) => t.title).join(', ')
133
153
  const leadTarget = pickLeadTarget(sdkTargets)
@@ -153,7 +173,14 @@ ${tagline}
153
173
  }
154
174
  Content(`${nonAffiliation(model)}
155
175
 
176
+ Learn more about Voxgig SDKs at [voxgig.com/sdk](${VOXGIG_SDK}).
177
+
178
+ `)
179
+ if (metaSourceLine) {
180
+ Content(`${metaSourceLine}
181
+
156
182
  `)
183
+ }
157
184
 
158
185
  // Positioning line, only when we actually have multiple SDK targets.
159
186
  if (sdkTargets.length > 1) {
@@ -242,19 +269,43 @@ rather than reasoning about raw HTTP routes and query parameters.
242
269
  `)
243
270
  }
244
271
 
272
+ // 2c. Offline unit testing — a headline feature: every SDK ships a mock
273
+ // transport, so it belongs high up, right after the entity model.
274
+ if (sdkTargets.length > 0) {
275
+ Content(`## Offline unit testing
276
+
277
+ Every SDK ships a built-in **test mode** that swaps the HTTP transport for
278
+ an in-memory mock, so your unit tests run fully offline — no server, no
279
+ network, and no credentials:
280
+
281
+ `)
282
+ sdkTargets.forEach((tgt: any) => {
283
+ const Test =
284
+ requirePath(ctx$, `./cmp/${tgt.name}/ReadmeTopTest_${tgt.name}`, { ignore: true })
285
+ if (Test) {
286
+ Content(`### ${tgt.title}
287
+
288
+ `)
289
+ Test['ReadmeTopTest']({ target: tgt })
290
+ Content(`
291
+ `)
292
+ }
293
+ })
294
+ }
295
+
245
296
  // 3. Packages — real published package name + install command per
246
297
  // ecosystem. A package that is NOT yet live on its registry (the fleet
247
298
  // default: 'pending') must NOT advertise a `npm install ...` that 404s —
248
299
  // its Install cell links to the git-tag releases page instead. The go
249
300
  // family resolves from the tag directly (`go get <mod>@latest`).
250
- if (sdkTargets.length > 0) {
301
+ if (pkgTargets.length > 0) {
251
302
  const { releasesUrl } = repoInfo(model)
252
303
  Content(`## Packages
253
304
 
254
305
  | Language | Package | Install |
255
306
  | --- | --- | --- |
256
307
  `)
257
- sdkTargets.forEach((tgt: any) => {
308
+ pkgTargets.forEach((tgt: any) => {
258
309
  const state = registryState(model, tgt.name)
259
310
  let cell: string
260
311
  if ('active' === state) {
@@ -402,28 +453,6 @@ own list above for exactly which it supports.
402
453
  })
403
454
  }
404
455
 
405
- // 9. Testing — keep, but slim
406
- if (sdkTargets.length > 0) {
407
- Content(`## Unit testing in offline mode
408
-
409
- Every SDK ships a test mode that swaps the HTTP transport for an
410
- in-memory mock, so unit tests run offline.
411
-
412
- `)
413
- sdkTargets.forEach((tgt: any) => {
414
- const Test =
415
- requirePath(ctx$, `./cmp/${tgt.name}/ReadmeTopTest_${tgt.name}`, { ignore: true })
416
- if (Test) {
417
- Content(`### ${tgt.title}
418
-
419
- `)
420
- Test['ReadmeTopTest']({ target: tgt })
421
- Content(`
422
- `)
423
- }
424
- })
425
- }
426
-
427
456
  // 10. Direct and prepare — a common everyday task (the low-level
428
457
  // escape hatch for endpoints the entity model doesn't cover).
429
458
  Content(`## Direct and prepare
@@ -11,8 +11,11 @@
11
11
  // words a `const` binding trips on). Go lists its 25 keywords — a var named
12
12
  // `type`/`func`/`range` fails `go build`; Go builtins like `delete`/`len` are
13
13
  // NOT keywords and are legal identifiers, so they are intentionally omitted.
14
- // py/php/rb/lua rarely collide on a lowercased entity name and are not treated
15
- // here (add a set if a real collision surfaces).
14
+ // rb/py/lua list the keywords that are illegal as a local-variable binding
15
+ // (e.g. a `Self` entity -> `self = client.Self` is a Ruby SyntaxError, an
16
+ // `End` entity -> `end = ...` a Lua one). PHP variables are `$`-prefixed so a
17
+ // lowercased entity name never collides (only `$this` is special, and no
18
+ // entity is named `this`), so PHP is not treated.
16
19
 
17
20
  const JS_RESERVED = new Set<string>([
18
21
  'break', 'case', 'catch', 'class', 'const', 'continue', 'debugger',
@@ -32,10 +35,37 @@ const GO_RESERVED = new Set<string>([
32
35
  'var',
33
36
  ])
34
37
 
38
+ // Ruby keywords — illegal as a local-variable name (e.g. `self`, `end`).
39
+ const RB_RESERVED = new Set<string>([
40
+ 'alias', 'and', 'begin', 'break', 'case', 'class', 'def', 'defined?',
41
+ 'do', 'else', 'elsif', 'end', 'ensure', 'false', 'for', 'if', 'in',
42
+ 'module', 'next', 'nil', 'not', 'or', 'redo', 'rescue', 'retry', 'return',
43
+ 'self', 'super', 'then', 'true', 'undef', 'unless', 'until', 'when',
44
+ 'while', 'yield', '__FILE__', '__LINE__', '__ENCODING__', 'BEGIN', 'END',
45
+ ])
46
+
47
+ // Python keywords — illegal as a variable name.
48
+ const PY_RESERVED = new Set<string>([
49
+ 'False', 'None', 'True', 'and', 'as', 'assert', 'async', 'await', 'break',
50
+ 'class', 'continue', 'def', 'del', 'elif', 'else', 'except', 'finally',
51
+ 'for', 'from', 'global', 'if', 'import', 'in', 'is', 'lambda', 'nonlocal',
52
+ 'not', 'or', 'pass', 'raise', 'return', 'try', 'while', 'with', 'yield',
53
+ ])
54
+
55
+ // Lua keywords — illegal as a variable name. (`self` is NOT reserved in Lua.)
56
+ const LUA_RESERVED = new Set<string>([
57
+ 'and', 'break', 'do', 'else', 'elseif', 'end', 'false', 'for', 'function',
58
+ 'goto', 'if', 'in', 'local', 'nil', 'not', 'or', 'repeat', 'return',
59
+ 'then', 'true', 'until', 'while',
60
+ ])
61
+
35
62
  const RESERVED: Record<string, Set<string>> = {
36
63
  ts: JS_RESERVED,
37
64
  js: JS_RESERVED,
38
65
  go: GO_RESERVED,
66
+ rb: RB_RESERVED,
67
+ py: PY_RESERVED,
68
+ lua: LUA_RESERVED,
39
69
  }
40
70
 
41
71
 
@@ -238,6 +238,93 @@ function entityPrimaryOp(ent: any): string | null {
238
238
  }
239
239
 
240
240
 
241
+ // Collision-free target-language CLASS name for an entity. The natural
242
+ // class name is `<Name>Entity`, but that can equal another entity's
243
+ // canonical DATA-type name `<Name'>` when Name' === Name + 'Entity' (e.g.
244
+ // GitLab's `project` -> class `ProjectEntity` collides with `project_entity`
245
+ // -> data type `ProjectEntity`), which redeclares a type in Go and merges/
246
+ // shadows it in ts/py/rb. This assigns each entity a class name that is
247
+ // unique across ALL emitted top-level type names (every entity's data type
248
+ // and per-op Match/Data types, plus already-assigned class names): the
249
+ // natural `<Name>Entity` when free, else `<Name>EntityClient`, `...Client2`,
250
+ // … The DATA type keeps its canonical `<Name>` — only the suffixed class
251
+ // yields. Deterministic (sorted-key iteration) and stable across runs.
252
+ //
253
+ // Memoised per entity-collection object so the O(n) assignment runs once.
254
+ const _classNameCache = new WeakMap<object, Record<string, string>>()
255
+
256
+ function entityClassNames(entityColl: any): Record<string, string> {
257
+ const cached = _classNameCache.get(entityColl)
258
+ if (null != cached) {
259
+ return cached
260
+ }
261
+
262
+ const ents = each(entityColl).filter((e: any) => e && e.active !== false)
263
+
264
+ // 1. Every top-level DATA-type name the target emits.
265
+ const taken: Record<string, boolean> = {}
266
+ ents.forEach((e: any) => {
267
+ taken[e.Name] = true
268
+ for (const op of ['load', 'list', 'create', 'update', 'remove']) {
269
+ if (e.op && e.op[op]) {
270
+ taken[opTypeName(e.Name, op)] = true
271
+ }
272
+ }
273
+ })
274
+
275
+ // 2. Assign each class name, avoiding all data types and prior classes.
276
+ const out: Record<string, string> = {}
277
+ ents.forEach((e: any) => {
278
+ let name = e.Name + 'Entity'
279
+ if (taken[name]) {
280
+ const base = name + 'Client'
281
+ name = base
282
+ let n = 1
283
+ while (taken[name]) {
284
+ n++
285
+ name = base + n
286
+ }
287
+ }
288
+ taken[name] = true
289
+ out[e.name] = name
290
+ })
291
+
292
+ _classNameCache.set(entityColl, out)
293
+ return out
294
+ }
295
+
296
+
297
+ // The collision-free class name for one entity (see entityClassNames).
298
+ // `entityColl` is main.<KIT>.entity (the collection the entity belongs to).
299
+ function entityClassName(ent: any, entityColl: any): string {
300
+ if (null == ent) {
301
+ return ''
302
+ }
303
+ const map = entityClassNames(entityColl)
304
+ return map[ent.name] || (ent.Name + 'Entity')
305
+ }
306
+
307
+
308
+ // Pick a representative entity for a single illustrative snippet (e.g. the
309
+ // README's test-mode example): the first ACTIVE entity that exposes a read
310
+ // op (list/load) so the snippet is meaningful, else the first with ANY
311
+ // active op, else the first active entity. Returns { entity, primaryOp }
312
+ // where primaryOp is null only when NO entity has an op — so callers never
313
+ // fabricate an op the entity lacks (the cause of `.load()` on an op-less
314
+ // entity like Cloudsmith's `Abort`). Entities iterate in sorted-key order
315
+ // for deterministic output.
316
+ function pickExampleEntity(entity: any): { entity: any, primaryOp: string | null } {
317
+ const actives = each(entity).filter((e: any) => e && e.active !== false)
318
+ const readable = actives.find((e: any) => {
319
+ const op = entityPrimaryOp(e)
320
+ return 'list' === op || 'load' === op
321
+ })
322
+ const withOp = actives.find((e: any) => null != entityPrimaryOp(e))
323
+ const chosen = readable || withOp || actives[0] || null
324
+ return { entity: chosen, primaryOp: null == chosen ? null : entityPrimaryOp(chosen) }
325
+ }
326
+
327
+
241
328
  // The id field on the entity's DATA type (its fields[]), or null. DISTINCT from
242
329
  // entityIdField (the load-MATCH key): an API can model a load match that carries
243
330
  // an `id` param while the response entity itself has no `id` field, so `.id`
@@ -267,6 +354,8 @@ export {
267
354
  entityDataIdField,
268
355
  entityOps,
269
356
  entityPrimaryOp,
357
+ pickExampleEntity,
358
+ entityClassName,
270
359
  }
271
360
 
272
361
  export type {
package/src/sdkgen.ts CHANGED
@@ -48,7 +48,7 @@ import { getMatchEntries } from './helpers/getMatchEntries'
48
48
  import { collectDeps } from './helpers/collectDeps'
49
49
  import type { DepEntry } from './helpers/collectDeps'
50
50
  import { canonToType, canonKey } from './helpers/canonType'
51
- import { OP_SUFFIX, opTypeName, opParams, opRequestShape, entityIdField, entityDataIdField, entityOps, entityPrimaryOp } from './helpers/opShape'
51
+ import { OP_SUFFIX, opTypeName, opParams, opRequestShape, entityIdField, entityDataIdField, entityOps, entityPrimaryOp, pickExampleEntity, entityClassName } from './helpers/opShape'
52
52
  import { isReservedName, safeVarName } from './helpers/naming'
53
53
  import {
54
54
  packageName,
@@ -448,6 +448,8 @@ export {
448
448
  entityDataIdField,
449
449
  entityOps,
450
450
  entityPrimaryOp,
451
+ pickExampleEntity,
452
+ entityClassName,
451
453
  isReservedName,
452
454
  safeVarName,
453
455