From aa7fddcfc20b3689400bd69d9b865f9306e6991f Mon Sep 17 00:00:00 2001 From: SamTolmay Date: Thu, 19 Aug 2021 14:06:22 +0200 Subject: [PATCH] feat(build): Add support for resolver functions in _ref operator. --- .../src/build/buildRefs/buildRefs.test.js | 956 +++++++++++------- .../src/build/buildRefs/getConfigFile.js | 39 + .../src/build/buildRefs/getRefContent.js | 44 +- .../src/build/buildRefs/makeRefDefinition.js | 7 +- .../src/build/buildRefs/parseRefContent.js | 43 + .../src/build/buildRefs/recursiveBuild.js | 2 +- .../src/build/buildRefs/runRefResolver.js | 41 + .../buildRefs/testBuildRefsAsyncFunction.js | 26 + .../buildRefs/testBuildRefsErrorResolver.js | 21 + .../buildRefs/testBuildRefsNullResolver.js | 22 + .../buildRefs/testBuildRefsParsingResolver.js | 42 + .../test/buildRefs/testBuildRefsResolver.js | 26 + .../test/buildRefs/testBuildRefsTransform.js | 29 + .../build/src/test/testBuildRefsTransform.js | 13 - 14 files changed, 899 insertions(+), 412 deletions(-) create mode 100644 packages/build/src/build/buildRefs/getConfigFile.js create mode 100644 packages/build/src/build/buildRefs/parseRefContent.js create mode 100644 packages/build/src/build/buildRefs/runRefResolver.js create mode 100644 packages/build/src/test/buildRefs/testBuildRefsAsyncFunction.js create mode 100644 packages/build/src/test/buildRefs/testBuildRefsErrorResolver.js create mode 100644 packages/build/src/test/buildRefs/testBuildRefsNullResolver.js create mode 100644 packages/build/src/test/buildRefs/testBuildRefsParsingResolver.js create mode 100644 packages/build/src/test/buildRefs/testBuildRefsResolver.js create mode 100644 packages/build/src/test/buildRefs/testBuildRefsTransform.js delete mode 100644 packages/build/src/test/testBuildRefsTransform.js diff --git a/packages/build/src/build/buildRefs/buildRefs.test.js b/packages/build/src/build/buildRefs/buildRefs.test.js index 2d2b1c93c..2d406aeda 100644 --- a/packages/build/src/build/buildRefs/buildRefs.test.js +++ b/packages/build/src/build/buildRefs/buildRefs.test.js @@ -34,21 +34,6 @@ const context = testContext({ readConfigFile: mockReadConfigFile, }); -test('buildRefs file not found', async () => { - const files = [ - { - path: 'lowdefy.yaml', - content: ` -doesNotExist: - _ref: doesNotExist`, - }, - ]; - mockReadConfigFile.mockImplementation(readConfigFileMockImplementation(files)); - await expect(buildRefs({ context })).rejects.toThrow( - 'Tried to reference file with path "doesNotExist", but file does not exist.' - ); -}); - test('buildRefs no refs', async () => { const files = [ { @@ -105,6 +90,21 @@ var1: }); }); +test('buildRefs file not found', async () => { + const files = [ + { + path: 'lowdefy.yaml', + content: ` +doesNotExist: + _ref: doesNotExist`, + }, + ]; + mockReadConfigFile.mockImplementation(readConfigFileMockImplementation(files)); + await expect(buildRefs({ context })).rejects.toThrow( + 'Tried to reference file "doesNotExist" from "lowdefy.yaml", but file does not exist.' + ); +}); + test('buildRefs max recursion depth', async () => { const files = [ { @@ -177,44 +177,6 @@ Hello.`, }); }); -test('buildRefs should copy vars', async () => { - const files = [ - { - path: 'lowdefy.yaml', - content: ` -ref: - _ref: - path: file.yaml - vars: - var1: - key: value`, - }, - { - path: 'file.yaml', - content: ` -ref1: - _var: var1 -ref2: - _var: var1`, - }, - ]; - mockReadConfigFile.mockImplementation(readConfigFileMockImplementation(files)); - const res = await buildRefs({ context }); - expect(res).toEqual({ - ref: { - ref1: { key: 'value' }, - ref2: { key: 'value' }, - }, - }); - res.ref.ref1.key = 'newValue'; - expect(res).toEqual({ - ref: { - ref1: { key: 'newValue' }, - ref2: { key: 'value' }, - }, - }); -}); - test('buildRefs null ref definition', async () => { const files = [ { @@ -261,57 +223,58 @@ invalid: ); }); -test('buildRefs nunjucks text file', async () => { - const files = [ - { - path: 'lowdefy.yaml', - content: ` +describe('Parse ref content', () => { + test('buildRefs nunjucks text file', async () => { + const files = [ + { + path: 'lowdefy.yaml', + content: ` templated: _ref: path: template.njk vars: var_1: There`, - }, - { - path: 'template.njk', - content: 'Hello {{ var_1 }}', - }, - ]; - mockReadConfigFile.mockImplementation(readConfigFileMockImplementation(files)); - const res = await buildRefs({ context }); - expect(res).toEqual({ - templated: 'Hello There', + }, + { + path: 'template.njk', + content: 'Hello {{ var_1 }}', + }, + ]; + mockReadConfigFile.mockImplementation(readConfigFileMockImplementation(files)); + const res = await buildRefs({ context }); + expect(res).toEqual({ + templated: 'Hello There', + }); }); -}); -test('buildRefs nunjucks json file', async () => { - const files = [ - { - path: 'lowdefy.yaml', - content: ` + test('buildRefs nunjucks json file', async () => { + const files = [ + { + path: 'lowdefy.yaml', + content: ` templated: _ref: path: template.json.njk vars: key: key1`, - }, - { - path: 'template.json.njk', - content: '{ "{{ key }}": true }', - }, - ]; - mockReadConfigFile.mockImplementation(readConfigFileMockImplementation(files)); - const res = await buildRefs({ context }); - expect(res).toEqual({ - templated: { key1: true }, + }, + { + path: 'template.json.njk', + content: '{ "{{ key }}": true }', + }, + ]; + mockReadConfigFile.mockImplementation(readConfigFileMockImplementation(files)); + const res = await buildRefs({ context }); + expect(res).toEqual({ + templated: { key1: true }, + }); }); -}); -test('buildRefs nunjucks yaml file', async () => { - const files = [ - { - path: 'lowdefy.yaml', - content: ` + test('buildRefs nunjucks yaml file', async () => { + const files = [ + { + path: 'lowdefy.yaml', + content: ` templated: _ref: path: template.yaml.njk @@ -319,28 +282,28 @@ templated: values: - value1 - value2`, - }, - { - path: 'template.yaml.njk', - content: `list: + }, + { + path: 'template.yaml.njk', + content: `list: {% for value in values %} - key: {{ value }} {% endfor %} `, - }, - ]; - mockReadConfigFile.mockImplementation(readConfigFileMockImplementation(files)); - const res = await buildRefs({ context }); - expect(res).toEqual({ - templated: { list: [{ key: 'value1' }, { key: 'value2' }] }, + }, + ]; + mockReadConfigFile.mockImplementation(readConfigFileMockImplementation(files)); + const res = await buildRefs({ context }); + expect(res).toEqual({ + templated: { list: [{ key: 'value1' }, { key: 'value2' }] }, + }); }); -}); -test('buildRefs nunjucks yml file', async () => { - const files = [ - { - path: 'lowdefy.yaml', - content: ` + test('buildRefs nunjucks yml file', async () => { + const files = [ + { + path: 'lowdefy.yaml', + content: ` templated: _ref: path: template.yml.njk @@ -348,109 +311,261 @@ templated: values: - value1 - value2`, - }, - { - path: 'template.yml.njk', - content: `list: + }, + { + path: 'template.yml.njk', + content: `list: {% for value in values %} - key: {{ value }} {% endfor %} `, - }, - ]; - mockReadConfigFile.mockImplementation(readConfigFileMockImplementation(files)); - const res = await buildRefs({ context }); - expect(res).toEqual({ - templated: { list: [{ key: 'value1' }, { key: 'value2' }] }, + }, + ]; + mockReadConfigFile.mockImplementation(readConfigFileMockImplementation(files)); + const res = await buildRefs({ context }); + expect(res).toEqual({ + templated: { list: [{ key: 'value1' }, { key: 'value2' }] }, + }); }); }); -test('buildRefs pass vars two levels', async () => { - const files = [ - { - path: 'lowdefy.yaml', - content: ` +describe('vars', () => { + test('buildRefs var specified by name', async () => { + const files = [ + { + path: 'lowdefy.yaml', + content: ` + ref: + _ref: + path: file.yaml + vars: + var1: value`, + }, + { + path: 'file.yaml', + content: ` + key: + _var: + name: var1`, + }, + ]; + mockReadConfigFile.mockImplementation(readConfigFileMockImplementation(files)); + const res = await buildRefs({ context }); + expect(res).toEqual({ + ref: { + key: 'value', + }, + }); + }); + + test('buildRefs var with default value, but value specified', async () => { + const files = [ + { + path: 'lowdefy.yaml', + content: ` + ref: + _ref: + path: file.yaml + vars: + var1: value`, + }, + { + path: 'file.yaml', + content: ` + key: + _var: + name: var1 + default: default`, + }, + ]; + mockReadConfigFile.mockImplementation(readConfigFileMockImplementation(files)); + const res = await buildRefs({ context }); + expect(res).toEqual({ + ref: { + key: 'value', + }, + }); + }); + + test('buildRefs var uses default value if value not specified', async () => { + const files = [ + { + path: 'lowdefy.yaml', + content: ` + ref: + _ref: + path: file.yaml + vars: + var2: value`, + }, + { + path: 'file.yaml', + content: ` + key: + _var: + name: var1 + default: default`, + }, + ]; + mockReadConfigFile.mockImplementation(readConfigFileMockImplementation(files)); + const res = await buildRefs({ context }); + expect(res).toEqual({ + ref: { + key: 'default', + }, + }); + }); + + test('buildRefs _var receives invalid type', async () => { + const files = [ + { + path: 'lowdefy.yaml', + content: ` + ref: + _ref: + path: file.yaml + vars: + var1: value`, + }, + { + path: 'file.yaml', + content: ` + key: + _var: [1]`, + }, + ]; + mockReadConfigFile.mockImplementation(readConfigFileMockImplementation(files)); + await expect(buildRefs({ context })).rejects.toThrow( + '"_var" operator takes a string or object with name field as arguments. Received "{"_var":[1]}"' + ); + }); + + test('buildRefs should copy vars', async () => { + const files = [ + { + path: 'lowdefy.yaml', + content: ` + ref: + _ref: + path: file.yaml + vars: + var1: + key: value`, + }, + { + path: 'file.yaml', + content: ` + ref1: + _var: var1 + ref2: + _var: var1`, + }, + ]; + mockReadConfigFile.mockImplementation(readConfigFileMockImplementation(files)); + const res = await buildRefs({ context }); + expect(res).toEqual({ + ref: { + ref1: { key: 'value' }, + ref2: { key: 'value' }, + }, + }); + res.ref.ref1.key = 'newValue'; + expect(res).toEqual({ + ref: { + ref1: { key: 'newValue' }, + ref2: { key: 'value' }, + }, + }); + }); +}); + +describe('Handle nested refs/vars', () => { + test('buildRefs pass vars two levels', async () => { + const files = [ + { + path: 'lowdefy.yaml', + content: ` ref1: _ref: path: file1.yaml vars: var1: Hello`, - }, - { - path: 'file1.yaml', - content: ` + }, + { + path: 'file1.yaml', + content: ` ref2: _ref: path: file2.yaml vars: var2: _var: var1`, - }, - { - path: 'file2.yaml', - content: ` + }, + { + path: 'file2.yaml', + content: ` value: _var: var2`, - }, - ]; - mockReadConfigFile.mockImplementation(readConfigFileMockImplementation(files)); - const res = await buildRefs({ context }); - expect(res).toEqual({ - ref1: { - ref2: { - value: 'Hello', }, - }, + ]; + mockReadConfigFile.mockImplementation(readConfigFileMockImplementation(files)); + const res = await buildRefs({ context }); + expect(res).toEqual({ + ref1: { + ref2: { + value: 'Hello', + }, + }, + }); }); -}); -test('buildRefs use a ref in a var', async () => { - const files = [ - { - path: 'lowdefy.yaml', - content: ` + test('buildRefs use a ref in a var', async () => { + const files = [ + { + path: 'lowdefy.yaml', + content: ` ref1: _ref: path: file1.yaml vars: file2: _ref: file2.md`, - }, - { - path: 'file1.yaml', - content: ` + }, + { + path: 'file1.yaml', + content: ` content: _var: file2`, - }, - { - path: 'file2.md', - content: 'Hello', - }, - ]; - mockReadConfigFile.mockImplementation(readConfigFileMockImplementation(files)); - const res = await buildRefs({ context }); - expect(res).toEqual({ - ref1: { - content: 'Hello', - }, + }, + { + path: 'file2.md', + content: 'Hello', + }, + ]; + mockReadConfigFile.mockImplementation(readConfigFileMockImplementation(files)); + const res = await buildRefs({ context }); + expect(res).toEqual({ + ref1: { + content: 'Hello', + }, + }); }); -}); -test('buildRefs use a ref in var, with a var from parent as a var', async () => { - const files = [ - { - path: 'lowdefy.yaml', - content: ` + test('buildRefs use a ref in var, with a var from parent as a var', async () => { + const files = [ + { + path: 'lowdefy.yaml', + content: ` ref1: _ref: path: file1.yaml vars: parent1: 1 parent2: 2`, - }, - { - path: 'file1.yaml', - content: ` + }, + { + path: 'file1.yaml', + content: ` ref2: _ref: path: file2.yaml @@ -465,229 +580,352 @@ ref2: _var: parent2 const: 3 `, - }, - { - path: 'file2.yaml', - content: ` + }, + { + path: 'file2.yaml', + content: ` value: _var: var ref: _var: ref`, - }, - { - path: 'file3.yaml', - content: ` + }, + { + path: 'file3.yaml', + content: ` value: _var: var const: _var: const`, - }, - ]; - mockReadConfigFile.mockImplementation(readConfigFileMockImplementation(files)); - const res = await buildRefs({ context }); - expect(res).toEqual({ - ref1: { - ref2: { - value: 1, - ref: { - value: 2, - const: 3, + }, + ]; + mockReadConfigFile.mockImplementation(readConfigFileMockImplementation(files)); + const res = await buildRefs({ context }); + expect(res).toEqual({ + ref1: { + ref2: { + value: 1, + ref: { + value: 2, + const: 3, + }, }, }, - }, + }); }); -}); -test('buildRefs var specified by name', async () => { - const files = [ - { - path: 'lowdefy.yaml', - content: ` -ref: + test('buildRefs _ref path is a var, shorthand path', async () => { + const files = [ + { + path: 'lowdefy.yaml', + content: ` _ref: - path: file.yaml + path: file1.yaml vars: - var1: value`, - }, - { - path: 'file.yaml', - content: ` -key: - _var: - name: var1`, - }, - ]; - mockReadConfigFile.mockImplementation(readConfigFileMockImplementation(files)); - const res = await buildRefs({ context }); - expect(res).toEqual({ - ref: { - key: 'value', - }, + filePath: file2.md`, + }, + { + path: 'file1.yaml', + content: ` + text: + _ref: + _var: filePath`, + }, + { + path: 'file2.md', + content: 'Hello', + }, + ]; + mockReadConfigFile.mockImplementation(readConfigFileMockImplementation(files)); + const res = await buildRefs({ context }); + expect(res).toEqual({ + text: 'Hello', + }); }); -}); -test('buildRefs var with default value, but value specified', async () => { - const files = [ - { - path: 'lowdefy.yaml', - content: ` -ref: + test('buildRefs _ref path is a var', async () => { + const files = [ + { + path: 'lowdefy.yaml', + content: ` _ref: - path: file.yaml + path: file1.yaml vars: - var1: value`, - }, - { - path: 'file.yaml', - content: ` -key: - _var: - name: var1 - default: default`, - }, - ]; - mockReadConfigFile.mockImplementation(readConfigFileMockImplementation(files)); - const res = await buildRefs({ context }); - expect(res).toEqual({ - ref: { - key: 'value', - }, + filePath: file2.md`, + }, + { + path: 'file1.yaml', + content: ` + text: + _ref: + path: + _var: filePath`, + }, + { + path: 'file2.md', + content: 'Hello', + }, + ]; + mockReadConfigFile.mockImplementation(readConfigFileMockImplementation(files)); + const res = await buildRefs({ context }); + expect(res).toEqual({ + text: 'Hello', + }); }); }); -test('buildRefs var uses default value if value not specified', async () => { - const files = [ - { - path: 'lowdefy.yaml', - content: ` -ref: +describe('transformer functions', () => { + test('buildRefs with transformer function', async () => { + const files = [ + { + path: 'lowdefy.yaml', + content: ` _ref: - path: file.yaml + path: target.yaml + transformer: src/test/buildRefs/testBuildRefsTransform.js vars: - var2: value`, - }, - { - path: 'file.yaml', - content: ` -key: - _var: - name: var1 - default: default`, - }, - ]; - mockReadConfigFile.mockImplementation(readConfigFileMockImplementation(files)); - const res = await buildRefs({ context }); - expect(res).toEqual({ - ref: { - key: 'default', - }, + var1: var1`, + }, + { + path: 'target.yaml', + content: 'a: 1', + }, + ]; + mockReadConfigFile.mockImplementation(readConfigFileMockImplementation(files)); + const res = await buildRefs({ context }); + expect(res).toEqual({ + add: 43, + json: '{"a":1}', + var: 'var1', + }); }); -}); -test('buildRefs with transformer function', async () => { - const files = [ - { - path: 'lowdefy.yaml', - content: ` + test('buildRefs with async transformer function', async () => { + const files = [ + { + path: 'lowdefy.yaml', + content: ` _ref: path: target.yaml - transformer: src/test/testBuildRefsTransform.js - vars: - var1: var1`, - }, - { - path: 'target.yaml', - content: 'a: 1', - }, - ]; - mockReadConfigFile.mockImplementation(readConfigFileMockImplementation(files)); - const res = await buildRefs({ context }); - expect(res).toEqual({ - add: 43, - json: '{"a":1}', - var: 'var1', + transformer: src/test/buildRefs/testBuildRefsAsyncFunction.js`, + }, + { + path: 'target.yaml', + content: 'a: 1', + }, + ]; + mockReadConfigFile.mockImplementation(readConfigFileMockImplementation(files)); + const res = await buildRefs({ context }); + expect(res).toEqual({ async: true }); }); }); -test('buildRefs _var receives invalid type', async () => { - const files = [ - { - path: 'lowdefy.yaml', - content: ` -ref: - _ref: - path: file.yaml - vars: - var1: value`, - }, - { - path: 'file.yaml', - content: ` -key: - _var: [1]`, - }, - ]; - mockReadConfigFile.mockImplementation(readConfigFileMockImplementation(files)); - await expect(buildRefs({ context })).rejects.toThrow( - '"_var" operator takes a string or object with name field as arguments. Received "{"_var":[1]}"' - ); -}); - -test('buildRefs _ref path is a var, shorthand path', async () => { - const files = [ - { - path: 'lowdefy.yaml', - content: ` +describe('resolver functions', () => { + test('buildRefs with resolver function, no path or vars', async () => { + const files = [ + { + path: 'lowdefy.yaml', + content: ` _ref: - path: file1.yaml - vars: - filePath: file2.md`, - }, - { - path: 'file1.yaml', - content: ` -text: - _ref: - _var: filePath`, - }, - { - path: 'file2.md', - content: 'Hello', - }, - ]; - mockReadConfigFile.mockImplementation(readConfigFileMockImplementation(files)); - const res = await buildRefs({ context }); - expect(res).toEqual({ - text: 'Hello', + resolver: src/test/buildRefs/testBuildRefsResolver.js`, + }, + ]; + mockReadConfigFile.mockImplementation(readConfigFileMockImplementation(files)); + const res = await buildRefs({ context }); + expect(mockReadConfigFile.mock.calls).toEqual([['lowdefy.yaml']]); + // Return context gets JSON stringified and parsed, so functions are stripped + expect(res).toEqual({ + resolved: true, + path: null, + vars: {}, + context: { configDirectory: '', logger: {} }, + }); }); -}); -test('buildRefs _ref path is a var', async () => { - const files = [ - { - path: 'lowdefy.yaml', - content: ` + test('buildRefs with resolver function, path and vars given', async () => { + const files = [ + { + path: 'lowdefy.yaml', + content: ` _ref: - path: file1.yaml + resolver: src/test/buildRefs/testBuildRefsResolver.js + path: target vars: - filePath: file2.md`, - }, - { - path: 'file1.yaml', - content: ` -text: - _ref: - path: - _var: filePath`, - }, - { - path: 'file2.md', - content: 'Hello', - }, - ]; - mockReadConfigFile.mockImplementation(readConfigFileMockImplementation(files)); - const res = await buildRefs({ context }); - expect(res).toEqual({ - text: 'Hello', + var: var1`, + }, + ]; + mockReadConfigFile.mockImplementation(readConfigFileMockImplementation(files)); + const res = await buildRefs({ context }); + expect(mockReadConfigFile.mock.calls).toEqual([['lowdefy.yaml']]); + expect(res).toEqual({ + resolved: true, + path: 'target', + vars: { + var: 'var1', + }, + // Return context gets JSON stringified and parsed, so functions are stripped + context: { configDirectory: '', logger: {} }, + }); + }); + + test('buildRefs with async resolver function', async () => { + const files = [ + { + path: 'lowdefy.yaml', + content: ` +_ref: + resolver: src/test/buildRefs/testBuildRefsAsyncFunction.js`, + }, + ]; + mockReadConfigFile.mockImplementation(readConfigFileMockImplementation(files)); + const res = await buildRefs({ context }); + expect(res).toEqual({ async: true }); + }); + + test('buildRefs with resolver function, returned yaml content is parsed', async () => { + let files = [ + { + path: 'lowdefy.yaml', + content: ` +_ref: + resolver: src/test/buildRefs/testBuildRefsParsingResolver.js + path: target.yaml + vars: + var: var1`, + }, + ]; + mockReadConfigFile.mockImplementation(readConfigFileMockImplementation(files)); + const res = await buildRefs({ context }); + expect(res).toEqual({ array: [1, 2] }); + }); + + test('buildRefs with resolver function, returned yml content is parsed', async () => { + let files = [ + { + path: 'lowdefy.yaml', + content: ` +_ref: + resolver: src/test/buildRefs/testBuildRefsParsingResolver.js + path: target.yml + vars: + var: var1`, + }, + ]; + mockReadConfigFile.mockImplementation(readConfigFileMockImplementation(files)); + const res = await buildRefs({ context }); + expect(res).toEqual({ array: [1, 2] }); + }); + + test('buildRefs with resolver function, returned json content is parsed', async () => { + let files = [ + { + path: 'lowdefy.yaml', + content: ` +_ref: + resolver: src/test/buildRefs/testBuildRefsParsingResolver.js + path: target.json + vars: + var: var1`, + }, + ]; + mockReadConfigFile.mockImplementation(readConfigFileMockImplementation(files)); + const res = await buildRefs({ context }); + expect(res).toEqual({ a: 42 }); + }); + + test('buildRefs with resolver function, returned yaml.njk content is parsed', async () => { + let files = [ + { + path: 'lowdefy.yaml', + content: ` +_ref: + resolver: src/test/buildRefs/testBuildRefsParsingResolver.js + path: target.yaml.njk + vars: + var: var1`, + }, + ]; + mockReadConfigFile.mockImplementation(readConfigFileMockImplementation(files)); + const res = await buildRefs({ context }); + expect(res).toEqual({ a: 'var1' }); + }); + + test('buildRefs with resolver function, returned yml.njk content is parsed', async () => { + let files = [ + { + path: 'lowdefy.yaml', + content: ` +_ref: + resolver: src/test/buildRefs/testBuildRefsParsingResolver.js + path: target.yaml.njk + vars: + var: var1`, + }, + ]; + mockReadConfigFile.mockImplementation(readConfigFileMockImplementation(files)); + const res = await buildRefs({ context }); + expect(res).toEqual({ a: 'var1' }); + }); + + test('buildRefs with resolver function, returned json.njk content is parsed', async () => { + let files = [ + { + path: 'lowdefy.yaml', + content: ` +_ref: + resolver: src/test/buildRefs/testBuildRefsParsingResolver.js + path: target.json.njk + vars: + var: var1`, + }, + ]; + mockReadConfigFile.mockImplementation(readConfigFileMockImplementation(files)); + const res = await buildRefs({ context }); + expect(res).toEqual({ a: 'var1' }); + }); + + test('buildRefs with resolver function, resolver throws error', async () => { + let files = [ + { + path: 'lowdefy.yaml', + content: ` +_ref: + resolver: src/test/buildRefs/testBuildRefsErrorResolver.js`, + }, + ]; + mockReadConfigFile.mockImplementation(readConfigFileMockImplementation(files)); + await expect(buildRefs({ context })).rejects.toThrow( + 'Error calling resolver "src/test/buildRefs/testBuildRefsErrorResolver.js" from "lowdefy.yaml": Test error' + ); + }); + + test('buildRefs with resolver function, resolver returns null', async () => { + let files = [ + { + path: 'lowdefy.yaml', + content: ` +_ref: + resolver: src/test/buildRefs/testBuildRefsNullResolver.js + path: "null"`, + }, + ]; + mockReadConfigFile.mockImplementation(readConfigFileMockImplementation(files)); + await expect(buildRefs({ context })).rejects.toThrow( + 'Tried to reference with resolver "src/test/buildRefs/testBuildRefsNullResolver.js" from "lowdefy.yaml", but received "null".' + ); + }); + + test('buildRefs with resolver function, resolver returns undefined', async () => { + let files = [ + { + path: 'lowdefy.yaml', + content: ` +_ref: + resolver: src/test/buildRefs/testBuildRefsNullResolver.js`, + }, + ]; + mockReadConfigFile.mockImplementation(readConfigFileMockImplementation(files)); + await expect(buildRefs({ context })).rejects.toThrow( + 'Tried to reference with resolver "src/test/buildRefs/testBuildRefsNullResolver.js" from "lowdefy.yaml", but received "undefined".' + ); }); }); diff --git a/packages/build/src/build/buildRefs/getConfigFile.js b/packages/build/src/build/buildRefs/getConfigFile.js new file mode 100644 index 000000000..1fd9ff35d --- /dev/null +++ b/packages/build/src/build/buildRefs/getConfigFile.js @@ -0,0 +1,39 @@ +/* + Copyright 2020-2021 Lowdefy, Inc + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +import { type } from '@lowdefy/helpers'; + +async function getConfigFile({ context, refDef, referencedFrom }) { + if (!type.isString(refDef.path)) { + throw new Error( + `Invalid _ref definition ${JSON.stringify({ + _ref: refDef.original, + })} in file ${referencedFrom}` + ); + } + + const content = context.readConfigFile(refDef.path); + + if (content === null) { + throw new Error( + `Tried to reference file "${refDef.path}" from "${referencedFrom}", but file does not exist.` + ); + } + + return content; +} + +export default getConfigFile; diff --git a/packages/build/src/build/buildRefs/getRefContent.js b/packages/build/src/build/buildRefs/getRefContent.js index 8bbce6835..918c1e8f5 100644 --- a/packages/build/src/build/buildRefs/getRefContent.js +++ b/packages/build/src/build/buildRefs/getRefContent.js @@ -14,47 +14,19 @@ limitations under the License. */ -import { type } from '@lowdefy/helpers'; -import { getFileExtension, getFileSubExtension } from '@lowdefy/node-utils'; -import JSON5 from 'json5'; -import YAML from 'js-yaml'; - -import parseNunjucks from './parseNunjucks'; - -function parseRefContent({ content, vars, path }) { - let ext = getFileExtension(path); - if (ext === 'njk') { - content = parseNunjucks(content, vars, path); - ext = getFileSubExtension(path); - } - - if (ext === 'yaml' || ext === 'yml') { - return YAML.load(content); - } - if (ext === 'json') { - return JSON5.parse(content); - } - return content; -} +import getConfigFile from './getConfigFile'; +import parseRefContent from './parseRefContent'; +import runRefResolver from './runRefResolver'; async function getRefContent({ context, refDef, referencedFrom }) { - if (!type.isString(refDef.path)) { - throw new Error( - `Invalid _ref definition ${JSON.stringify({ - _ref: refDef.original, - })} in file ${referencedFrom}` - ); - } - - const { path, vars } = refDef; let content; - content = await context.readConfigFile(path); - - if (content === null) { - throw new Error(`Tried to reference file with path "${path}", but file does not exist.`); + if (refDef.resolver) { + content = await runRefResolver({ context, refDef, referencedFrom }); + } else { + content = await getConfigFile({ context, refDef, referencedFrom }); } - return parseRefContent({ content, vars, path }); + return parseRefContent({ content, refDef }); } export default getRefContent; diff --git a/packages/build/src/build/buildRefs/makeRefDefinition.js b/packages/build/src/build/buildRefs/makeRefDefinition.js index 91293caec..d14e8feee 100644 --- a/packages/build/src/build/buildRefs/makeRefDefinition.js +++ b/packages/build/src/build/buildRefs/makeRefDefinition.js @@ -22,10 +22,11 @@ import getRefPath from './getRefPath'; function makeRefDefinition(refDefinition) { return { id: uuid(), - path: getRefPath(refDefinition), - vars: get(refDefinition, 'vars', { default: {} }), - transformer: get(refDefinition, 'transformer'), original: refDefinition, + path: getRefPath(refDefinition), + resolver: get(refDefinition, 'resolver'), + transformer: get(refDefinition, 'transformer'), + vars: get(refDefinition, 'vars', { default: {} }), }; } diff --git a/packages/build/src/build/buildRefs/parseRefContent.js b/packages/build/src/build/buildRefs/parseRefContent.js new file mode 100644 index 000000000..600858160 --- /dev/null +++ b/packages/build/src/build/buildRefs/parseRefContent.js @@ -0,0 +1,43 @@ +/* + Copyright 2020-2021 Lowdefy, Inc + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +import { type } from '@lowdefy/helpers'; +import { getFileExtension, getFileSubExtension } from '@lowdefy/node-utils'; +import JSON5 from 'json5'; +import YAML from 'js-yaml'; + +import parseNunjucks from './parseNunjucks'; + +function parseRefContent({ content, refDef }) { + const { path, vars } = refDef; + if (type.isString(path)) { + let ext = getFileExtension(path); + if (ext === 'njk') { + content = parseNunjucks(content, vars); + ext = getFileSubExtension(path); + } + + if (ext === 'yaml' || ext === 'yml') { + return YAML.load(content); + } + if (ext === 'json') { + return JSON5.parse(content); + } + } + return content; +} + +export default parseRefContent; diff --git a/packages/build/src/build/buildRefs/recursiveBuild.js b/packages/build/src/build/buildRefs/recursiveBuild.js index 0c6ed8b58..e309de179 100644 --- a/packages/build/src/build/buildRefs/recursiveBuild.js +++ b/packages/build/src/build/buildRefs/recursiveBuild.js @@ -21,7 +21,7 @@ import runTransformer from './runTransformer'; async function recursiveParseFile({ context, refDef, count, referencedFrom }) { // TODO: Maybe it would be better to detect a cycle, since this is the real issue here? - if (count > 20) { + if (count > 40) { throw new Error(`Maximum recursion depth of references exceeded.`); } let fileContent = await getRefContent({ context, refDef, referencedFrom }); diff --git a/packages/build/src/build/buildRefs/runRefResolver.js b/packages/build/src/build/buildRefs/runRefResolver.js new file mode 100644 index 000000000..7713f4861 --- /dev/null +++ b/packages/build/src/build/buildRefs/runRefResolver.js @@ -0,0 +1,41 @@ +/* + Copyright 2020-2021 Lowdefy, Inc + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +import { type } from '@lowdefy/helpers'; +import getUserJavascriptFunction from './getUserJavascriptFunction'; + +async function runRefResolver({ context, refDef, referencedFrom }) { + const resolverFn = await getUserJavascriptFunction({ + context, + filePath: refDef.resolver, + }); + let content; + try { + content = await resolverFn(refDef.path, refDef.vars, context); + } catch (error) { + throw new Error( + `Error calling resolver "${refDef.resolver}" from "${referencedFrom}": ${error.message}` + ); + } + if (type.isNone(content)) { + throw new Error( + `Tried to reference with resolver "${refDef.resolver}" from "${referencedFrom}", but received "${content}".` + ); + } + return content; +} + +export default runRefResolver; diff --git a/packages/build/src/test/buildRefs/testBuildRefsAsyncFunction.js b/packages/build/src/test/buildRefs/testBuildRefsAsyncFunction.js new file mode 100644 index 000000000..51f0e973b --- /dev/null +++ b/packages/build/src/test/buildRefs/testBuildRefsAsyncFunction.js @@ -0,0 +1,26 @@ +/* + Copyright 2020-2021 Lowdefy, Inc + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +const wait = (ms) => { + return new Promise((resolve) => setTimeout(resolve, ms)); +}; + +async function asyncFn() { + await wait(20); + return { async: true }; +} + +module.exports = asyncFn; diff --git a/packages/build/src/test/buildRefs/testBuildRefsErrorResolver.js b/packages/build/src/test/buildRefs/testBuildRefsErrorResolver.js new file mode 100644 index 000000000..ea4756cc1 --- /dev/null +++ b/packages/build/src/test/buildRefs/testBuildRefsErrorResolver.js @@ -0,0 +1,21 @@ +/* + Copyright 2020-2021 Lowdefy, Inc + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +function resolver() { + throw new Error('Test error'); +} + +module.exports = resolver; diff --git a/packages/build/src/test/buildRefs/testBuildRefsNullResolver.js b/packages/build/src/test/buildRefs/testBuildRefsNullResolver.js new file mode 100644 index 000000000..8febf94fa --- /dev/null +++ b/packages/build/src/test/buildRefs/testBuildRefsNullResolver.js @@ -0,0 +1,22 @@ +/* + Copyright 2020-2021 Lowdefy, Inc + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +function resolver(path) { + if (path === 'null') return null; + return undefined; +} + +module.exports = resolver; diff --git a/packages/build/src/test/buildRefs/testBuildRefsParsingResolver.js b/packages/build/src/test/buildRefs/testBuildRefsParsingResolver.js new file mode 100644 index 000000000..b22db23ed --- /dev/null +++ b/packages/build/src/test/buildRefs/testBuildRefsParsingResolver.js @@ -0,0 +1,42 @@ +/* + Copyright 2020-2021 Lowdefy, Inc + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +function resolver(path) { + switch (path) { + case 'target.yaml': + return ` +array: + - 1 + - 2`; + case 'target.yml': + return ` +array: +- 1 +- 2`; + case 'target.json': + return `{"a": 42}`; + case 'target.yaml.njk': + return 'a: {{ var }}'; + case 'target.yml.njk': + return 'a: {{ var }}'; + case 'target.json.njk': + return `{ "a": "{{ var }}" }`; + default: + return null; + } +} + +module.exports = resolver; diff --git a/packages/build/src/test/buildRefs/testBuildRefsResolver.js b/packages/build/src/test/buildRefs/testBuildRefsResolver.js new file mode 100644 index 000000000..21da40349 --- /dev/null +++ b/packages/build/src/test/buildRefs/testBuildRefsResolver.js @@ -0,0 +1,26 @@ +/* + Copyright 2020-2021 Lowdefy, Inc + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +function resolver(path, vars, context) { + return { + resolved: true, + path, + vars, + context, + }; +} + +module.exports = resolver; diff --git a/packages/build/src/test/buildRefs/testBuildRefsTransform.js b/packages/build/src/test/buildRefs/testBuildRefsTransform.js new file mode 100644 index 000000000..afda25581 --- /dev/null +++ b/packages/build/src/test/buildRefs/testBuildRefsTransform.js @@ -0,0 +1,29 @@ +/* + Copyright 2020-2021 Lowdefy, Inc + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +function add(a, b) { + return a + b; +} + +function transformer(obj, vars) { + return { + json: JSON.stringify(obj), + add: add(obj.a, 42), + var: vars.var1, + }; +} + +module.exports = transformer; diff --git a/packages/build/src/test/testBuildRefsTransform.js b/packages/build/src/test/testBuildRefsTransform.js deleted file mode 100644 index 2e0db9c13..000000000 --- a/packages/build/src/test/testBuildRefsTransform.js +++ /dev/null @@ -1,13 +0,0 @@ -function add(a, b) { - return a + b; -} - -function transformer(obj, vars) { - return { - json: JSON.stringify(obj), - add: add(obj.a, 42), - var: vars.var1, - }; -} - -module.exports = transformer;