@toa.io/norm 0.2.1-dev.1

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 (71) hide show
  1. package/features/component.operations.feature +54 -0
  2. package/features/steps/.test/manifest.test.js +110 -0
  3. package/features/steps/.types/context.d.ts +9 -0
  4. package/features/steps/hooks.js +18 -0
  5. package/features/steps/manifest.js +66 -0
  6. package/features/steps/parameters.js +9 -0
  7. package/package.json +31 -0
  8. package/src/.component/.expand/bridge.js +9 -0
  9. package/src/.component/.expand/entity.js +14 -0
  10. package/src/.component/.expand/events.js +14 -0
  11. package/src/.component/.expand/extensions.js +10 -0
  12. package/src/.component/.expand/index.js +15 -0
  13. package/src/.component/.expand/operations.js +17 -0
  14. package/src/.component/.expand/receivers.js +16 -0
  15. package/src/.component/.normalize/events.js +13 -0
  16. package/src/.component/.normalize/extensions.js +35 -0
  17. package/src/.component/.normalize/index.js +9 -0
  18. package/src/.component/.normalize/operations.js +12 -0
  19. package/src/.component/collapse.js +44 -0
  20. package/src/.component/defaults.js +25 -0
  21. package/src/.component/dereference.js +72 -0
  22. package/src/.component/expand.js +14 -0
  23. package/src/.component/index.js +17 -0
  24. package/src/.component/merge.js +55 -0
  25. package/src/.component/normalize.js +13 -0
  26. package/src/.component/schema.yaml +222 -0
  27. package/src/.component/validate.js +40 -0
  28. package/src/.context/.dependencies/connectors.js +44 -0
  29. package/src/.context/.dependencies/describe.js +10 -0
  30. package/src/.context/.dependencies/extensions.js +24 -0
  31. package/src/.context/.dependencies/index.js +9 -0
  32. package/src/.context/.dependencies/resolve.js +45 -0
  33. package/src/.context/complete.js +33 -0
  34. package/src/.context/dependencies.js +16 -0
  35. package/src/.context/dereference.js +31 -0
  36. package/src/.context/expand.js +14 -0
  37. package/src/.context/index.js +15 -0
  38. package/src/.context/normalize.js +19 -0
  39. package/src/.context/schema.yaml +60 -0
  40. package/src/.context/validate.js +19 -0
  41. package/src/component.js +59 -0
  42. package/src/context.js +48 -0
  43. package/src/index.js +9 -0
  44. package/src/shortcuts.js +52 -0
  45. package/test/component/collapse.fixtures.js +134 -0
  46. package/test/component/collapse.test.js +81 -0
  47. package/test/component/dereference.fixtures.js +165 -0
  48. package/test/component/dereference.test.js +31 -0
  49. package/test/component/dummies/extension/index.js +9 -0
  50. package/test/component/dummies/extension/package.json +5 -0
  51. package/test/component/expand.fixtures.js +137 -0
  52. package/test/component/expand.test.js +17 -0
  53. package/test/component/normalize.fixtures.js +22 -0
  54. package/test/component/normalize.test.js +45 -0
  55. package/test/component/validate.fixtures.js +58 -0
  56. package/test/component/validate.test.js +250 -0
  57. package/test/context/complete.fixtures.js +59 -0
  58. package/test/context/complete.test.js +31 -0
  59. package/test/context/dereference.fixtures.js +60 -0
  60. package/test/context/dereference.test.js +24 -0
  61. package/test/context/expand.fixtures.js +5 -0
  62. package/test/context/expand.test.js +41 -0
  63. package/test/context/normalize.fixtures.js +32 -0
  64. package/test/context/normalize.test.js +34 -0
  65. package/test/context/validate.fixtures.js +38 -0
  66. package/test/context/validate.test.js +91 -0
  67. package/test/shortcuts.fixtures.js +17 -0
  68. package/test/shortcuts.test.js +92 -0
  69. package/types/component.d.ts +78 -0
  70. package/types/context.d.ts +75 -0
  71. package/types/index.d.ts +5 -0
