@voxgig/sdkgen 1.3.4 → 1.3.5

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 (87) 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/helpers/naming.js +29 -2
  7. package/dist/helpers/naming.js.map +1 -1
  8. package/dist/helpers/opShape.d.ts +6 -1
  9. package/dist/helpers/opShape.js +78 -0
  10. package/dist/helpers/opShape.js.map +1 -1
  11. package/dist/sdkgen.d.ts +2 -2
  12. package/dist/sdkgen.js +3 -1
  13. package/dist/sdkgen.js.map +1 -1
  14. package/package.json +1 -1
  15. package/project/.sdk/src/cmp/go/EntityOperation_go.ts +8 -1
  16. package/project/.sdk/src/cmp/go/Entity_go.ts +20 -3
  17. package/project/.sdk/src/cmp/go/Main_go.ts +2 -1
  18. package/project/.sdk/src/cmp/go/ReadmeEntity_go.ts +5 -5
  19. package/project/.sdk/src/cmp/go/ReadmeHowto_go.ts +23 -12
  20. package/project/.sdk/src/cmp/go/ReadmeModel_go.ts +27 -17
  21. package/project/.sdk/src/cmp/go/ReadmeRef_go.ts +5 -5
  22. package/project/.sdk/src/cmp/go/ReadmeTopTest_go.ts +5 -6
  23. package/project/.sdk/src/cmp/go/fragment/Entity.fragment.go +11 -11
  24. package/project/.sdk/src/cmp/go/fragment/EntityCreateOp.fragment.go +2 -2
  25. package/project/.sdk/src/cmp/go/fragment/EntityListOp.fragment.go +2 -2
  26. package/project/.sdk/src/cmp/go/fragment/EntityLoadOp.fragment.go +2 -2
  27. package/project/.sdk/src/cmp/go/fragment/EntityRemoveOp.fragment.go +2 -2
  28. package/project/.sdk/src/cmp/go/fragment/EntityUpdateOp.fragment.go +2 -2
  29. package/project/.sdk/src/cmp/js/Entity_js.ts +18 -2
  30. package/project/.sdk/src/cmp/js/MainEntity_js.ts +12 -2
  31. package/project/.sdk/src/cmp/js/Main_js.ts +5 -2
  32. package/project/.sdk/src/cmp/js/ReadmeEntity_js.ts +5 -5
  33. package/project/.sdk/src/cmp/js/ReadmeHowto_js.ts +37 -26
  34. package/project/.sdk/src/cmp/js/ReadmeRef_js.ts +5 -5
  35. package/project/.sdk/src/cmp/js/ReadmeTopTest_js.ts +5 -6
  36. package/project/.sdk/src/cmp/js/fragment/Entity.fragment.js +3 -3
  37. package/project/.sdk/src/cmp/lua/EntityOperation_lua.ts +6 -1
  38. package/project/.sdk/src/cmp/lua/Entity_lua.ts +20 -2
  39. package/project/.sdk/src/cmp/lua/ReadmeEntity_lua.ts +8 -5
  40. package/project/.sdk/src/cmp/lua/ReadmeHowto_lua.ts +14 -7
  41. package/project/.sdk/src/cmp/lua/ReadmeQuick_lua.ts +9 -6
  42. package/project/.sdk/src/cmp/lua/ReadmeRef_lua.ts +5 -2
  43. package/project/.sdk/src/cmp/lua/ReadmeTopQuick_lua.ts +8 -5
  44. package/project/.sdk/src/cmp/lua/ReadmeTopTest_lua.ts +5 -6
  45. package/project/.sdk/src/cmp/lua/fragment/Entity.fragment.lua +13 -13
  46. package/project/.sdk/src/cmp/lua/fragment/EntityCreateOp.fragment.lua +1 -1
  47. package/project/.sdk/src/cmp/lua/fragment/EntityListOp.fragment.lua +1 -1
  48. package/project/.sdk/src/cmp/lua/fragment/EntityLoadOp.fragment.lua +1 -1
  49. package/project/.sdk/src/cmp/lua/fragment/EntityRemoveOp.fragment.lua +1 -1
  50. package/project/.sdk/src/cmp/lua/fragment/EntityUpdateOp.fragment.lua +1 -1
  51. package/project/.sdk/src/cmp/php/Entity_php.ts +17 -1
  52. package/project/.sdk/src/cmp/php/MainEntity_php.ts +12 -3
  53. package/project/.sdk/src/cmp/php/ReadmeHowto_php.ts +15 -8
  54. package/project/.sdk/src/cmp/php/ReadmeTopTest_php.ts +5 -6
  55. package/project/.sdk/src/cmp/php/fragment/Entity.fragment.php +2 -2
  56. package/project/.sdk/src/cmp/py/Entity_py.ts +18 -1
  57. package/project/.sdk/src/cmp/py/MainEntity_py.ts +14 -4
  58. package/project/.sdk/src/cmp/py/Main_py.ts +2 -1
  59. package/project/.sdk/src/cmp/py/ReadmeEntity_py.ts +8 -5
  60. package/project/.sdk/src/cmp/py/ReadmeHowto_py.ts +18 -8
  61. package/project/.sdk/src/cmp/py/ReadmeQuick_py.ts +10 -7
  62. package/project/.sdk/src/cmp/py/ReadmeRef_py.ts +7 -4
  63. package/project/.sdk/src/cmp/py/ReadmeTopQuick_py.ts +9 -6
  64. package/project/.sdk/src/cmp/py/ReadmeTopTest_py.ts +6 -7
  65. package/project/.sdk/src/cmp/py/fragment/Entity.fragment.py +2 -2
  66. package/project/.sdk/src/cmp/rb/Entity_rb.ts +17 -1
  67. package/project/.sdk/src/cmp/rb/MainEntity_rb.ts +11 -2
  68. package/project/.sdk/src/cmp/rb/ReadmeEntity_rb.ts +8 -5
  69. package/project/.sdk/src/cmp/rb/ReadmeHowto_rb.ts +20 -8
  70. package/project/.sdk/src/cmp/rb/ReadmeQuick_rb.ts +9 -6
  71. package/project/.sdk/src/cmp/rb/ReadmeRef_rb.ts +5 -2
  72. package/project/.sdk/src/cmp/rb/ReadmeTopQuick_rb.ts +8 -5
  73. package/project/.sdk/src/cmp/rb/ReadmeTopTest_rb.ts +10 -8
  74. package/project/.sdk/src/cmp/rb/fragment/Entity.fragment.rb +2 -2
  75. package/project/.sdk/src/cmp/ts/Entity_ts.ts +18 -2
  76. package/project/.sdk/src/cmp/ts/MainEntity_ts.ts +12 -2
  77. package/project/.sdk/src/cmp/ts/Main_ts.ts +5 -2
  78. package/project/.sdk/src/cmp/ts/ReadmeEntity_ts.ts +5 -5
  79. package/project/.sdk/src/cmp/ts/ReadmeHowto_ts.ts +36 -26
  80. package/project/.sdk/src/cmp/ts/ReadmeRef_ts.ts +4 -5
  81. package/project/.sdk/src/cmp/ts/ReadmeTopTest_ts.ts +5 -6
  82. package/project/.sdk/src/cmp/ts/fragment/Entity.fragment.ts +4 -4
  83. package/src/cmp/ReadmeErrors.ts +16 -17
  84. package/src/cmp/ReadmeExplanation.ts +37 -35
  85. package/src/helpers/naming.ts +32 -2
  86. package/src/helpers/opShape.ts +89 -0
  87. package/src/sdkgen.ts +3 -1
