@voxgig/sdkgen 4.2.7 → 4.2.8

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/bin/voxgig-sdkgen CHANGED
@@ -8,7 +8,7 @@ const { Shape, One, Skip } = require('shape')
8
8
 
9
9
  const { SdkGen } = require('../dist/sdkgen.js')
10
10
 
11
- const VERSION = '4.2.7'
11
+ const VERSION = '4.2.8'
12
12
  const KONSOLE = console
13
13
 
14
14
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@voxgig/sdkgen",
3
- "version": "4.2.7",
3
+ "version": "4.2.8",
4
4
  "main": "dist/sdkgen.js",
5
5
  "type": "commonjs",
6
6
  "engines": {
@@ -3,7 +3,7 @@
3
3
  "package": 1
4
4
  },
5
5
  "name": "@voxgig/sdkgen",
6
- "version": "4.2.7",
6
+ "version": "4.2.8",
7
7
  "provides": {
8
8
  "target": [
9
9
  "c",
@@ -1,264 +0,0 @@
1
-
2
- // Feature behaviour, driven by the SHARED corpus.
3
- //
4
- // This is the route PrimaryUtility.test.ts already takes for the utilities:
5
- // language-neutral cases in .sdk/test/test.json, executed against the REAL
6
- // generated SDK. Features here are ordinary classes in ordinary compiled
7
- // source, unit-tested the ordinary way — no transpiled templates, and no
8
- // miniature of the pipeline standing in for the pipeline (which is what
9
- // harness.ts does, and why its assertions can only be as right as the
10
- // miniature is). A feature is built through the generated config, wrapped
11
- // into a client built by the generated constructor, and driven by a real
12
- // entity operation. What is asserted is what ships.
13
- //
14
- // Everything in a case is data: features are activated by name, options are
15
- // plain JSON, the transport is scripted by `res`, and the assertion is a
16
- // subset of the client's own record. Turning `res` into a fetcher is the one
17
- // piece each language writes for itself.
18
- //
19
- // The ts twin of this file is the reference; keep the two in step.
20
-
21
- const { test, describe, before } = require('node:test')
22
- const { ok, deepStrictEqual } = require('node:assert')
23
-
24
- const { readFileSync } = require('node:fs')
25
- const { join } = require('node:path')
26
-
27
- const { SDK, TEST_JSON_FILE } = require('../utility/index')
28
-
29
-
30
- // Features with a corpus section. A name here with no section is a skip, not
31
- // a failure: an SDK generated without the feature has nothing to run.
32
- const FEATURES = ['cost']
33
-
34
-
35
- // One operation this SDK can actually perform is described by
36
- // { key, accessor, entity, op }: `key` is '<entity>.<op>', how features
37
- // attribute spend, and `accessor` is the client method returning the entity.
38
-
39
-
40
- // A scripted transport built from a case's `res` list. Responses are consumed
41
- // in order and the last one repeats, so a case that does not care how many
42
- // attempts happen need only declare one.
43
- function scriptedFetcher(res) {
44
- let n = -1
45
- return async function (_ctx, _url, _fetchdef) {
46
- n++
47
- const spec = res[n < res.length ? n : res.length - 1] || {}
48
-
49
- if (true === spec.throw) {
50
- throw new Error('scripted transport failure')
51
- }
52
-
53
- const headers = spec.headers || {}
54
- const status = null == spec.status ? 200 : spec.status
55
-
56
- return {
57
- status,
58
- statusText: status < 400 ? 'OK' : 'ERR',
59
- body: 'not-used',
60
- json: async () => (undefined === spec.body ? {} : spec.body),
61
- headers: {
62
- get(key) {
63
- const lower = String(key).toLowerCase()
64
- for (const k of Object.keys(headers)) {
65
- if (k.toLowerCase() === lower) { return headers[k] }
66
- }
67
- return undefined
68
- },
69
- forEach(cb) { Object.keys(headers).forEach((k) => cb(headers[k], k, this)) },
70
- },
71
- }
72
- }
73
- }
74
-
75
-
76
- function makeClient(kase) {
77
- return new SDK({
78
- feature: kase.feature,
79
- utility: { fetcher: scriptedFetcher(kase.res || [{ status: 200, body: {} }]) },
80
- })
81
- }
82
-
83
-
84
- // Every operation this SDK declares, in a stable order.
85
- //
86
- // The corpus cannot name an entity — it is shared by SDKs that have none in
87
- // common — so the runner finds them here. The generated client exposes one
88
- // capitalised, zero-argument accessor per entity, and the entity it returns
89
- // carries the same `name` the config is keyed by; that pairing is what turns
90
- // a config entry back into a callable method.
91
- function candidates(client) {
92
- const entities = client._rootctx.config.entity || {}
93
-
94
- const accessor = {}
95
- for (const m of Object.getOwnPropertyNames(Object.getPrototypeOf(client))) {
96
- if (!/^[A-Z]/.test(m) || 'function' !== typeof client[m]) { continue }
97
- let inst
98
- try { inst = client[m]() }
99
- catch (e) { continue }
100
- if (null != inst && 'string' === typeof inst.name && null != entities[inst.name]) {
101
- accessor[inst.name] = m
102
- }
103
- }
104
-
105
- const out = []
106
- for (const entity of Object.keys(entities).sort()) {
107
- if (null == accessor[entity]) { continue }
108
- for (const op of Object.keys(entities[entity].op || {}).sort()) {
109
- out.push({ key: entity + '.' + op, accessor: accessor[entity], entity, op })
110
- }
111
- }
112
- return out
113
- }
114
-
115
-
116
- // Pick operations the corpus can drive, by DRIVING them: an op is usable when
117
- // it completes against a plain 200 with no feature active. Declared ops are
118
- // not all callable with no arguments (a required path parameter, a body), and
119
- // a case that failed for that reason would look like a feature defect.
120
- async function usableOps(want) {
121
- const picked = []
122
- for (const cand of candidates(makeClient({}))) {
123
- const client = makeClient({})
124
- try {
125
- await client[cand.accessor]()[cand.op]({}, {})
126
- }
127
- catch (e) { continue }
128
- picked.push(cand)
129
- if (want <= picked.length) { break }
130
- }
131
- return picked
132
- }
133
-
134
-
135
- // Replace #OP1/#OP2 throughout a case, keys included.
136
- function resolve(node, tokens) {
137
- if ('string' === typeof node) {
138
- let s = node
139
- for (const t of Object.keys(tokens)) { s = s.split(t).join(tokens[t]) }
140
- return s
141
- }
142
- if (Array.isArray(node)) {
143
- return node.map((n) => resolve(n, tokens))
144
- }
145
- if (null != node && 'object' === typeof node) {
146
- const out = {}
147
- for (const k of Object.keys(node)) {
148
- out[resolve(k, tokens)] = resolve(node[k], tokens)
149
- }
150
- return out
151
- }
152
- return node
153
- }
154
-
155
-
156
- // Which #OPn tokens a case uses. A case wanting more operations than this SDK
157
- // has is skipped rather than failed.
158
- function tokensUsed(kase) {
159
- const m = JSON.stringify(kase).match(/#OP(\d+)/g) || []
160
- return m.reduce((max, t) => Math.max(max, Number(t.slice(3))), 0)
161
- }
162
-
163
-
164
- // Assert that `actual` contains `expect`, recursively. Cases assert only the
165
- // fields they are about, so a full deepStrictEqual would force every case to
166
- // restate the whole record.
167
- function subset(actual, expect, path) {
168
- if (null != expect && 'object' === typeof expect && !Array.isArray(expect)) {
169
- for (const k of Object.keys(expect)) {
170
- ok(null != actual, `${path}.${k}: nothing at ${path}`)
171
- subset(actual[k], expect[k], `${path}.${k}`)
172
- }
173
- return
174
- }
175
- deepStrictEqual(actual, expect, path)
176
- }
177
-
178
-
179
- describe('FeatureCorpus', () => {
180
-
181
- let corpus
182
- let ops = []
183
- let byKey = {}
184
-
185
-
186
- before(async () => {
187
- corpus = JSON.parse(readFileSync(join(__dirname, '..', TEST_JSON_FILE), 'utf8'))
188
- ops = await usableOps(2)
189
- byKey = {}
190
- for (const o of ops) { byKey[o.key] = o }
191
- })
192
-
193
-
194
- test('the corpus carries a feature section', () => {
195
- ok(null != corpus.feature,
196
- 'no `feature` section in test.json — recompile the corpus')
197
- })
198
-
199
-
200
- // At least one operation, or every case below would skip and the whole
201
- // suite would report green having run nothing.
202
- test('this SDK has an operation the corpus can drive', () => {
203
- ok(0 < ops.length,
204
- 'no declared operation completed against a plain 200 — the corpus ' +
205
- 'cannot exercise a feature without one')
206
- })
207
-
208
-
209
- for (const name of FEATURES) {
210
-
211
- test(name, async (t) => {
212
- const section = corpus.feature?.[name]
213
- if (null == section) {
214
- return t.skip(`no corpus section for ${name}`)
215
- }
216
-
217
- const probe = makeClient({})
218
- if (!probe._rootctx.config.hasFeature(name)) {
219
- return t.skip(`this SDK was generated without the ${name} feature`)
220
- }
221
-
222
- const cases = section.basic?.set || []
223
- ok(0 < cases.length,
224
- `corpus section feature.${name} ran ZERO cases — a renamed section ` +
225
- `or an emptied fixture must fail loudly, not pass silently`)
226
-
227
- let ran = 0
228
- for (const raw of cases) {
229
- const need = tokensUsed(raw)
230
- if (ops.length < need) {
231
- t.diagnostic(`skip "${raw.name}": needs ${need} operations, this SDK offers ${ops.length}`)
232
- continue
233
- }
234
-
235
- const tokens = {}
236
- for (let i = 0; i < need; i++) { tokens['#OP' + (i + 1)] = ops[i].key }
237
-
238
- const kase = resolve(raw, tokens)
239
- const client = makeClient(kase)
240
-
241
- for (const step of (kase.op || [])) {
242
- const ref = byKey[step.op]
243
- ok(null != ref, `${kase.name}: no operation ${step.op}`)
244
- try {
245
- await client[ref.accessor]()[ref.op]({}, step.ctrl || {})
246
- ok(null == step.err,
247
- `${kase.name}: ${step.op} was expected to fail, and did not`)
248
- }
249
- catch (err) {
250
- if (null == step.err) { throw err }
251
- if ('string' === typeof step.err) {
252
- deepStrictEqual(err.code, step.err, `${kase.name}: wrong error code`)
253
- }
254
- }
255
- }
256
-
257
- subset(client[`_${name}`], kase.out, `${kase.name}: _${name}`)
258
- ran++
259
- }
260
-
261
- ok(0 < ran, `every feature.${name} case was skipped`)
262
- })
263
- }
264
- })
@@ -1,266 +0,0 @@
1
-
2
- // Feature behaviour, driven by the SHARED corpus.
3
- //
4
- // This is the route PrimaryUtility.test.ts already takes for the utilities:
5
- // language-neutral cases in .sdk/test/test.json, executed against the REAL
6
- // generated SDK. Features here are ordinary classes in ordinary compiled
7
- // source, unit-tested the ordinary way — no transpiled templates, and no
8
- // miniature of the pipeline standing in for the pipeline (which is what
9
- // harness.ts does, and why its assertions can only be as right as the
10
- // miniature is). A feature is built through the generated config, wrapped
11
- // into a client built by the generated constructor, and driven by a real
12
- // entity operation. What is asserted is what ships.
13
- //
14
- // Everything in a case is data: features are activated by name, options are
15
- // plain JSON, the transport is scripted by `res`, and the assertion is a
16
- // subset of the client's own record. Turning `res` into a fetcher is the one
17
- // piece each language writes for itself.
18
-
19
- import { test, describe, before } from 'node:test'
20
- import { ok, deepStrictEqual } from 'node:assert'
21
-
22
- import { readFileSync } from 'node:fs'
23
- import { join } from 'node:path'
24
-
25
- import { SDK, TEST_JSON_FILE } from '../utility/index'
26
-
27
-
28
- // Features with a corpus section. A name here with no section is a skip, not
29
- // a failure: an SDK generated without the feature has nothing to run.
30
- const FEATURES = ['cost']
31
-
32
-
33
- // One operation this SDK can actually perform.
34
- type OpRef = {
35
- key: string // '<entity>.<op>' — how features attribute it
36
- accessor: string // the client method returning the entity
37
- entity: string
38
- op: string
39
- }
40
-
41
-
42
- // A scripted transport built from a case's `res` list. Responses are consumed
43
- // in order and the last one repeats, so a case that does not care how many
44
- // attempts happen need only declare one.
45
- function scriptedFetcher(res: any[]) {
46
- let n = -1
47
- return async function (_ctx: any, _url: string, _fetchdef: any) {
48
- n++
49
- const spec = res[n < res.length ? n : res.length - 1] || {}
50
-
51
- if (true === spec.throw) {
52
- throw new Error('scripted transport failure')
53
- }
54
-
55
- const headers: Record<string, any> = spec.headers || {}
56
- const status = null == spec.status ? 200 : spec.status
57
-
58
- return {
59
- status,
60
- statusText: status < 400 ? 'OK' : 'ERR',
61
- body: 'not-used',
62
- json: async () => (undefined === spec.body ? {} : spec.body),
63
- headers: {
64
- get(key: string) {
65
- const lower = String(key).toLowerCase()
66
- for (const k of Object.keys(headers)) {
67
- if (k.toLowerCase() === lower) { return headers[k] }
68
- }
69
- return undefined
70
- },
71
- forEach(cb: any) { Object.keys(headers).forEach((k) => cb(headers[k], k, this)) },
72
- },
73
- }
74
- }
75
- }
76
-
77
-
78
- function makeClient(kase: any): any {
79
- return new (SDK as any)({
80
- feature: kase.feature,
81
- utility: { fetcher: scriptedFetcher(kase.res || [{ status: 200, body: {} }]) },
82
- })
83
- }
84
-
85
-
86
- // Every operation this SDK declares, in a stable order.
87
- //
88
- // The corpus cannot name an entity — it is shared by SDKs that have none in
89
- // common — so the runner finds them here. The generated client exposes one
90
- // capitalised, zero-argument accessor per entity, and the entity it returns
91
- // carries the same `name` the config is keyed by; that pairing is what turns
92
- // a config entry back into a callable method.
93
- function candidates(client: any): OpRef[] {
94
- const entities: Record<string, any> = client._rootctx.config.entity || {}
95
-
96
- const accessor: Record<string, string> = {}
97
- for (const m of Object.getOwnPropertyNames(Object.getPrototypeOf(client))) {
98
- if (!/^[A-Z]/.test(m) || 'function' !== typeof client[m]) { continue }
99
- let inst: any
100
- try { inst = client[m]() }
101
- catch (e) { continue }
102
- if (null != inst && 'string' === typeof inst.name && null != entities[inst.name]) {
103
- accessor[inst.name] = m
104
- }
105
- }
106
-
107
- const out: OpRef[] = []
108
- for (const entity of Object.keys(entities).sort()) {
109
- if (null == accessor[entity]) { continue }
110
- for (const op of Object.keys(entities[entity].op || {}).sort()) {
111
- out.push({ key: entity + '.' + op, accessor: accessor[entity], entity, op })
112
- }
113
- }
114
- return out
115
- }
116
-
117
-
118
- // Pick operations the corpus can drive, by DRIVING them: an op is usable when
119
- // it completes against a plain 200 with no feature active. Declared ops are
120
- // not all callable with no arguments (a required path parameter, a body), and
121
- // a case that failed for that reason would look like a feature defect.
122
- async function usableOps(want: number): Promise<OpRef[]> {
123
- const picked: OpRef[] = []
124
- for (const cand of candidates(makeClient({}))) {
125
- const client = makeClient({})
126
- try {
127
- await client[cand.accessor]()[cand.op]({}, {})
128
- }
129
- catch (e) { continue }
130
- picked.push(cand)
131
- if (want <= picked.length) { break }
132
- }
133
- return picked
134
- }
135
-
136
-
137
- // Replace #OP1/#OP2 throughout a case, keys included.
138
- function resolve(node: any, tokens: Record<string, string>): any {
139
- if ('string' === typeof node) {
140
- let s = node
141
- for (const t of Object.keys(tokens)) { s = s.split(t).join(tokens[t]) }
142
- return s
143
- }
144
- if (Array.isArray(node)) {
145
- return node.map((n) => resolve(n, tokens))
146
- }
147
- if (null != node && 'object' === typeof node) {
148
- const out: any = {}
149
- for (const k of Object.keys(node)) {
150
- out[resolve(k, tokens)] = resolve(node[k], tokens)
151
- }
152
- return out
153
- }
154
- return node
155
- }
156
-
157
-
158
- // Which #OPn tokens a case uses. A case wanting more operations than this SDK
159
- // has is skipped rather than failed.
160
- function tokensUsed(kase: any): number {
161
- const m = JSON.stringify(kase).match(/#OP(\d+)/g) || []
162
- return m.reduce((max, t) => Math.max(max, Number(t.slice(3))), 0)
163
- }
164
-
165
-
166
- // Assert that `actual` contains `expect`, recursively. Cases assert only the
167
- // fields they are about, so a full deepStrictEqual would force every case to
168
- // restate the whole record.
169
- function subset(actual: any, expect: any, path: string) {
170
- if (null != expect && 'object' === typeof expect && !Array.isArray(expect)) {
171
- for (const k of Object.keys(expect)) {
172
- ok(null != actual, `${path}.${k}: nothing at ${path}`)
173
- subset(actual[k], expect[k], `${path}.${k}`)
174
- }
175
- return
176
- }
177
- deepStrictEqual(actual, expect, path)
178
- }
179
-
180
-
181
- describe('FeatureCorpus', () => {
182
-
183
- let corpus: any
184
- let ops: OpRef[] = []
185
- let byKey: Record<string, OpRef> = {}
186
-
187
-
188
- before(async () => {
189
- corpus = JSON.parse(readFileSync(join(__dirname, '..', TEST_JSON_FILE), 'utf8'))
190
- ops = await usableOps(2)
191
- byKey = {}
192
- for (const o of ops) { byKey[o.key] = o }
193
- })
194
-
195
-
196
- test('the corpus carries a feature section', () => {
197
- ok(null != corpus.feature,
198
- 'no `feature` section in test.json — recompile the corpus')
199
- })
200
-
201
-
202
- // At least one operation, or every case below would skip and the whole
203
- // suite would report green having run nothing.
204
- test('this SDK has an operation the corpus can drive', () => {
205
- ok(0 < ops.length,
206
- 'no declared operation completed against a plain 200 — the corpus ' +
207
- 'cannot exercise a feature without one')
208
- })
209
-
210
-
211
- for (const name of FEATURES) {
212
-
213
- test(name, async (t) => {
214
- const section = corpus.feature?.[name]
215
- if (null == section) {
216
- return t.skip(`no corpus section for ${name}`)
217
- }
218
-
219
- const probe: any = makeClient({})
220
- if (!probe._rootctx.config.hasFeature(name)) {
221
- return t.skip(`this SDK was generated without the ${name} feature`)
222
- }
223
-
224
- const cases: any[] = section.basic?.set || []
225
- ok(0 < cases.length,
226
- `corpus section feature.${name} ran ZERO cases — a renamed section ` +
227
- `or an emptied fixture must fail loudly, not pass silently`)
228
-
229
- let ran = 0
230
- for (const raw of cases) {
231
- const need = tokensUsed(raw)
232
- if (ops.length < need) {
233
- t.diagnostic(`skip "${raw.name}": needs ${need} operations, this SDK offers ${ops.length}`)
234
- continue
235
- }
236
-
237
- const tokens: Record<string, string> = {}
238
- for (let i = 0; i < need; i++) { tokens['#OP' + (i + 1)] = ops[i].key }
239
-
240
- const kase = resolve(raw, tokens)
241
- const client = makeClient(kase)
242
-
243
- for (const step of (kase.op || [])) {
244
- const ref = byKey[step.op]
245
- ok(null != ref, `${kase.name}: no operation ${step.op}`)
246
- try {
247
- await client[ref.accessor]()[ref.op]({}, step.ctrl || {})
248
- ok(null == step.err,
249
- `${kase.name}: ${step.op} was expected to fail, and did not`)
250
- }
251
- catch (err: any) {
252
- if (null == step.err) { throw err }
253
- if ('string' === typeof step.err) {
254
- deepStrictEqual(err.code, step.err, `${kase.name}: wrong error code`)
255
- }
256
- }
257
- }
258
-
259
- subset(client[`_${name}`], kase.out, `${kase.name}: _${name}`)
260
- ran++
261
- }
262
-
263
- ok(0 < ran, `every feature.${name} case was skipped`)
264
- })
265
- }
266
- })