@@ -0,0 +1,137 @@
1
+ 'use strict'
2
+
3
+ const source = {
4
+ entity: {
5
+ schema: {
6
+ properties: {
7
+ foo: 'string'
8
+ }
9
+ },
10
+ storage: 'mongodb'
11
+ },
12
+ bridge: 'node',
13
+ operations: {
14
+ add: {
15
+ bridge: 'node',
16
+ bindings: ['amqp'],
17
+ input: {
18
+ properties: {
19
+ foo: 'integer',
20
+ bar: {
21
+ type: 'array',
22
+ items: {
23
+ properties: {
24
+ baz: 'string'
25
+ }
26
+ }
27
+ }
28
+ }
29
+ },
30
+ output: {
31
+ properties: {
32
+ foo: 'integer',
33
+ bar: {
34
+ type: 'array',
35
+ items: {
36
+ properties: {
37
+ baz: 'string'
38
+ }
39
+ }
40
+ }
41
+ }
42
+ }
43
+ }
44
+ },
45
+ events: {
46
+ happened: {
47
+ bridge: 'node',
48
+ binding: 'amqp'
49
+ }
50
+ },
51
+ receivers: {
52
+ one: 'transit',
53
+ two: {
54
+ binding: 'amqp',
55
+ bridge: 'node',
56
+ transition: 'transit'
57
+ }
58
+ }
59
+ }
60
+
61
+ const target = {
62
+ entity: {
63
+ schema: {
64
+ type: 'object',
65
+ properties: {
66
+ foo: {
67
+ type: 'string'
68
+ }
69
+ }
70
+ },
71
+ storage: '@toa.io/storages.mongodb'
72
+ },
73
+ bridge: '@toa.io/bridges.node',
74
+ operations: {
75
+ add: {
76
+ bridge: '@toa.io/bridges.node',
77
+ bindings: ['@toa.io/bindings.amqp'],
78
+ input: {
79
+ type: 'object',
80
+ properties: {
81
+ foo: {
82
+ type: 'integer'
83
+ },
84
+ bar: {
85
+ type: 'array',
86
+ items: {
87
+ type: 'object',
88
+ properties: {
89
+ baz: {
90
+ type: 'string'
91
+ }
92
+ }
93
+ }
94
+ }
95
+ }
96
+ },
97
+ output: {
98
+ type: 'object',
99
+ properties: {
100
+ foo: {
101
+ type: 'integer'
102
+ },
103
+ bar: {
104
+ type: 'array',
105
+ items: {
106
+ type: 'object',
107
+ properties: {
108
+ baz: {
109
+ type: 'string'
110
+ }
111
+ }
112
+ }
113
+ }
114
+ }
115
+ }
116
+ }
117
+ },
118
+ events: {
119
+ happened: {
120
+ bridge: '@toa.io/bridges.node',
121
+ binding: '@toa.io/bindings.amqp'
122
+ }
123
+ },
124
+ receivers: {
125
+ one: {
126
+ transition: 'transit'
127
+ },
128
+ two: {
129
+ bridge: '@toa.io/bridges.node',
130
+ binding: '@toa.io/bindings.amqp',
131
+ transition: 'transit'
132
+ }
133
+ }
134
+ }
135
+
136
+ exports.source = source
137
+ exports.target = target
@@ -0,0 +1,17 @@
1
+ 'use strict'
2
+
3
+ const clone = require('clone-deep')
4
+
5
+ const { expand } = require('../../src/.component')
6
+ const fixtures = require('./expand.fixtures')
7
+
8
+ let source
9
+
10
+ beforeEach(() => {
11
+ source = clone(fixtures.source)
12
+ })
13
+
14
+ it('should expand', () => {
15
+ expand(source)
16
+ expect(source).toMatchObject(fixtures.target)
17
+ })
@@ -0,0 +1,22 @@
1
+ 'use strict'
2
+
3
+ const { generate } = require('randomstring')
4
+
5
+ const operations = {
6
+ path: __dirname,
7
+ bindings: ['foo', 'bar'],
8
+ 'bindings@local': ['foo'],
9
+ operations: {
10
+ add: {}
11
+ },
12
+ extensions: {
13
+ '@toa.io/extensions.exposition': {
14
+ ['/' + generate()]: ['add']
15
+ },
16
+ './dummies/extension': {
17
+ ok: true
18
+ }
19
+ }
20
+ }
21
+
22
+ exports.operations = operations
@@ -0,0 +1,45 @@
1
+ 'use strict'
2
+
3
+ const { dirname } = require('node:path')
4
+ const clone = require('clone-deep')
5
+
6
+ const { normalize } = require('../../src/.component')
7
+ const fixtures = require('./normalize.fixtures')
8
+
9
+ let manifest
10
+
11
+ beforeEach(() => {
12
+ manifest = clone(fixtures.operations)
13
+ })
14
+
15
+ describe('environment', () => {
16
+ it('should convolve with environment argument', () => {
17
+ normalize(manifest, 'local')
18
+ expect(manifest.operations.add.bindings).toStrictEqual(['foo'])
19
+ })
20
+ })
21
+
22
+ describe('operations', () => {
23
+ it('should set default bindings', () => {
24
+ normalize(manifest)
25
+
26
+ expect(manifest.operations.add.bindings).toStrictEqual(manifest.bindings)
27
+ })
28
+ })
29
+
30
+ describe('extensions', () => {
31
+ it('should resolve absolute references', () => {
32
+ const origins = manifest.extensions['@toa.io/extensions.origins']
33
+ const path = dirname(require.resolve('@toa.io/extensions.origins/package.json'))
34
+
35
+ normalize(manifest)
36
+
37
+ expect(manifest.extensions[path]).toStrictEqual(origins)
38
+ })
39
+
40
+ it('should throw if manifest is undefined', () => {
41
+ manifest.extensions['./dummies/extension'].ok = false
42
+
43
+ expect(() => normalize(manifest)).toThrow(/didn't return manifest/)
44
+ })
45
+ })
@@ -0,0 +1,58 @@
1
+ 'use strict'
2
+
3
+ const ok = {
4
+ namespace: 'foo',
5
+ name: 'bar',
6
+ entity: {
7
+ storage: 'whatever',
8
+ schema: {
9
+ properties: {
10
+ name: {
11
+ type: 'string',
12
+ minLength: 1,
13
+ maxLength: 128
14
+ }
15
+ }
16
+ }
17
+ },
18
+ operations: {
19
+ get: {
20
+ type: 'observation',
21
+ scope: 'objects',
22
+ bridge: 'whatever',
23
+ bindings: ['@toa.io/bindings.http']
24
+ },
25
+ add: {
26
+ type: 'transition',
27
+ concurrency: 'none',
28
+ scope: 'object',
29
+ bridge: 'whatever',
30
+ bindings: ['@toa.io/bindings.http', '@toa.io/bindings.amqp']
31
+ },
32
+ set: {
33
+ type: 'assignment',
34
+ scope: 'changeset',
35
+ bridge: 'whatever',
36
+ bindings: ['@toa.io/bindings.http', '@toa.io/bindings.amqp']
37
+ }
38
+ },
39
+ events: {
40
+ created: {
41
+ bridge: 'whatever',
42
+ path: '/somewhere',
43
+ conditioned: true,
44
+ subjective: false,
45
+ binding: '@toa.io/bindings.amqp'
46
+ }
47
+ },
48
+ receivers: {
49
+ 'foo.bar.happened': {
50
+ transition: 'add',
51
+ bridge: 'whatever',
52
+ binding: 'amqp',
53
+ path: '/somewhere'
54
+ }
55
+ }
56
+ }
57
+
58
+ exports.ok = ok
@@ -0,0 +1,250 @@
1
+ // noinspection JSUnresolvedVariable
2
+
3
+ 'use strict'
4
+
5
+ const clone = require('clone-deep')
6
+
7
+ const { validate } = require('../../src/.component')
8
+ const fixtures = require('./validate.fixtures')
9
+
10
+ let manifest
11
+
12
+ beforeEach(() => {
13
+ manifest = clone(fixtures.ok)
14
+ })
15
+
16
+ it('should be ok', () => {
17
+ expect(() => validate(manifest)).not.toThrow()
18
+ })
19
+
20
+ it('should provide error', () => {
21
+ manifest.foo = 'bar'
22
+
23
+ expect(() => validate(manifest)).toThrow(/must NOT have additional property/)
24
+ })
25
+
26
+ it('should not have additional properties', () => {
27
+ manifest.foo = 'bar'
28
+
29
+ expect(() => validate(manifest)).toThrow(/must NOT have additional property/)
30
+ })
31
+
32
+ describe('namespace', () => {
33
+ it('should match token pattern', () => {
34
+ manifest.namespace = '1'
35
+ expect(() => validate(manifest)).toThrow(/must match pattern/)
36
+
37
+ manifest.namespace = 'foo_'
38
+ expect(() => validate(manifest)).toThrow(/must match pattern/)
39
+
40
+ manifest.namespace = 'foo-'
41
+ expect(() => validate(manifest)).toThrow(/must match pattern/)
42
+
43
+ manifest.namespace = 'foo-BAR'
44
+ expect(() => validate(manifest)).not.toThrow()
45
+
46
+ manifest.namespace = 'foo_bar'
47
+ expect(() => validate(manifest)).not.toThrow()
48
+
49
+ manifest.namespace = 'FooBar12'
50
+ expect(() => validate(manifest)).not.toThrow()
51
+ })
52
+
53
+ it('should forbid \'system\' namespace', () => {
54
+ manifest.namespace = 'system'
55
+ expect(() => validate(manifest)).toThrow(/must NOT be valid/)
56
+ })
57
+
58
+ it('should forbid \'default\' namespace', () => {
59
+ manifest.namespace = 'default'
60
+ expect(() => validate(manifest)).toThrow(/must NOT be valid/)
61
+ })
62
+ })
63
+
64
+ describe('name', () => {
65
+ it('should be optional', () => {
66
+ delete manifest.name
67
+ expect(() => validate(manifest)).not.toThrow()
68
+ })
69
+ })
70
+
71
+ describe('entity', () => {
72
+ it('should be optional', () => {
73
+ delete manifest.entity
74
+ expect(() => validate(manifest)).not.toThrow()
75
+ })
76
+
77
+ it('should be object', () => {
78
+ manifest.entity = 'foo'
79
+ expect(() => validate(manifest)).toThrow(/must be object/)
80
+ })
81
+
82
+ it('should not have additional properties', () => {
83
+ manifest.entity.foo = 'bar'
84
+ expect(() => validate(manifest)).toThrow(/must NOT have additional property/)
85
+ })
86
+
87
+ describe('schema', () => {
88
+ it('should be required', () => {
89
+ delete manifest.entity.schema
90
+ expect(() => validate(manifest)).toThrow(/must have required property/)
91
+ })
92
+
93
+ it('should be JSON schema object', () => {
94
+ manifest.entity.schema = { properties: { foo: 'bar' } }
95
+ expect(() => validate(manifest)).toThrow()
96
+ })
97
+
98
+ it('should be JSON schema object of type object', () => {
99
+ manifest.entity.schema = { type: 'integer' }
100
+ expect(() => validate(manifest)).toThrow(/must be equal to constant 'object'/)
101
+
102
+ manifest.entity.schema = {}
103
+ validate(manifest)
104
+ expect(manifest.entity.schema.type).toBe('object')
105
+ })
106
+
107
+ it('should forbid additional properties', () => {
108
+ manifest.entity.schema = { additionalProperties: true }
109
+ expect(() => validate(manifest)).toThrow(/must be equal to constant 'false'/)
110
+
111
+ manifest.entity.schema = {}
112
+ validate(manifest)
113
+ expect(manifest.entity.schema.additionalProperties).toBe(false)
114
+ })
115
+
116
+ it('should have property names matching token pattern', () => {
117
+ manifest.entity.schema.properties._foo = { type: 'string' }
118
+ expect(() => validate(manifest)).toThrow(/pattern/)
119
+ })
120
+
121
+ it('should allow default id', () => {
122
+ manifest.entity.schema.properties.id = { type: 'string', pattern: '^[a-fA-F0-9]+$' }
123
+ expect(() => validate(manifest)).not.toThrow()
124
+ })
125
+ })
126
+
127
+ describe('initialized', () => {
128
+ it('should provide default', () => {
129
+ expect(() => validate(manifest)).not.toThrow()
130
+ expect(manifest.entity.initialized).toBe(false)
131
+ })
132
+ })
133
+ })
134
+
135
+ describe('bindings', () => {
136
+ it('should be array of unique strings', () => {
137
+ manifest.bindings = 'oops'
138
+ expect(() => validate(manifest)).toThrow(/must be array/)
139
+
140
+ manifest.bindings = ['oops', 'oops']
141
+ expect(() => validate(manifest)).toThrow(/duplicate items/)
142
+
143
+ manifest.bindings = ['oops', {}]
144
+ expect(() => validate(manifest)).toThrow(/must be string/)
145
+ })
146
+
147
+ it('should forbid explicit loop', () => {
148
+ manifest.bindings = ['@toa.io/bindings.loop']
149
+ expect(() => validate(manifest)).toThrow(/must NOT be valid/)
150
+ })
151
+ })
152
+
153
+ describe('operations', () => {
154
+ it('should be object', () => {
155
+ manifest.operations.get = 'bar'
156
+ expect(() => validate(manifest)).toThrow(/must be object/)
157
+ })
158
+
159
+ it('should not have additional properties', () => {
160
+ manifest.operations.get.foo = 'bar'
161
+ expect(() => validate(manifest)).toThrow(/additional property/)
162
+ })
163
+
164
+ it('should have type (transition or observation)', () => {
165
+ delete manifest.operations.get.type
166
+ expect(() => validate(manifest)).toThrow()
167
+
168
+ manifest.operations.get.type = 'foo'
169
+ expect(() => validate(manifest)).toThrow(/one of the allowed values/)
170
+ })
171
+
172
+ it('should forbid explicit loop', () => {
173
+ manifest.operations.get.bindings = ['@toa.io/bindings.loop']
174
+ expect(() => validate(manifest)).toThrow(/must NOT be valid/)
175
+ })
176
+
177
+ it('should forbid query: false for observations', () => {
178
+ manifest.operations.get.query = false
179
+ expect(() => validate(manifest)).toThrow(/must NOT be valid/)
180
+ })
181
+
182
+ describe('scope', () => {
183
+ it('should have scope', () => {
184
+ delete manifest.operations.get.scope
185
+ expect(() => validate(manifest)).toThrow(/required property/)
186
+ })
187
+
188
+ it('should allow only entity or set for observations', () => {
189
+ manifest.operations.get.scope = 'changeset'
190
+ expect(() => validate(manifest)).toThrow(/allowed values/)
191
+ })
192
+
193
+ it('should allow only entity for transitions', () => {
194
+ manifest.operations.add.scope = 'changeset'
195
+ expect(() => validate(manifest)).toThrow(/allowed values/)
196
+
197
+ manifest.operations.add.scope = 'set'
198
+ expect(() => validate(manifest)).toThrow(/allowed values/)
199
+ })
200
+
201
+ it('should allow only changeset for assignments', () => {
202
+ manifest.operations.set.scope = 'changeset'
203
+ expect(() => validate(manifest)).not.toThrow()
204
+
205
+ manifest.operations.set.scope = 'set'
206
+ expect(() => validate(manifest)).toThrow(/allowed values/)
207
+ })
208
+ })
209
+
210
+ describe('concurrency', () => {
211
+ it('should be required for transitions', () => {
212
+ delete manifest.operations.add.concurrency
213
+ expect(() => validate(manifest)).toThrow(/required property/)
214
+ })
215
+
216
+ it('should throw for observations, assignments', () => {
217
+ manifest.operations.get.concurrency = 'none'
218
+ expect(() => validate(manifest)).toThrow()
219
+ delete manifest.operations.get.concurrency
220
+
221
+ manifest.operations.set.concurrency = 'none'
222
+ expect(() => validate(manifest)).toThrow()
223
+ })
224
+ })
225
+
226
+ describe('input, output', () => {
227
+ it('should be schema', () => {
228
+ manifest.operations.get.input = { properties: { foo: 'bar' } }
229
+ expect(() => validate(manifest)).toThrow()
230
+
231
+ delete manifest.operations.get.input
232
+ manifest.operations.get.output = { properties: { foo: 'bar' } }
233
+ expect(() => validate(manifest)).toThrow()
234
+ })
235
+ })
236
+ })
237
+
238
+ describe('receivers', () => {
239
+ it('should throw if transition points to undefined transition', () => {
240
+ manifest.receivers['foo.bar.happened'].transition = 'not-exists'
241
+
242
+ expect(() => validate(manifest)).toThrow(/refers to undefined transition/)
243
+ })
244
+
245
+ it('should throw if transition points to non transition', () => {
246
+ manifest.receivers['foo.bar.happened'].transition = 'get'
247
+
248
+ expect(() => validate(manifest)).toThrow(/refers to non-transition/)
249
+ })
250
+ })
@@ -0,0 +1,59 @@
1
+ 'use strict'
2
+
3
+ const component = (id) => {
4
+ const [namespace, name] = id.split('.')
5
+
6
+ return {
7
+ namespace,
8
+ name,
9
+ version: '0.0.0',
10
+ locator: {
11
+ namespace,
12
+ name,
13
+ id,
14
+ label: `${namespace}-${name}`
15
+ }
16
+ }
17
+ }
18
+
19
+ const context = {
20
+ runtime: '0.0.0',
21
+ name: 'test',
22
+ description: 'context fixture',
23
+ version: '0.0.0',
24
+ packages: 'namespaces/**/*',
25
+ registry: 'localhost:5000',
26
+ components: [
27
+ component('a.b'),
28
+ component('b.a'),
29
+ component('d.a'),
30
+ component('d.b'),
31
+ component('d.c')
32
+ ],
33
+ compositions: [
34
+ {
35
+ name: 'foo',
36
+ components: [component('a.b'), component('b.a')]
37
+ },
38
+ {
39
+ name: 'bar',
40
+ components: [component('d.c'), component('a.b')]
41
+ }
42
+ ]
43
+ }
44
+
45
+ /** @type {Array<toa.norm.context.Composition>} */
46
+ const compositions = [
47
+ ...context.compositions,
48
+ {
49
+ name: 'd-a',
50
+ components: [component('d.a')]
51
+ },
52
+ {
53
+ name: 'd-b',
54
+ components: [component('d.b')]
55
+ }
56
+ ]
57
+
58
+ exports.context = context
59
+ exports.compositions = compositions
@@ -0,0 +1,31 @@
1
+ 'use strict'
2
+
3
+ const clone = require('clone-deep')
4
+
5
+ const { complete } = require('../../src/.context')
6
+ const fixtures = require('./complete.fixtures')
7
+
8
+ /** @type {toa.norm.Context} */
9
+ let context
10
+
11
+ beforeEach(() => {
12
+ context = clone(fixtures.context)
13
+ complete(context)
14
+ })
15
+
16
+ it('should complete compositions', () => {
17
+ expect(context.compositions.length).toEqual(fixtures.compositions.length)
18
+ expect(context.compositions).toEqual(expect.arrayContaining(fixtures.compositions))
19
+ })
20
+
21
+ it('should create if compositions are not set', () => {
22
+ context.compositions = undefined
23
+
24
+ const compositions = context.components.map((component) => ({
25
+ name: component.locator.label,
26
+ components: [component]
27
+ }))
28
+
29
+ expect(() => complete(context)).not.toThrow()
30
+ expect(context.compositions).toStrictEqual(compositions)
31
+ })
@@ -0,0 +1,60 @@
1
+ 'use strict'
2
+
3
+ const clone = require('clone-deep')
4
+
5
+ /**
6
+ * @param id {string}
7
+ * @return {toa.norm.component.Declaration}
8
+ */
9
+ const component = (id) => {
10
+ const [namespace, name] = id.split('.')
11
+
12
+ return {
13
+ namespace,
14
+ name,
15
+ version: '0.0.0',
16
+ locator: {
17
+ namespace,
18
+ name,
19
+ id,
20
+ label: `${namespace}-${name}`
21
+ },
22
+ entity: null
23
+ }
24
+ }
25
+
26
+ const context = {
27
+ name: 'test',
28
+ description: 'context fixture',
29
+ version: '0.0.0',
30
+ runtime: '0.0.0',
31
+ packages: 'namespaces/**/*',
32
+ registry: 'localhost:5000',
33
+ components: [component('a.b'), component('b.a'), component('d.c')],
34
+ compositions: [
35
+ {
36
+ name: 'foo',
37
+ components: ['a.b', 'b.a']
38
+ },
39
+ {
40
+ name: 'bar',
41
+ components: ['d.c', 'a.b']
42
+ }
43
+ ]
44
+ }
45
+
46
+ const expected = clone(context)
47
+
48
+ expected.compositions = [
49
+ {
50
+ name: 'foo',
51
+ components: [component('a.b'), component('b.a')]
52
+ },
53
+ {
54
+ name: 'bar',
55
+ components: [component('d.c'), component('a.b')]
56
+ }
57
+ ]
58
+
59
+ exports.context = context
60
+ exports.expected = expected
@@ -0,0 +1,24 @@
1
+ 'use strict'
2
+
3
+ const clone = require('clone-deep')
4
+
5
+ const { dereference } = require('../../src/.context')
6
+ const fixtures = require('./dereference.fixtures')
7
+
8
+ /** @type {toa.norm.Context} */
9
+ let context
10
+
11
+ beforeAll(() => {
12
+ context = clone(fixtures.context)
13
+ dereference(context)
14
+ })
15
+
16
+ it('should dereference', () => {
17
+ expect(context).toEqual(expect.objectContaining(fixtures.expected))
18
+ })
19
+
20
+ it('should not throw on empty compositions', () => {
21
+ context.compositions = undefined
22
+
23
+ expect(() => dereference(context)).not.toThrow()
24
+ })