@@ -1,5 +1,5 @@
1
1
 
2
- import { cmp, each, Content, canonToType, canonKey, File, isAuthActive, entityIdField, opRequestShape } from '@voxgig/sdkgen'
2
+ import { cmp, each, Content, canonToType, canonKey, File, isAuthActive, entityIdField, opRequestShape, safeVarName } from '@voxgig/sdkgen'
3
3
 
4
4
  import {
5
5
  KIT,
@@ -172,6 +172,9 @@ same parameters as \`direct()\`. Raises on error.
172
172
  // Model-driven id key: null when this entity has no id-like field, in
173
173
  // which case load/remove match on no argument and update omits the id.
174
174
  const idF = entityIdField(ent)
175
+ // Sanitise the local variable name — an entity whose lowercased name is
176
+ // a Ruby keyword (e.g. `self`) would otherwise emit uncompilable code.
177
+ const eVar = safeVarName(ent.name, 'rb')
175
178
 
176
179
  Content(`
177
180
  ---
@@ -187,7 +190,7 @@ same parameters as \`direct()\`. Raises on error.
187
190
  }
188
191
 
189
192
  Content(`\`\`\`ruby
190
- ${ent.name} = client.${ent.Name}
193
+ ${eVar} = client.${ent.Name}
191
194
  \`\`\`
192
195
 
193
196
  `)
