@toa.io/userland 0.7.3 → 0.8.0

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@toa.io/userland",
3
- "version": "0.7.3",
3
+ "version": "0.8.0",
4
4
  "description": "Toa development kit",
5
5
  "homepage": "https://toa.io",
6
6
  "author": {
@@ -33,5 +33,5 @@
33
33
  "@toa.io/yaml": "0.7.1",
34
34
  "tap": "16.3.4"
35
35
  },
36
- "gitHead": "9989ea316ebf5698b1eee933e83a86e6a18b406b"
36
+ "gitHead": "38a4b7aa2f80ba650a796c517056fde8daeec75d"
37
37
  }
package/samples/readme.md CHANGED
@@ -15,11 +15,10 @@ See [features](/features/replay).
15
15
 
16
16
  Sample is an object containing values of operation inputs (i.e.: request, context outputs and
17
17
  current state) to be substituted and outcomes (reply, context calls, next state and events emission)
18
- to be verified. See its [schema](./src/.suite/.component/sample.cos.yaml).
18
+ to be verified. See its [schema](./src/.replay/.suite/translate/schemas/operation.cos.yaml).
19
19
 
20
20
  > Although `input` and `output` are declared as arbitrary values, they must conform to the
21
- > corresponding
22
- > operation schemas.
21
+ > corresponding operation schemas.
23
22
 
24
23
  ### Declaration
25
24
 
@@ -35,7 +34,7 @@ and `name` must match corresponding component, therefore are optional.
35
34
 
36
35
  Message Sample is an object containing receiver's input (`payload`) to be substituted and
37
36
  outcomes (`input` and `query`) to be verified. Message sample may contain corresponding operation
