sendscript 2.3.1 → 2.3.3

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/README.mz CHANGED
@@ -11,23 +11,25 @@ Write JS code that you can run on servers, browsers or other clients.
11
11
 
12
12
  ## Introduction
13
13
 
14
- There has been interest in improving APIs by allowing aggregations in a
15
- single request. Examples include
14
+ There has been interest in improving APIs by allowing aggregations in a single
15
+ request. Examples include
16
16
 
17
- - [JSON-RPC](https://json-rpc.dev/) which allows you to do multiple requests
18
- but it does not allow you to compose the return value of one endpoint to be the
17
+ - [JSON-RPC](https://json-rpc.dev/) which allows you to do multiple requests but
18
+ it does not allow you to compose the return value of one endpoint to be the
19
19
  input/arguments of another.
20
20
 
21
- - [GraphQL](https://graphql.org/) is very cool but also introduces a new languages and the
22
- tooling that is required to wield it.
21
+ - [GraphQL](https://graphql.org/) is very cool but also introduces a new
22
+ languages and the tooling that is required to wield it.
23
23
 
24
- What SendScript attempts is to allow for very expressive queries and mutations to be performed
25
- that read and write like ordinary JS. That means that the queries and complete programs
26
- that are sent to the server from a client can also just run on the server as is. The only
27
- limitation being the serialization which by default is limited by JSON and could be extended by
28
- using more advanced (de)serialization libraries.
24
+ What SendScript attempts is to allow for very expressive queries and mutations
25
+ to be performed that read and write like ordinary JS. That means that the
26
+ queries and complete programs that are sent to the server from a client can also
27
+ just run on the server as is. The only limitation being the serialization which
28
+ by default is limited by JSON and could be extended by using more advanced
29
+ (de)serialization libraries.
29
30
 
30
- SendScript produces an intermediate JSON representation of the program. Let's see what that looks like.
31
+ SendScript produces an intermediate JSON representation of the program. Let's
32
+ see what that looks like.
31
33
 
32
34
  ```js|json node --input-type=module | tee /tmp/sendscript.json
33
35
  import Stringify from 'sendscript/stringify.mjs'
@@ -60,22 +62,26 @@ console.log(parse(program))
60
62
  SendScript does more than a simple function call. It supports function
61
63
  composition and even await.
62
64
 
63
- This package is nothing more than the absolute core of sendscript. It
64
- includes:
65
+ This package is nothing more than the absolute core of sendscript. It includes:
65
66
 
66
67
  - The `references` function to create stubs to write the programs.
67
68
  - `stringify` which takes the program and returns a JSON string.
68
- - `parse` which takes the `stringify` JSON string and a real module and returns the result.
69
+ - `parse` which takes the `stringify` JSON string and a real module and returns
70
+ the result.
69
71
 
70
- The naming could use more love and there are many things to solve either in the core or around it.
71
- Things like supporting more complex (de)serializers, errors and maybe mixing client functions with
72
- sendscript programs. Contact me if I have piqued your interest.
72
+ The naming could use more love and there are many things to solve either in the
73
+ core or around it. Things like supporting more complex (de)serializers, errors
74
+ and maybe mixing client functions with sendscript programs. Contact me if I have
75
+ piqued your interest.
73
76
 
74
77
  ---
75
78
 
76
- SendScript leaves it up to you to choose HTTP, web-sockets or any other
77
- method of communication between servers and clients that best fits your
78
- needs.
79
+ SendScript leaves it up to you to choose HTTP, web-sockets or any other method
80
+ of communication between servers and clients that best fits your needs.
81
+
82
+ ## Reference
83
+
84
+ <!-- Reference -->
79
85
 
80
86
  ## Socket example
81
87
 
@@ -89,7 +95,7 @@ We write a simple module.
89
95
  // ./example/math.mjs
90
96
 
91
97
  export const add = (a, b) => a + b
92
- export const square = a => a * a
98
+ export const square = (a) => a * a
93
99
  ```
94
100
 
95
101
  ### Server
@@ -141,12 +147,10 @@ const stringify = Stringify()
141
147
  const port = process.env.PORT || 3000
142
148
  const client = socketClient(`http://localhost:${port}`)
143
149
 
144
- const send = program => {
150
+ const send = (program) => {
145
151
  return new Promise((resolve, reject) => {
146
152
  client.emit('message', stringify(program), (error, result) => {
147
- error
148
- ? reject(error)
149
- : resolve(result)
153
+ error ? reject(error) : resolve(result)
150
154
  })
151
155
  })
152
156
  }
@@ -179,9 +183,11 @@ pkill sendscript
179
183
 
180
184
  ## Repl
181
185
 
182
- Sendscript ships with a barebones (no-dependencies) node-repl script. One can run it by simply typing `sendscript` in their console.
186
+ Sendscript ships with a barebones (no-dependencies) node-repl script. One can
187
+ run it by simply typing `sendscript` in their console.
183
188
 
184
- > Use the `DEBUG='*'` to enable all logs or `DEBUG='sendscript:*'` for printingonly sendscript logs.
189
+ > Use the `DEBUG='*'` to enable all logs or `DEBUG='sendscript:*'` for
190
+ > printingonly sendscript logs.
185
191
 
186
192
  ## Promises
187
193
 
@@ -193,27 +199,33 @@ Supported since vs `v2.3`.
193
199
  const getOrCreatePost = send(createPost(title).catch(createPost(title)))
194
200
  ```
195
201
 
196
- You will likely need to define better helpers that makes it safer to handle rejections and work with promises. It is however sensible to have
197
- this basic behavior for the sendscript DSL and parser.
202
+ You will likely need to define better helpers that makes it safer to handle
203
+ rejections and work with promises. It is however sensible to have this basic
204
+ behavior for the sendscript DSL and parser.
198
205
 
199
206
  ### await
200
207
 
201
- SendScript supports async/await seamlessly within a single request. This avoids the performance pitfalls of waterfall-style messaging, which can be especially slow on high-latency networks.
208
+ SendScript supports async/await seamlessly within a single request. This avoids
209
+ the performance pitfalls of waterfall-style messaging, which can be especially
210
+ slow on high-latency networks.
202
211
 
203
- While it's possible to chain promises manually or use utility functions, native async/await support makes your code more readable, modern, and easier to reason about — aligning SendScript with today’s JavaScript best practices.
212
+ While it's possible to chain promises manually or use utility functions, native
213
+ async/await support makes your code more readable, modern, and easier to reason
214
+ about — aligning SendScript with today’s JavaScript best practices.
204
215
 
205
216
  ```js
206
217
  const userId = 'user-123'
207
218
  const program = {
208
219
  unread: await fetchUnreadMessages(userId),
209
220
  emptyTrash: await emptyTrash(userId),
210
- archived: await archiveMessages(selectMessages({ old: true }))
221
+ archived: await archiveMessages(selectMessages({ old: true })),
211
222
  }
212
223
 
213
224
  const result = await send(program)
214
225
  ```
215
226
 
216
- This operation is done in a single round-trip. The result is an object with the defined properties and returned values.
227
+ This operation is done in a single round-trip. The result is an object with the
228
+ defined properties and returned values.
217
229
 
218
230
  ## TypeScript
219
231
 
@@ -246,15 +258,16 @@ npx typedoc --plugin typedoc-plugin-markdown --out ./example/typescript/docs ./e
246
258
 
247
259
  You can see the docs [here](./example/typescript/docs/globals.md)
248
260
 
249
- > [!NOTE]
250
- > Although type coercion on the client side can improve the development
251
- > experience, it does not represent the actual type.
252
- > Values are subject to serialization and deserialization.
253
-
261
+ > [!NOTE] Although type coercion on the client side can improve the development
262
+ > experience, it does not represent the actual type. Values are subject to
263
+ > serialization and deserialization.
254
264
 
255
265
  ## Schema and Nested Modules
256
266
 
257
- Sendscript allows you to define your API as a **nested object of functions**, making it easy to organize your DSL into modules and submodules. Each function is instrumented so that when serialized, it produces a structured reference that can be safely sent and executed elsewhere.
267
+ Sendscript allows you to define your API as a **nested object of functions**,
268
+ making it easy to organize your DSL into modules and submodules. Each function
269
+ is instrumented so that when serialized, it produces a structured reference that
270
+ can be safely sent and executed elsewhere.
258
271
 
259
272
  ### Defining a Nested Module
260
273
 
@@ -286,17 +299,13 @@ Functions are referenced via their **path in the module tree**:
286
299
  ```js
287
300
  const { math, vector } = references(schema)
288
301
 
289
- math.add(
290
- 1,
291
- vector.length(
292
- vector.multiply([1,2], 3)
293
- )
294
- )
302
+ math.add(1, vector.length(vector.multiply([1, 2], 3)))
295
303
  ```
296
304
 
297
305
  ## Validation (using Zod)
298
306
 
299
- SendScript focuses on program serialization and execution. For runtime input validation, you can use [Zod](https://zod.dev).
307
+ SendScript focuses on program serialization and execution. For runtime input
308
+ validation, you can use [Zod](https://zod.dev).
300
309
 
301
310
  ### Validating structured input
302
311
 
@@ -304,7 +313,7 @@ SendScript focuses on program serialization and execution. For runtime input val
304
313
  const userSchema = z.object({
305
314
  id: z.string().uuid(),
306
315
  name: z.string(),
307
- roles: z.array(z.string())
316
+ roles: z.array(z.string()),
308
317
  })
309
318
 
310
319
  export function createUser(user) {
@@ -322,13 +331,19 @@ export function createUser(user) {
322
331
 
323
332
  ## Leaf Serializer
324
333
 
325
- By default, SendScript uses JSON for serialization, which limits support to primitives and plain objects/arrays. To support richer JavaScript types like `Date`, `RegExp`, `BigInt`, `Map`, `Set`, and `undefined`, you can provide custom serialization functions.
334
+ By default, SendScript uses JSON for serialization, which limits support to
335
+ primitives and plain objects/arrays. To support richer JavaScript types like
336
+ `Date`, `RegExp`, `BigInt`, `Map`, `Set`, and `undefined`, you can provide
337
+ custom serialization functions.
326
338
 
327
- The `stringify` function accepts an optional `leafSerializer` parameter, and `parse` accepts an optional `leafDeserializer` parameter. These functions control how non-SendScript values (leaves) are encoded and decoded.
339
+ The `stringify` function accepts an optional `leafSerializer` parameter, and
340
+ `parse` accepts an optional `leafDeserializer` parameter. These functions
341
+ control how non-SendScript values (leaves) are encoded and decoded.
328
342
 
329
343
  ### Example with superjson
330
344
 
331
- Here's how to use [superjson](https://github.com/blitz-js/superjson) to support extended types:
345
+ Here's how to use [superjson](https://github.com/blitz-js/superjson) to support
346
+ extended types:
332
347
 
333
348
  ```js
334
349
  import SuperJSON from 'superjson'
@@ -358,7 +373,10 @@ const program = {
358
373
  pattern: /foo/gi,
359
374
  count: BigInt('9007199254740992'),
360
375
  items: new Set([1, 2, 3]),
361
- mapping: new Map([['a', 1], ['b', 2]])
376
+ mapping: new Map([
377
+ ['a', 1],
378
+ ['b', 2],
379
+ ]),
362
380
  }
363
381
 
364
382
  // Serialize with custom leaf serializer
@@ -368,8 +386,8 @@ const json = stringify(processData(program))
368
386
  const env = {
369
387
  processData: (data) => ({
370
388
  success: true,
371
- received: data
372
- })
389
+ received: data,
390
+ }),
373
391
  }
374
392
 
375
393
  // Parse with custom leaf deserializer
@@ -378,24 +396,8 @@ const parse = Parse(schema, env, leadDeserializer)
378
396
  const result = parse(json)
379
397
  ```
380
398
 
381
- The leaf wrapper format is `['leaf', serializedPayload]`, making it unambiguous and safe from colliding with SendScript operators.
382
-
383
- ## Tests
384
-
385
- Tests with 100% code coverage.
386
-
387
- ```bash bash
388
- npm t -- -R silent
389
- npm t -- report text-summary
390
- ```
391
-
392
- ## Formatting
393
-
394
- Standard because no config.
395
-
396
- ```bash bash
397
- npx standard
398
- ```
399
+ The leaf wrapper format is `['leaf', serializedPayload]`, making it unambiguous
400
+ and safe from colliding with SendScript operators.
399
401
 
400
402
  ## Changelog
401
403
 
@@ -406,24 +408,17 @@ The [changelog][changelog] is generated using the useful
406
408
  npx auto-changelog -p
407
409
  ```
408
410
 
409
- ## Dependencies
410
-
411
- Check if packages are up to date on release.
412
-
413
- ```bash bash
414
- npm outdated && echo 'No outdated packages found'
415
- ```
416
-
417
411
  ## License
418
412
 
419
413
  See the [LICENSE.txt][license] file for details.
420
414
 
421
- ## Roadmap
415
+ ## Issues
422
416
 
423
- - [ ] Support for simple lambdas to compose functions more easily.
417
+ See [issues][issues] for roadmap and known bugs.
424
418
 
425
- [license]:./LICENSE.txt
426
- [socket.io]:https://socket.io/
427
- [changelog]:./CHANGELOG.md
428
- [auto-changelog]:https://www.npmjs.com/package/auto-changelog
429
- [typedoc]:https://github.com/TypeStrong/typedoc
419
+ [license]: ./LICENSE.txt
420
+ [socket.io]: https://socket.io/
421
+ [changelog]: ./CHANGELOG.md
422
+ [auto-changelog]: https://www.npmjs.com/package/auto-changelog
423
+ [typedoc]: https://github.com/TypeStrong/typedoc
424
+ [issues]: https://github.com/bas080/sendscript/issues
@@ -11,12 +11,10 @@ const stringify = Stringify()
11
11
  const port = process.env.PORT || 3000
12
12
  const client = socketClient(`http://localhost:${port}`)
13
13
 
14
- const send = program => {
14
+ const send = (program) => {
15
15
  return new Promise((resolve, reject) => {
16
16
  client.emit('message', stringify(program), (error, result) => {
17
- error
18
- ? reject(error)
19
- : resolve(result)
17
+ error ? reject(error) : resolve(result)
20
18
  })
21
19
  })
22
20
  }
package/example/math.mjs CHANGED
@@ -1,4 +1,4 @@
1
1
  // ./example/math.mjs
2
2
 
3
3
  export const add = (a, b) => a + b
4
- export const square = a => a * a
4
+ export const square = (a) => a * a
@@ -8,7 +8,7 @@
8
8
 
9
9
  > **add**(`a`, `b`): `number`
10
10
 
11
- Defined in: [math.ts:1](https://github.com/bas080/sendscript/blob/69e8cc48251af650c02faf3214ff0f9c2b61dbc9/example/typescript/math.ts#L1)
11
+ Defined in: [math.ts:1](https://github.com/bas080/sendscript/blob/08eb266b06d8998e6849dbc5c9738fbe782ada18/example/typescript/math.ts#L1)
12
12
 
13
13
  ## Parameters
14
14
 
@@ -8,7 +8,7 @@
8
8
 
9
9
  > **square**(`a`): `number`
10
10
 
11
- Defined in: [math.ts:2](https://github.com/bas080/sendscript/blob/69e8cc48251af650c02faf3214ff0f9c2b61dbc9/example/typescript/math.ts#L2)
11
+ Defined in: [math.ts:2](https://github.com/bas080/sendscript/blob/08eb266b06d8998e6849dbc5c9738fbe782ada18/example/typescript/math.ts#L2)
12
12
 
13
13
  ## Parameters
14
14
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sendscript",
3
- "version": "2.3.1",
3
+ "version": "2.3.3",
4
4
  "description": "Blur the line between server and client code.",
5
5
  "module": true,
6
6
  "main": "index.mjs",
package/parse.mjs CHANGED
@@ -6,13 +6,14 @@ function flattenSchema (schema) {
6
6
 
7
7
  for (const item of schema) {
8
8
  if (typeof item === 'string') {
9
- // leaf function
10
9
  obj[item] = true
11
10
  } else if (Array.isArray(item)) {
12
11
  const [name, children] = item
12
+
13
13
  if (!Array.isArray(children)) {
14
14
  throw new Error(`Expected children array for namespace "${name}"`)
15
15
  }
16
+
16
17
  obj[name] = flattenSchema(children)
17
18
  } else {
18
19
  throw new Error('Schema items must be strings or [name, children] arrays')
@@ -38,7 +39,6 @@ const spy = (type, fn) => (...args) => {
38
39
  return value
39
40
  }
40
41
 
41
- // Recursively resolve awaited values in a parsed tree
42
42
  const evaluate = spy('eval', (value, awaits = []) => {
43
43
  if (value === undefinedSentinel) return undefined
44
44
 
@@ -47,49 +47,41 @@ const evaluate = spy('eval', (value, awaits = []) => {
47
47
 
48
48
  if (operator === 'await') {
49
49
  const [index] = rest
50
- // No need to check index. It is closely tied to the program.
51
- // if (typeof index !== 'number' || index < 0 || index >= awaitsResolved.length) {
52
- // throw new Error(`Invalid await index: ${index}`);
53
- // }
54
50
  return awaits[index]
55
51
  }
56
52
 
57
53
  if (operator === 'then') {
58
54
  const [v, onResolve, onReject] = rest
59
- return evaluate(v, awaits).then(evaluate(onResolve, awaits), evaluate(onReject, awaits))
55
+ return evaluate(v, awaits).then(
56
+ evaluate(onResolve, awaits),
57
+ evaluate(onReject, awaits)
58
+ )
60
59
  }
61
60
 
62
61
  if (operator === 'call') {
63
- // Step 1: evaluate the function itself
64
62
  let [fn, args] = rest
65
63
  fn = evaluate(fn, awaits)
66
64
 
67
- // Step 2: evaluate each argument AFTER fn is ready
68
65
  for (let i = 0; i < args.length; i++) {
69
66
  args[i] = evaluate(args[i], awaits)
70
67
  }
71
68
 
72
- // Step 3: call the function
73
69
  return fn(...args)
74
70
  }
75
71
 
76
72
  if (operator === 'quote') {
77
73
  const [quoted] = rest
78
- return quoted // return as-is without evaluating
74
+ return quoted
79
75
  }
80
76
 
81
- // fallback: evaluate each element
82
- // re-uses the array again.
83
-
84
- for (let index = 0; index < value.length; index++) {
85
- const item = value[index]
86
- value[index] = evaluate(item, awaits)
77
+ for (let i = 0; i < value.length; i++) {
78
+ value[i] = evaluate(value[i], awaits)
87
79
  }
80
+
88
81
  return value
89
82
  }
90
83
 
91
84
  if (isPlainObject(value)) {
92
- // We muatate the object itself. No need to make a new one.
93
85
  for (const key of Object.keys(value)) {
94
86
  value[key] = evaluate(value[key], awaits)
95
87
  }
@@ -99,23 +91,56 @@ const evaluate = spy('eval', (value, awaits = []) => {
99
91
  return value
100
92
  })
101
93
 
94
+ /**
95
+ * Default deserializer for leaf nodes.
96
+ *
97
+ * @param {string} text
98
+ * @returns {any}
99
+ * @public
100
+ */
102
101
  const defaultLeafDeserializer = (text) => JSON.parse(text)
103
102
 
104
- export default (schemaArg, env, deserialize = defaultLeafDeserializer) => {
103
+ /**
104
+ * Creates a program parser for a given schema and environment.
105
+ *
106
+ * The parser:
107
+ * - resolves references into runtime functions
108
+ * - deserializes leaf nodes
109
+ * - collects and executes async awaits
110
+ * - evaluates AST-like JSON programs
111
+ *
112
+ * @param {Array<string | [string, Array]>} schemaArg
113
+ * @param {Object} env - runtime environment for refs
114
+ * @param {(text: string) => any} [deserialize=defaultLeafDeserializer]
115
+ * @returns {(program: string) => any|Promise<any>}
116
+ * @public
117
+ */
118
+ export default function Parse (schemaArg, env, deserialize = defaultLeafDeserializer) {
105
119
  const schema = flattenSchema(schemaArg)
106
120
 
121
+ /**
122
+ * Parses and executes a serialized program.
123
+ *
124
+ * @param {string} program - JSON encoded program
125
+ * @returns {any|Promise<any>}
126
+ * @public
127
+ */
107
128
  return function parse (program) {
108
129
  debug('program', program)
109
130
  const awaits = []
110
131
 
111
- // Creates the list of awaits that will resolve in order
112
- // and also deserializes the leaves.
132
+ /**
133
+ * JSON reviver used during parsing.
134
+ * Converts encoded operators into runtime structures.
135
+ *
136
+ * @param {string} key
137
+ * @param {any} value
138
+ * @returns {any}
139
+ */
113
140
  const reviver = spy('revive', (key, value) => {
114
141
  if (value === null) return value
115
142
 
116
- if (!Array.isArray(value)) {
117
- return value
118
- }
143
+ if (!Array.isArray(value)) return value
119
144
 
120
145
  const [operator, ...rest] = value
121
146
 
@@ -126,7 +151,6 @@ export default (schemaArg, env, deserialize = defaultLeafDeserializer) => {
126
151
 
127
152
  if (operator === 'await') {
128
153
  const [program] = rest
129
-
130
154
  return ['await', awaits.push(program) - 1]
131
155
  }
132
156
 
@@ -151,19 +175,17 @@ export default (schemaArg, env, deserialize = defaultLeafDeserializer) => {
151
175
  })
152
176
 
153
177
  const parsed = JSON.parse(program, reviver)
154
-
155
178
  debug('parsed', parsed)
156
179
 
157
180
  if (awaits.length) {
158
181
  debug('awaits', awaits)
159
182
 
160
183
  return (async function () {
161
- for (let index = 0; index < awaits.length; index++) {
162
- awaits[index] = await evaluate(awaits[index], awaits)
184
+ for (let i = 0; i < awaits.length; i++) {
185
+ awaits[i] = await evaluate(awaits[i], awaits)
163
186
  }
164
187
 
165
188
  debug('awaits(awaited)', awaits)
166
-
167
189
  return evaluate(parsed, awaits)
168
190
  })()
169
191
  }
package/references.mjs CHANGED
@@ -1,6 +1,19 @@
1
1
  import { awaitSymbol, call, ref, then, referenceSymbol } from './symbol.mjs'
2
2
 
3
+ /**
4
+ * Creates a callable reference node bound to a path.
5
+ * Used to build a lazy/instrumented execution structure.
6
+ *
7
+ * @param {Array<string>} path - Path representing the function location in schema.
8
+ * @returns {Function} Reference function with attached control methods (.then, .catch, toJSON).
9
+ */
3
10
  function instrument (path) {
11
+ /**
12
+ * Creates a callable reference invocation.
13
+ *
14
+ * @param {...any} args - Arguments passed to the call.
15
+ * @returns {Function} New instrumented reference node.
16
+ */
4
17
  function reference (...args) {
5
18
  const called = instrument(path)
6
19
 
@@ -13,6 +26,13 @@ function instrument (path) {
13
26
  return called
14
27
  }
15
28
 
29
+ /**
30
+ * Internal helper for building promise-like continuation nodes.
31
+ *
32
+ * @param {Function|null} resolve
33
+ * @param {Function|null} reject
34
+ * @returns {Function} Instrumented continuation node.
35
+ */
16
36
  function dotThen (resolve, reject) {
17
37
  const node = instrument(path)
18
38
 
@@ -26,18 +46,32 @@ function instrument (path) {
26
46
  return node
27
47
  }
28
48
 
49
+ /**
50
+ * Registers rejection handler (promise-style).
51
+ *
52
+ * @param {Function} reject
53
+ * @returns {Function}
54
+ */
29
55
  reference.catch = (reject) => {
30
56
  return dotThen(null, reject)
31
57
  }
32
58
 
59
+ /**
60
+ * Handles async chaining or awaiting logic.
61
+ *
62
+ * If resolve/reject contain a reference marker, it behaves like a .then chain.
63
+ * Otherwise it behaves like an await wrapper.
64
+ *
65
+ * @param {Function} resolve
66
+ * @param {Function} reject
67
+ * @returns {Function|any}
68
+ */
33
69
  reference.then = (resolve, reject) => {
34
- // That is how we know if it is an await or a .then call.
35
70
  if (resolve?.[referenceSymbol] || reject?.[referenceSymbol]) {
36
71
  return dotThen(resolve, reject)
37
72
  }
38
73
 
39
74
  const awaited = instrument(path)
40
- // Prevent infinite recur
41
75
  delete awaited.then
42
76
 
43
77
  awaited.toJSON = () => ({
@@ -48,6 +82,11 @@ function instrument (path) {
48
82
  return resolve(awaited)
49
83
  }
50
84
 
85
+ /**
86
+ * JSON representation of the reference path node.
87
+ *
88
+ * @returns {{ref: symbol, path: Array<string>}}
89
+ */
51
90
  reference.toJSON = () => ({
52
91
  [ref]: ref,
53
92
  path
@@ -58,17 +97,29 @@ function instrument (path) {
58
97
  return reference
59
98
  }
60
99
 
61
- export default function module (schema, parentPath = []) {
100
+ /**
101
+ * Builds a nested API structure from a schema definition.
102
+ *
103
+ * Schema supports:
104
+ * - string => leaf function node
105
+ * - [name, children] => namespace with nested schema
106
+ *
107
+ * @param {Array<string | [string, Array]>} schema
108
+ * @param {Array<string>} [parentPath=[]]
109
+ * @returns {Object} Nested instrumented API object
110
+ *
111
+ * @throws {Error} If schema format is invalid
112
+ * @public
113
+ */
114
+ export default function References (schema, parentPath = []) {
62
115
  return schema.reduce((acc, item) => {
63
116
  if (typeof item === 'string') {
64
- // leaf function
65
117
  acc[item] = instrument([...parentPath, item])
66
118
  } else if (Array.isArray(item)) {
67
119
  const [name, children] = item
68
120
 
69
121
  if (Array.isArray(children)) {
70
- // recurse: children can be strings or [name, children] arrays
71
- acc[name] = module(children, [...parentPath, name])
122
+ acc[name] = References(children, [...parentPath, name])
72
123
  } else {
73
124
  throw new Error(`Expected children array for namespace "${name}"`)
74
125
  }