@@ -1,5 +1,5 @@
1
1
 
2
- import { cmp, Content, isAuthActive, envName, canonKey, entityIdField, opRequestShape } from '@voxgig/sdkgen'
2
+ import { cmp, Content, isAuthActive, envName, canonKey, entityIdField, opRequestShape, safeVarName } from '@voxgig/sdkgen'
3
3
 
4
4
  import {
5
5
  KIT,
@@ -43,6 +43,9 @@ client = ${ctor}
43
43
 
44
44
  if (exampleEntity) {
45
45
  const eName = nom(exampleEntity, 'Name')
46
+ // Sanitise the local variable name — an entity whose lowercased name is a
47
+ // Ruby keyword (e.g. `self`) would otherwise emit uncompilable code.
48
+ const eVar = safeVarName(eName.toLowerCase(), 'rb')
46
49
  const opnames = Object.keys(exampleEntity.op || {})
47
50
  // Model-driven id key: null when the entity has no id-like field, in which
48
51
  // case the load example takes no match argument.
@@ -52,8 +55,8 @@ client = ${ctor}
52
55
 
53
56
  if (opnames.includes('list')) {
54
57
  Content(`# List all ${eName.toLowerCase()}s (returns an Array; raises on error)
55
- ${eName.toLowerCase()}s = client.${eName}.list
56
- puts ${eName.toLowerCase()}s
58
+ ${eVar}s = client.${eName}.list
59
+ puts ${eVar}s
57
60
  `)
58
61
  hasCall = true
59
62
  }
@@ -73,8 +76,8 @@ puts ${eName.toLowerCase()}s
73
76
  : ''
74
77
  Content(`
75
78
  # Load a specific ${eName.toLowerCase()} (returns the bare record; raises on error)
76
- ${eName.toLowerCase()} = client.${eName}.load(${loadArg})
77
- puts ${eName.toLowerCase()}
79
+ ${eVar} = client.${eName}.load(${loadArg})
80
+ puts ${eVar}
78
81
  `)
79
82
  hasCall = true
80
83
  }
@@ -1,5 +1,5 @@
1
1
 
2
- import { cmp, Content, canonKey, entityIdField, entityPrimaryOp, opRequestShape } from '@voxgig/sdkgen'
2
+ import { cmp, Content, canonKey, entityIdField, pickExampleEntity, opRequestShape, safeVarName } from '@voxgig/sdkgen'
3
3
 
4
4
  import {
5
5
  KIT,
@@ -24,17 +24,16 @@ const ReadmeTopTest = cmp(function ReadmeTopTest(props: any) {
24
24
 
25
25
  const entity = getModelPath(model, `main.${KIT}.entity`)
26
26
 
27
- const exampleEntity = Object.values(entity).find((e: any) => e.active !== false) as any
27
+ // Pick an entity with a real op (prefer a read op) never fabricate a
28
+ // `load` on an op-less entity like Cloudsmith's `Abort`.
29
+ const { entity: exampleEntity, primaryOp } = pickExampleEntity(entity)
28
30
 
29
- if (exampleEntity) {
31
+ if (exampleEntity && primaryOp) {
30
32
  const eName = nom(exampleEntity, 'Name')
31
33
  const ename = eName.toLowerCase()
32
34
  // Model-driven id key: null when the entity has no id-like field, so the
33
35
  // seeded record carries no id and a match op takes no argument.
34
36
  const idF = entityIdField(exampleEntity)
35
- // Drive the test-mode example off the entity's PRIMARY op — never a
36
- // hardcoded `load` a create-only entity lacks.
37
- const primaryOp = entityPrimaryOp(exampleEntity) || 'load'
38
37
  const isMatchOp = 'load' === primaryOp || 'remove' === primaryOp
39
38
  const recBody = idF ? `{ "${idF}" => "test01" }` : '{}'
40
39
  let callArg = ''
@@ -56,8 +55,11 @@ const ReadmeTopTest = cmp(function ReadmeTopTest(props: any) {
56
55
  const chosen = required.length ? required : items.slice(0, 3)
57
56
  callArg = `{ ${chosen.map((it: any) => `"${it.name}" => ${rbLit(it.type)}`).join(', ')} }`
58
57
  }
59
- // A list result is an Array — name the variable accordingly.
60
- const eVar = ename + ('list' === primaryOp ? 's' : '')
58
+ // A list result is an Array — name the variable accordingly. Sanitise the
59
+ // base name an entity whose lowercased name is a Ruby keyword (e.g.
60
+ // `self`) would otherwise emit uncompilable code. The fixture KEY (`ename`)
61
+ // stays raw so the mock lookup resolves.
62
+ const eVar = safeVarName(ename, 'rb') + ('list' === primaryOp ? 's' : '')
61
63
  Content(`\`\`\`ruby
62
64
  # Seed fixture data so offline calls resolve without a live server.
63
65
  client = ${model.const.Name}SDK.test({
@@ -3,7 +3,7 @@
3
3
  require_relative '../utility/struct/voxgig_struct'
4
4
  require_relative '../core/helpers'
5
5
 
6
- class EntityNameEntity
6
+ class EntyClass
7
7
  def initialize(client, entopts = nil)
8
8
  entopts ||= {}
9
9
  if entopts["active"].nil?
@@ -35,7 +35,7 @@ class EntityNameEntity
35
35
 
36
36
  def make
37
37
  opts = @_entopts.dup
38
- EntityNameEntity.new(@_client, opts)
38
+ EntyClass.new(@_client, opts)
39
39
  end
40
40
 
41
41
  def data_set(args)
@@ -4,9 +4,14 @@ import * as Path from 'node:path'
4
4
  import {
5
5
  cmp, each, camelify, names,
6
6
  File, Content, Folder, Fragment, Line, FeatureHook, Slot,
7
- opTypeName,
7
+ opTypeName, entityClassName,
8
8
  } from '@voxgig/sdkgen'
9
9
 
10
+ import {
11
+ KIT,
12
+ getModelPath
13
+ } from '@voxgig/apidef'
14
+
10
15
  import { EntityOperation } from './EntityOperation_ts'
11
16
  // import { EntityTest } from './EntityTest_ts'
12
17
 
@@ -15,6 +20,13 @@ const Entity = cmp(function Entity(props: any) {
15
20
  const { model, stdrep } = props.ctx$
16
21
  const { target, entity } = props
17
22
 
23
+ // Collision-free entity CLASS name (see entityClassName): normally
24
+ // `<Name>Entity`, disambiguated when it would clash with another entity's
25
+ // data-type name. The DATA type stays `<Name>`. The class file name and the
26
+ // Main import path both use this, so they always agree.
27
+ const entityColl = getModelPath(model, `main.${KIT}.entity`)
28
+ const cls = entityClassName(entity, entityColl)
29
+
18
30
  const entrep = {
19
31
  ...stdrep,
20
32
  }
@@ -38,7 +50,7 @@ const Entity = cmp(function Entity(props: any) {
38
50
 
39
51
  Folder({ name: 'src/entity' }, () => {
40
52
 
41
- File({ name: entity.Name + 'Entity.' + target.name }, () => {
53
+ File({ name: cls + '.' + target.name }, () => {
42
54
 
43
55
  const opnames = Object.keys(entity.op)
44
56
 
@@ -58,6 +70,10 @@ const Entity = cmp(function Entity(props: any) {
58
70
  SdkName: model.const.Name,
59
71
  EntityName: entity.Name,
60
72
 
73
+ // Class token decoupled from the EntityName data-type token in
74
+ // Entity.fragment.ts so the class can be renamed independently.
75
+ EntyClass: cls,
76
+
61
77
  '#TypeImports': ({ indent }: any) => Content({ indent }, typeImport),
62
78
 
63
79
  '#Feature-Hook': ({ name, indent }: any) =>
@@ -1,16 +1,26 @@
1
1
 
2
2
 
3
- import { cmp, Content } from '@voxgig/sdkgen'
3
+ import { cmp, Content, entityClassName } from '@voxgig/sdkgen'
4
+
5
+ import {
6
+ KIT,
7
+ getModelPath
8
+ } from '@voxgig/apidef'
4
9
 
5
10
 
6
11
  const MainEntity = cmp(async function MainEntity(props: any) {
7
12
  const { entity } = props
13
+ const { model } = props.ctx$
14
+
15
+ // Return the collision-free class TYPE (entityClassName); the accessor METHOD
16
+ // name (entity.Name) is unchanged so callers still write client.<Name>().
17
+ const cls = entityClassName(entity, getModelPath(model, `main.${KIT}.entity`))
8
18
 
9
19
  Content(`
10
20
  // Entity access: \`client.${entity.Name}().list()\` / \`client.${entity.Name}().load({ id })\`.
11
21
  ${entity.Name}(data?: any) {
12
22
  const self = this
13
- return new ${entity.Name}Entity(self,data)
23
+ return new ${cls}(self,data)
14
24
  }
15
25
 
16
26
  `)
@@ -4,6 +4,7 @@ import * as Path from 'node:path'
4
4
  import {
5
5
  cmp, each, names, cmap,
6
6
  List, File, Content, Copy, Folder, Fragment, Line, FeatureHook,
7
+ entityClassName,
7
8
  } from '@voxgig/sdkgen'
8
9
 
9
10
 
@@ -55,8 +56,10 @@ const Main = cmp(async function Main(props: any) {
55
56
 
56
57
  Line(`// ${model.const.Name} ${target.Name} SDK\n`)
57
58
 
58
- List({ item: entity }, ({ item }: any) =>
59
- Line(`import { ${item.Name}Entity } from './entity/${item.Name}Entity'`))
59
+ List({ item: entity }, ({ item }: any) => {
60
+ const cls = entityClassName(item, entity)
61
+ return Line(`import { ${cls} } from './entity/${cls}'`)
62
+ })
60
63
 
61
64
  // Re-export the generated typed models so consumers can
62
65
  // `import { Advice, AdviceLoadMatch } from '<pkg>'`.
@@ -131,10 +131,10 @@ const ${eVar}s = await client.${entity.Name}().list()
131
131
  }
132
132
 
133
133
  if (opnames.includes('create')) {
134
- // Members come from the SAME shape that generates <Name>CreateData:
135
- // every required member must appear (the /* type */ placeholders also
136
- // mark the block as an illustration for the doc gates); an
137
- // all-optional create renders an empty still assignable literal.
134
+ // Members come from the SAME shape that generates <Name>CreateData
135
+ // (every required member appears), with a type-correct example VALUE
136
+ // via exampleValue a `name: /* type */` comment is not a value and
137
+ // yields invalid TS (TS1109), so the example must carry a real literal.
138
138
  const createItems = opRequestShape(entity, 'create').items
139
139
  .filter((it: any) => !it.optional)
140
140
  Content(`#### Example: Create
@@ -143,7 +143,7 @@ const ${eVar}s = await client.${entity.Name}().list()
143
143
  const ${eVar} = await client.${entity.Name}().create({
144
144
  `)
145
145
  createItems.map((it: any) => {
146
- Content(` ${it.name}: /* ${canonToType(it.type, target.name)} */,
146
+ Content(` ${it.name}: ${exampleValue(entity, entity.op && entity.op.create, it.name, 'example_' + it.name)},
147
147
  `)
148
148
  })
149
149
  Content(`})
@@ -1,5 +1,5 @@
1
1
 
2
- import { cmp, Content, isAuthActive, envName, entityIdField, entityDataIdField, entityPrimaryOp, opRequestShape, safeVarName } from '@voxgig/sdkgen'
2
+ import { cmp, Content, isAuthActive, envName, entityIdField, entityDataIdField, pickExampleEntity, opRequestShape, safeVarName } from '@voxgig/sdkgen'
3
3
 
4
4
  import {
5
5
  KIT,
@@ -14,15 +14,15 @@ const ReadmeHowto = cmp(function ReadmeHowto(props: any) {
14
14
  const { target, ctx$: { model } } = props
15
15
 
16
16
  const entity = getModelPath(model, `main.${KIT}.entity`)
17
- const exampleEntity = Object.values(entity || {}).find((e: any) => e && e.active !== false) as any
17
+ // Pick an entity with a real op (prefer a read op) never fabricate a
18
+ // `load` on an op-less entity like Cloudsmith's `Abort`. primaryOp is null
19
+ // only when NO entity exposes any op (a direct()-only SDK).
20
+ const { entity: exampleEntity, primaryOp } = pickExampleEntity(entity)
18
21
  const eName = exampleEntity ? nom(exampleEntity, 'Name') : 'Entity'
19
22
  // Variable-safe lowercase name (a `Delete` entity must not bind `delete`).
20
23
  const eVar = safeVarName(eName.toLowerCase(), 'ts')
21
24
 
22
- // Drive the test-mode / stateful examples off the entity's PRIMARY op an op
23
- // it actually exposes — never a hardcoded `load` a create-only entity lacks.
24
- const primaryOp = exampleEntity ? (entityPrimaryOp(exampleEntity) || 'load') : 'load'
25
- const primaryOpDef = exampleEntity && exampleEntity.op && exampleEntity.op[primaryOp]
25
+ const primaryOpDef = exampleEntity && primaryOp && exampleEntity.op && exampleEntity.op[primaryOp]
26
26
  const isMatchOp = 'load' === primaryOp || 'remove' === primaryOp
27
27
  // Model-driven id key: `idF` is the entity's id-like MATCH field name, or null
28
28
  // when it has none. `dataIdF` is the id on the RETURNED record's data type —
@@ -32,7 +32,7 @@ const ReadmeHowto = cmp(function ReadmeHowto(props: any) {
32
32
 
33
33
  // A type-correct, language-idiomatic argument for the primary op call.
34
34
  const primaryArg = (idPlaceholder: string): string => {
35
- if (!exampleEntity) return ''
35
+ if (!exampleEntity || !primaryOp) return ''
36
36
  if ('list' === primaryOp) return ''
37
37
  if (isMatchOp) {
38
38
  return idF ? `{ ${idF}: ${exampleValue(exampleEntity, primaryOpDef, idF, idPlaceholder)} }` : ''
@@ -53,6 +53,33 @@ const ReadmeHowto = cmp(function ReadmeHowto(props: any) {
53
53
  ? `console.log(data.${dataIdF})`
54
54
  : `console.log(data)`
55
55
 
56
+ // The op-driven example lines, shown only when the SDK has an entity op.
57
+ // A direct()-only SDK (no ops anywhere) shows a direct() test call instead.
58
+ const testModeExample = primaryOp
59
+ ? `const ${eVar} = await client.${eName}().${primaryOp}(${testCallArg})
60
+ // ${eVar} is a bare entity populated with mock response data
61
+ console.log(${eVar})`
62
+ : `const result = await client.direct({ path: '/api/resource', method: 'GET' })
63
+ console.log(result)`
64
+ const stateSection = primaryOp
65
+ ? `### Retain entity state across calls
66
+
67
+ Entity instances remember their last match and data:
68
+
69
+ \`\`\`ts
70
+ const entity = client.${eName}()
71
+
72
+ // First call runs the operation and stores its result
73
+ await entity.${primaryOp}(${stateCallArg})
74
+
75
+ // Subsequent calls reuse the stored state
76
+ const data = entity.data()
77
+ ${stateDataLine}
78
+ \`\`\`
79
+
80
+ `
81
+ : ''
82
+
56
83
  const authActive = isAuthActive(model)
57
84
  const apikeyTesterCtor = authActive
58
85
  ? `new ${model.const.Name}SDK({ apikey: '...' })`
@@ -102,9 +129,7 @@ Create a mock client for unit testing \u2014 no server required:
102
129
  \`\`\`ts
103
130
  const client = ${model.const.Name}SDK.test()
104
131
 
105
- const ${eVar} = await client.${eName}().${primaryOp}(${testCallArg})
106
- // ${eVar} is a bare entity populated with mock response data
107
- console.log(${eVar})
132
+ ${testModeExample}
108
133
  \`\`\`
109
134
 
110
135
  You can also use the instance method:
@@ -114,22 +139,7 @@ const client = ${apikeyTesterCtor}
114
139
  const testClient = client.tester()
115
140
  \`\`\`
116
141
 
117
- ### Retain entity state across calls
118
-
119
- Entity instances remember their last match and data:
120
-
121
- \`\`\`ts
122
- const entity = client.${eName}()
123
-
124
- // First call runs the operation and stores its result
125
- await entity.${primaryOp}(${stateCallArg})
126
-
127
- // Subsequent calls reuse the stored state
128
- const data = entity.data()
129
- ${stateDataLine}
130
- \`\`\`
131
-
132
- ### Add custom middleware
142
+ ${stateSection}### Add custom middleware
133
143
 
134
144
  Pass features via the \`extend\` option:
135
145
 
@@ -299,17 +299,16 @@ const results = await client.${ent.Name}().${opname}()
299
299
  }
300
300
  else if ('create' === opname) {
301
301
  // Members come from the SAME shape that generates
302
- // <Name>CreateData: every required member must appear (the
303
- // /* type */ placeholders also mark the block as an illustration
304
- // for the doc gates); an all-optional create renders an empty —
305
- // still assignable — literal.
302
+ // <Name>CreateData (every required member appears), each with a
303
+ // type-correct example VALUE via exampleValue a `name: /* type */`
304
+ // comment is not a value and yields invalid TS (TS1109).
306
305
  const createItems = opRequestShape(ent, 'create').items
307
306
  .filter((it: any) => !it.optional)
308
307
  Content(`\`\`\`ts
309
308
  const result = await client.${ent.Name}().create({
310
309
  `)
311
310
  createItems.map((it: any) => {
312
- Content(` ${it.name}: /* ${canonToType(it.type, target.name)} */,
311
+ Content(` ${it.name}: ${exampleValue(ent, ent.op && ent.op.create, it.name, 'example_' + it.name)},
313
312
  `)
314
313
  })
315
314
  Content(`})
@@ -1,5 +1,5 @@
1
1
 
2
- import { cmp, Content, entityIdField, entityPrimaryOp, opRequestShape, safeVarName } from '@voxgig/sdkgen'
2
+ import { cmp, Content, entityIdField, pickExampleEntity, opRequestShape, safeVarName } from '@voxgig/sdkgen'
3
3
 
4
4
  import {
5
5
  KIT,
@@ -14,17 +14,16 @@ const ReadmeTopTest = cmp(function ReadmeTopTest(props: any) {
14
14
  const { target, ctx$: { model } } = props
15
15
 
16
16
  const entity = getModelPath(model, `main.${KIT}.entity`)
17
- const exampleEntity = Object.values(entity).find((e: any) => e.active !== false) as any
17
+ // Pick an entity with a real op (prefer a read op) never fabricate a
18
+ // `load` on an op-less entity like Cloudsmith's `Abort`.
19
+ const { entity: exampleEntity, primaryOp } = pickExampleEntity(entity)
18
20
 
19
21
  Content(`\`\`\`ts
20
22
  const client = ${model.const.Name}SDK.test()
21
23
  `)
22
24
 
23
- if (exampleEntity) {
25
+ if (exampleEntity && primaryOp) {
24
26
  const eName = nom(exampleEntity, 'Name')
25
- // Drive the test-mode example off the entity's PRIMARY op (never a
26
- // hardcoded `load` a create-only entity lacks).
27
- const primaryOp = entityPrimaryOp(exampleEntity) || 'load'
28
27
  // A list() result is an array — name the variable accordingly.
29
28
  const eVar = safeVarName(eName.toLowerCase(), 'ts') +
30
29
  ('list' === primaryOp ? 's' : '')
@@ -18,7 +18,7 @@ import type {
18
18
 
19
19
 
20
20
  // TODO: needs Entity superclass
21
- class EntityNameEntity extends ProjectNameEntityBase<EntityName> {
21
+ class EntyClass extends ProjectNameEntityBase<EntityName> {
22
22
 
23
23
  constructor(client: ProjectNameSDK, entopts: any) {
24
24
  super(client, entopts)
@@ -28,8 +28,8 @@ class EntityNameEntity extends ProjectNameEntityBase<EntityName> {
28
28
  }
29
29
 
30
30
 
31
- make(this: EntityNameEntity) {
32
- return new EntityNameEntity(this._client, this.entopts())
31
+ make(this: EntyClass) {
32
+ return new EntyClass(this._client, this.entopts())
33
33
  }
34
34
 
35
35
 
@@ -47,5 +47,5 @@ class EntityNameEntity extends ProjectNameEntityBase<EntityName> {
47
47
 
48
48
 
49
49
  export {
50
- EntityNameEntity
50
+ EntyClass
51
51
  }
@@ -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
@@ -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