38
- sample. See its [schema](#).
37
+ sample. See its [schema](./src/.replay/.suite/translate/schemas/message.cos.yaml).
39
38
 
40
39
  > Message samples are always [autonomous](#autonomy).
41
40
 
@@ -4,8 +4,8 @@ const { components: load } = require('./suite')
4
4
  const { replay } = require('./replay')
5
5
 
6
6
  /** @type {toa.samples.replay.components} */
7
- const components = async (paths) => {
8
- const suite = await load(paths)
7
+ const components = async (paths, options = {}) => {
8
+ const suite = await load(paths, options)
9
9
 
10
10
  return await replay(suite, paths)
11
11
  }
@@ -7,12 +7,17 @@ const { context: load } = require('./suite')
7
7
  const { replay } = require('./replay')
8
8
 
9
9
  /** @type {toa.samples.replay.context} */
10
- const context = async (path) => {
10
+ const context = async (path, options = {}) => {
11
11
  const context = await norm.context(path)
12
12
  const paths = context.components.map((component) => component.path)
13
- const suite = await load(path)
13
+ const suite = await load(path, options)
14
14
 
15
- return await test.components(paths) && await replay(suite, paths)
15
+ let ok = true
16
+
17
+ if (options.integration !== true) ok = await test.components(paths, options)
18
+ if (ok) ok = await replay(suite, paths)
19
+
20
+ return ok
16
21
  }
17
22
 
18
23
  exports.context = context
@@ -0,0 +1,15 @@
1
+ 'use strict'
2
+
3
+ /**
4
+ * @template {{ title: string }} T
5
+ * @param {T[]} samples
6
+ * @param {string} expression
7
+ * @returns {T}
8
+ */
9
+ function filter (samples, expression) {
10
+ const rx = new RegExp(expression)
11
+
12
+ return samples.filter((sample) => rx.test(sample.title))
13
+ }
14
+
15
+ exports.filter = filter
@@ -4,12 +4,14 @@ const { join, basename } = require('node:path')
4
4
  const { file: { glob } } = require('@toa.io/filesystem')
5
5
  const yaml = require('@toa.io/yaml')
6
6
 
7
+ const { filter } = require('./filter')
8
+
7
9
  /**
8
10
  * @param {string} path
9
- * @param {string} [id]
11
+ * @param {toa.samples.suite.Options} options
10
12
  * @returns {Promise<toa.samples.messages.Set>}
11
13
  */
12
- const messages = async (path, id) => {
14
+ const messages = async (path, options) => {
13
15
  /** @type {toa.samples.messages.Set} */
14
16
  const messages = {}
15
17
 
@@ -18,9 +20,12 @@ const messages = async (path, id) => {
18
20
 
19
21
  for (const file of files) {
20
22
  const label = basename(file, EXTENSION)
21
- const samples = /** @type {toa.samples.Message[]} */ await yaml.load.all(file)
22
23
 
23
- if (id !== undefined) samples.forEach((sample) => (sample.component = id))
24
+ let samples = /** @type {toa.samples.Message[]} */ await yaml.load.all(file)
25
+
26
+ if (options.id !== undefined) samples.forEach((sample) => (sample.component = options.id))
27
+ if (options.component !== undefined) samples = samples.filter((sample) => sample.component === options.component)
28
+ if (options.title !== undefined) samples = filter(samples, options.title)
24
29
 
25
30
  messages[label] = samples
26
31
  }
@@ -6,13 +6,14 @@ const { merge } = require('@toa.io/generic')
6
6
  const yaml = require('@toa.io/yaml')
7
7
 
8
8
  const { parse } = require('./parse')
9
+ const { filter } = require('./filter')
9
10
 
10
11
  /**
11
12
  * @param {string} path
12
- * @param {string} [id]
13
+ * @param {toa.samples.suite.Options} options
13
14
  * @returns {Promise<toa.samples.suite.Operations>}
14
15
  */
15
- const operations = async (path, id) => {
16
+ const operations = async (path, options) => {
16
17
  /** @type {toa.samples.suite.Operations} */
17
18
  const operations = {}
18
19
 
@@ -21,10 +22,15 @@ const operations = async (path, id) => {
21
22
 
22
23
  for (const file of files) {
23
24
  const name = basename(file, EXTENSION)
24
- const [component, operation] = parse(name, id)
25
+ const [component, operation] = parse(name, options.id)
25
26
 
26
- /** @type {toa.samples.Operation[]} */
27
- const samples = await yaml.load.all(file)
27
+ if (options.component !== undefined && component !== options.component) continue
28
+ if (options.operation !== undefined && operation !== options.operation) continue
29
+
30
+ let samples = /** @type {toa.samples.Operation[]} */ await yaml.load.all(file)
31
+
32
+ if (options.title !== undefined) samples = filter(samples, options.title)
33
+ if (samples.length === 0) continue
28
34
 
29
35
  if (operations[component] === undefined) operations[component] = {}
30
36
 
@@ -32,7 +38,7 @@ const operations = async (path, id) => {
32
38
  const set = operations[component]
33
39
 
34
40
  if (set[operation] === undefined) set[operation] = samples
35
- else set[operation] = merge(set[operation], samples)
41
+ else set[operation] = /** @type {toa.samples.Operation[]} */ merge(set[operation], samples)
36
42
  }
37
43
 
38
44
  return operations
@@ -14,10 +14,6 @@ const parse = (name, def) => {
14
14
  throw new Error(`Component id mismatch: '${id}' expected, '${component}' given`)
15
15
  }
16
16
 
17
- // if (id === undefined) {
18
- // throw new Error('Sample file name must be an operation endpoint')
19
- // }
20
-
21
17
  return [id, endpoint]
22
18
  }
23
19
 
@@ -7,17 +7,20 @@ const read = require('./.read')
7
7
 
8
8
  /**
9
9
  * @param {string[]} paths
10
+ * @param {toa.samples.suite.Options} [options]
10
11
  * @returns {Promise<toa.samples.Suite>}
11
12
  */
12
- const components = async (paths) => {
13
+ const components = async (paths, options = {}) => {
13
14
  /** @type {toa.samples.Suite} */
14
15
  const suite = { title: 'Component samples', autonomous: true, operations: {}, messages: {} }
15
16
 
16
17
  for (const path of paths) {
17
18
  const manifest = await norm.component(path)
18
- const id = manifest.locator.id
19
- const operations = await read.operations(path, id)
20
- const messages = await read.messages(path, id)
19
+
20
+ options.id = manifest.locator.id
21
+
22
+ const operations = await read.operations(path, options)
23
+ const messages = await read.messages(path, options)
21
24
 
22
25
  merge(suite, { operations, messages })
23
26
  }
@@ -4,14 +4,15 @@ const read = require('./.read')
4
4
 
5
5
  /**
6
6
  * @param {string} path
7
+ * @param {toa.samples.suite.Options} [options]
7
8
  * @returns {Promise<toa.samples.Suite>}
8
9
  */
9
- const context = async (path) => {
10
+ const context = async (path, options = {}) => {
10
11
  /** @type {toa.samples.Suite} */
11
12
  const suite = { title: 'Integration samples', autonomous: false }
12
13
 
13
- suite.operations = await read.operations(path)
14
- suite.messages = await read.messages(path)
14
+ suite.operations = await read.operations(path, options)
15
+ suite.messages = await read.messages(path, options)
15
16
 
16
17
  return suite
17
18
  }
@@ -17,15 +17,18 @@ it('should be', () => {
17
17
 
18
18
  const paths = [generate()]
19
19
 
20
+ /** @type {toa.samples.suite.Options} */
21
+ const options = { component: generate() }
22
+
20
23
  /** @type {boolean} */
21
24
  let result
22
25
 
23
26
  beforeAll(async () => {
24
- result = await components(paths)
27
+ result = await components(paths, options)
25
28
  })
26
29
 
27
30
  it('should load suite', async () => {
28
- expect(mock.suite.components).toHaveBeenCalledWith(paths)
31
+ expect(mock.suite.components).toHaveBeenCalledWith(paths, options)
29
32
  })
30
33
 
31
34
  it('should replay suite', async () => {
@@ -0,0 +1,6 @@
1
+ title: Something happened with a dummy
2
+ payload:
3
+ foo: bar
4
+ input: foo
5
+ query:
6
+ id: bar
@@ -0,0 +1,2 @@
1
+ namespace: dummies
2
+ name: pot
@@ -0,0 +1,11 @@
1
+ title: First case
2
+ input:
3
+ foo: bar
4
+ output:
5
+ baz: true
6
+ ---
7
+ title: Second case
8
+ input:
9
+ foo: qux
10
+ output:
11
+ baz: false
@@ -1,4 +1,4 @@
1
- title: Foobar
1
+ title: Something happened with a pot
2
2
  payload:
3
3
  foo: bar
4
4
  input: foo
@@ -14,6 +14,7 @@ jest.mock('../src/replay', () => mock.replay)
14
14
  jest.mock('../src/components', () => mock.components)
15
15
 
16
16
  const { context } = require('../src/context')
17
+ const { generate } = require('randomstring')
17
18
 
18
19
  it('should be', async () => {
19
20
  expect(context).toBeDefined()
@@ -25,6 +26,9 @@ const COMPONENTS = resolve(CONTEXT, 'components/*')
25
26
  /** @type {string[]} */
26
27
  let paths
27
28
 
29
+ /** @type {toa.samples.suite.Options} */
30
+ const options = { component: generate() }
31
+
28
32
  /** @type {boolean} */
29
33
  let ok
30
34
 
@@ -37,15 +41,15 @@ beforeEach(async () => {
37
41
 
38
42
  mock.components.components.mockImplementation(async () => true)
39
43
 
40
- ok = await context(CONTEXT)
44
+ ok = await context(CONTEXT, options)
41
45
  })
42
46
 
43
47
  it('should replay context components sample sets', async () => {
44
- expect(mock.components.components).toHaveBeenCalledWith(paths)
48
+ expect(mock.components.components).toHaveBeenCalledWith(paths, options)
45
49
  })
46
50
 
47
51
  it('should load integration suite', async () => {
48
- expect(mock.suite.context).toHaveBeenCalledWith(CONTEXT)
52
+ expect(mock.suite.context).toHaveBeenCalledWith(CONTEXT, options)
49
53
  })
50
54
 
51
55
  it('should replay integration suite', async () => {
@@ -9,14 +9,16 @@ it('should be', () => {
9
9
  expect(components).toBeDefined()
10
10
  })
11
11
 
12
- const root = resolve(__dirname, 'context/components/ok')
13
- const paths = [root]
12
+ const dummy = resolve(__dirname, 'context/components/dummy')
13
+ const pot = resolve(__dirname, 'context/components/pot')
14
14
  const component = 'dummies.dummy'
15
15
 
16
16
  /** @type {toa.samples.Suite} */
17
17
  let suite
18
18
 
19
19
  beforeAll(async () => {
20
+ const paths = [dummy]
21
+
20
22
  suite = await components(paths)
21
23
  })
22
24
 
@@ -51,11 +53,72 @@ it('should load message samples', async () => {
51
53
  expect(suite.messages).toStrictEqual(expected)
52
54
  })
53
55
 
56
+ describe('options', () => {
57
+ const paths = [dummy, pot]
58
+
59
+ it('should filter samples by component id', async () => {
60
+ /** @type {toa.samples.suite.Options} */
61
+ const options = { component: 'dummies.dummy' }
62
+
63
+ suite = await components(paths, options)
64
+
65
+ expect(suite.operations['dummies.pot']).toBeUndefined()
66
+
67
+ const messages = suite.messages['somewhere.something.happened']
68
+
69
+ expect(messages.length).toStrictEqual(1)
70
+ expect(messages[0].component).toStrictEqual('dummies.dummy')
71
+ })
72
+
73
+ it('should filter samples by operation name', async () => {
74
+ /** @type {toa.samples.suite.Options} */
75
+ const options = { operation: 'do' }
76
+
77
+ suite = await components(paths, options)
78
+
79
+ expect('undo' in suite.operations['dummies.dummy']).toStrictEqual(false)
80
+ })
81
+
82
+ it('should filter operation samples by title', async () => {
83
+ /** @type {toa.samples.suite.Options} */
84
+ const options = { title: 'Should not undo' }
85
+
86
+ suite = await components(paths, options)
87
+
88
+ expect('do' in suite.operations['dummies.dummy']).toStrictEqual(false)
89
+ expect(suite.operations['dummies.dummy'].undo.length).toStrictEqual(1)
90
+ expect(suite.operations['dummies.dummy'].undo[0].title).toStrictEqual(options.title)
91
+ })
92
+
93
+ it('should filter operation samples by title as regexp', async () => {
94
+ /** @type {toa.samples.suite.Options} */
95
+ const options = { title: 'Should [a-z]{2}t undo' }
96
+
97
+ suite = await components(paths, options)
98
+
99
+ expect(suite.operations['dummies.dummy']?.do).toBeUndefined()
100
+ expect(suite.operations['dummies.dummy'].undo.length).toStrictEqual(1)
101
+ expect(suite.operations['dummies.dummy'].undo[0].title).toStrictEqual('Should not undo')
102
+ })
103
+
104
+ it('should filter message samples by title', async () => {
105
+ /** @type {toa.samples.suite.Options} */
106
+ const options = { title: 'Something happened with a dummy' }
107
+
108
+ suite = await components(paths, options)
109
+
110
+ const messages = suite.messages['somewhere.something.happened']
111
+
112
+ expect(messages.length).toStrictEqual(1)
113
+ expect(messages[0].title).toStrictEqual(options.title)
114
+ })
115
+ })
116
+
54
117
  /**
55
118
  * @returns {Promise<toa.samples.operations.Set>}
56
119
  */
57
120
  const operations = async () => {
58
- const path = resolve(root, 'samples')
121
+ const path = resolve(dummy, 'samples')
59
122
 
60
123
  /** @type {toa.samples.Operation[]} */
61
124
  const do1 = (await yaml.load.all(resolve(path, 'do.yaml')))
@@ -77,7 +140,7 @@ const operations = async () => {
77
140
  */
78
141
  const messages = async () => {
79
142
  const label = 'somewhere.something.happened'
80
- const file = resolve(root, 'samples/messages', label + '.yaml')
143
+ const file = resolve(dummy, 'samples/messages', label + '.yaml')
81
144
  const declarations = await yaml.load.all(file)
82
145
  const messages = declarations.map((sample) => ({ component, ...sample }))
83
146
 
@@ -1,9 +1,9 @@
1
- import * as _suite from "./suite";
1
+ import * as _suite from './suite'
2
2
 
3
3
  declare namespace toa.samples.replay {
4
4
 
5
- type components = (paths: string[]) => Promise<boolean>
6
- type context = (path: string) => Promise<boolean>
5
+ type components = (paths: string[], options?: _suite.Options) => Promise<boolean>
6
+ type context = (path: string, options?: _suite.Options) => Promise<boolean>
7
7
  type replay = (suite: _suite.Suite, paths: string[]) => Promise<boolean>
8
8
 
9
9
  }
@@ -5,6 +5,14 @@ declare namespace toa.samples {
5
5
 
6
6
  namespace suite {
7
7
  type Operations = Record<string, _operations.Set>
8
+
9
+ type Options = {
10
+ id?: string
11
+ integration?: boolean
12
+ component?: string
13
+ operation?: string
14
+ title?: string
15
+ }
8
16
  }
9
17
 
10
18
  type Suite = {
@@ -17,3 +25,4 @@ declare namespace toa.samples {
17
25
  }
18
26
 
19
27
  export type Suite = toa.samples.Suite
28
+ export type Options = toa.samples.suite.Options