From 66b5c12bf017893713260dec0af7dc554c00f080 Mon Sep 17 00:00:00 2001 From: kanga333 Date: Wed, 11 Nov 2020 20:39:57 +0900 Subject: [PATCH 1/6] Introduce Matcher interface Allow mapper to switch between different match strategies. --- dist/index.js | 18 ++++++++++++------ src/mapper.ts | 28 +++++++++++++++++++++------- 2 files changed, 33 insertions(+), 13 deletions(-) diff --git a/dist/index.js b/dist/index.js index 56cc952..208546b 100644 --- a/dist/index.js +++ b/dist/index.js @@ -405,6 +405,16 @@ class KeyVariablesPair { } } } +class FirstMatch { + match(key, pairs) { + for (const param of pairs) { + const ok = param.match(key); + if (ok) { + return param; + } + } + } +} class Mapper { validate(input) { const ajv = new ajv_1.default(); @@ -413,12 +423,7 @@ class Mapper { throw new Error(`Validation failed: ${ajv.errorsText()}`); } match(key) { - for (const param of this.pairs) { - const ok = param.match(key); - if (ok) { - return param; - } - } + return this.matcher.match(key, this.pairs); } } Mapper.schema = { @@ -431,6 +436,7 @@ Mapper.schema = { class JSONMapper extends Mapper { constructor(rawJSON) { super(); + this.matcher = new FirstMatch(); const parsed = JSON.parse(rawJSON); this.validate(parsed); const tmpPairs = new Array(); diff --git a/src/mapper.ts b/src/mapper.ts index 673b765..b3cd6df 100644 --- a/src/mapper.ts +++ b/src/mapper.ts @@ -23,6 +23,21 @@ class KeyVariablesPair { } } +interface Matcher { + match(key: string, pairs: KeyVariablesPair[]): KeyVariablesPair | undefined +} + +class FirstMatch implements Matcher { + match(key: string, pairs: KeyVariablesPair[]): KeyVariablesPair | undefined { + for (const param of pairs) { + const ok = param.match(key) + if (ok) { + return param + } + } + } +} + abstract class Mapper { static schema = { type: 'object', @@ -37,23 +52,22 @@ abstract class Mapper { const valid = ajv.validate(Mapper.schema, input) if (!valid) throw new Error(`Validation failed: ${ajv.errorsText()}`) } - + abstract matcher: Matcher abstract pairs: KeyVariablesPair[] match(key: string): KeyVariablesPair | undefined { - for (const param of this.pairs) { - const ok = param.match(key) - if (ok) { - return param - } - } + return this.matcher.match(key, this.pairs) } } export class JSONMapper extends Mapper { pairs: KeyVariablesPair[] + matcher: Matcher constructor(rawJSON: string) { super() + + this.matcher = new FirstMatch() + const parsed = JSON.parse(rawJSON) this.validate(parsed as object) From 75b2669df459909ff109708f1668491119a3e123 Mon Sep 17 00:00:00 2001 From: kanga333 Date: Fri, 13 Nov 2020 23:58:27 +0900 Subject: [PATCH 2/6] Implement Overwrite Matcher --- __tests__/mapper.test.ts | 26 ++++++++++++++++++++++++-- dist/index.js | 36 +++++++++++++++++++++++++++++++++--- src/main.ts | 2 +- src/mapper.ts | 36 ++++++++++++++++++++++++++++++++++-- 4 files changed, 92 insertions(+), 8 deletions(-) diff --git a/__tests__/mapper.test.ts b/__tests__/mapper.test.ts index 693f4d7..e982eed 100644 --- a/__tests__/mapper.test.ts +++ b/__tests__/mapper.test.ts @@ -2,7 +2,8 @@ import {JSONMapper} from '../src/mapper' describe('JSONMapper', () => { const mapper = new JSONMapper( - '{"k.y":{"env1":"value1"},".*":{"env2":"value2"}}' + '{"k.y":{"env1":"value1"},".*":{"env2":"value2"}}', + 'first_match' ) it('JSONMapper holds the order of keys', () => { @@ -23,7 +24,28 @@ describe('JSONMapper', () => { it('JSONMapper should throw an exception on invalid input', () => { expect(() => { - new JSONMapper('{"invalid":"schema"}') + new JSONMapper('{"invalid":"schema"}', 'first_match') }).toThrow() }) + + describe('Overwrite Matcher', () => { + const overwrite = new JSONMapper( + '{"k.y":{"env1":"value1","env2":"value2"},".*":{"env2":"overwrite"}}', + 'overwrite' + ) + + it('Overwrite Matcher can match and overwrite multiple values', () => { + const got = overwrite.match('key') + if (!got) { + throw new Error('No match') + } + expect(got.key).toBe('k.y\n.*') + expect(got.variables).toMatchObject( + new Map([ + ['env1', 'value1'], + ['env2', 'overwrite'] + ]) + ) + }) + }) }) diff --git a/dist/index.js b/dist/index.js index 208546b..d0e9152 100644 --- a/dist/index.js +++ b/dist/index.js @@ -404,6 +404,13 @@ class KeyVariablesPair { fn(variable[0], variable[1]); } } + merge(kvp) { + this.variables = new Map([ + ...this.variables.entries(), + ...kvp.variables.entries() + ]); + this.key = `${this.key}\n${kvp.key}`; + } } class FirstMatch { match(key, pairs) { @@ -415,6 +422,22 @@ class FirstMatch { } } } +class Overwrite { + match(key, pairs) { + let pair; + for (const param of pairs) { + const ok = param.match(key); + if (ok) { + if (pair === undefined) { + pair = param; + continue; + } + pair.merge(param); + } + } + return pair; + } +} class Mapper { validate(input) { const ajv = new ajv_1.default(); @@ -434,9 +457,16 @@ Mapper.schema = { } }; class JSONMapper extends Mapper { - constructor(rawJSON) { + constructor(rawJSON, mode) { super(); - this.matcher = new FirstMatch(); + switch (mode) { + case 'overwrite': + this.matcher = new Overwrite(); + break; + default: + this.matcher = new FirstMatch(); + break; + } const parsed = JSON.parse(rawJSON); this.validate(parsed); const tmpPairs = new Array(); @@ -662,7 +692,7 @@ function run() { const map = core.getInput('map'); const key = core.getInput('key'); const to = core.getInput('export_to'); - const params = new mapper_1.JSONMapper(map); + const params = new mapper_1.JSONMapper(map, 'first_match'); const matched = params.match(key); if (!matched) { core.info(`No match for the ${key}`); diff --git a/src/main.ts b/src/main.ts index f6ae776..be11bb4 100644 --- a/src/main.ts +++ b/src/main.ts @@ -8,7 +8,7 @@ function run(): void { const key: string = core.getInput('key') const to: string = core.getInput('export_to') - const params = new JSONMapper(map) + const params = new JSONMapper(map, 'first_match') const matched = params.match(key) if (!matched) { core.info(`No match for the ${key}`) diff --git a/src/mapper.ts b/src/mapper.ts index b3cd6df..a31ba08 100644 --- a/src/mapper.ts +++ b/src/mapper.ts @@ -21,6 +21,14 @@ class KeyVariablesPair { fn(variable[0], variable[1]) } } + + merge(kvp: KeyVariablesPair): void { + this.variables = new Map([ + ...this.variables.entries(), + ...kvp.variables.entries() + ]) + this.key = `${this.key}\n${kvp.key}` + } } interface Matcher { @@ -38,6 +46,23 @@ class FirstMatch implements Matcher { } } +class Overwrite implements Matcher { + match(key: string, pairs: KeyVariablesPair[]): KeyVariablesPair | undefined { + let pair: KeyVariablesPair | undefined + for (const param of pairs) { + const ok = param.match(key) + if (ok) { + if (pair === undefined) { + pair = param + continue + } + pair.merge(param) + } + } + return pair + } +} + abstract class Mapper { static schema = { type: 'object', @@ -63,10 +88,17 @@ export class JSONMapper extends Mapper { pairs: KeyVariablesPair[] matcher: Matcher - constructor(rawJSON: string) { + constructor(rawJSON: string, mode: string) { super() - this.matcher = new FirstMatch() + switch (mode) { + case 'overwrite': + this.matcher = new Overwrite() + break + default: + this.matcher = new FirstMatch() + break + } const parsed = JSON.parse(rawJSON) this.validate(parsed as object) From 772a0c9d5f95d985f4ef0faaed9136487b098c2a Mon Sep 17 00:00:00 2001 From: kanga333 Date: Sat, 14 Nov 2020 22:07:37 +0900 Subject: [PATCH 3/6] Implement Fill Matcher --- __tests__/mapper.test.ts | 21 +++++++++++++++++++++ dist/index.js | 19 +++++++++++++++++++ src/mapper.ts | 20 ++++++++++++++++++++ 3 files changed, 60 insertions(+) diff --git a/__tests__/mapper.test.ts b/__tests__/mapper.test.ts index e982eed..e9edb02 100644 --- a/__tests__/mapper.test.ts +++ b/__tests__/mapper.test.ts @@ -48,4 +48,25 @@ describe('JSONMapper', () => { ) }) }) + + describe('Fill Matcher', () => { + const overwrite = new JSONMapper( + '{"k.y":{"env1":"value1"},".*":{"env1":"not_overwrite", "env2":"fill"}}', + 'fill' + ) + + it('Overwrite Matcher can match and overwrite multiple values', () => { + const got = overwrite.match('key') + if (!got) { + throw new Error('No match') + } + expect(got.key).toBe('.*\nk.y') + expect(got.variables).toMatchObject( + new Map([ + ['env1', 'value1'], + ['env2', 'fill'] + ]) + ) + }) + }) }) diff --git a/dist/index.js b/dist/index.js index d0e9152..5ab0f16 100644 --- a/dist/index.js +++ b/dist/index.js @@ -438,6 +438,22 @@ class Overwrite { return pair; } } +class Fill { + match(key, pairs) { + let pair; + for (const param of pairs.reverse()) { + const ok = param.match(key); + if (ok) { + if (pair === undefined) { + pair = param; + continue; + } + pair.merge(param); + } + } + return pair; + } +} class Mapper { validate(input) { const ajv = new ajv_1.default(); @@ -463,6 +479,9 @@ class JSONMapper extends Mapper { case 'overwrite': this.matcher = new Overwrite(); break; + case 'fill': + this.matcher = new Fill(); + break; default: this.matcher = new FirstMatch(); break; diff --git a/src/mapper.ts b/src/mapper.ts index a31ba08..3282e04 100644 --- a/src/mapper.ts +++ b/src/mapper.ts @@ -63,6 +63,23 @@ class Overwrite implements Matcher { } } +class Fill implements Matcher { + match(key: string, pairs: KeyVariablesPair[]): KeyVariablesPair | undefined { + let pair: KeyVariablesPair | undefined + for (const param of pairs.reverse()) { + const ok = param.match(key) + if (ok) { + if (pair === undefined) { + pair = param + continue + } + pair.merge(param) + } + } + return pair + } +} + abstract class Mapper { static schema = { type: 'object', @@ -95,6 +112,9 @@ export class JSONMapper extends Mapper { case 'overwrite': this.matcher = new Overwrite() break + case 'fill': + this.matcher = new Fill() + break default: this.matcher = new FirstMatch() break From 01a6cf2ca020abbc50fcdef3fbcf2d16f6812cf6 Mon Sep 17 00:00:00 2001 From: kanga333 Date: Sat, 14 Nov 2020 22:24:30 +0900 Subject: [PATCH 4/6] Introduce the mode parameter --- action.yml | 3 +++ dist/index.js | 9 ++++++--- src/main.ts | 3 ++- src/mapper.ts | 6 ++++-- 4 files changed, 15 insertions(+), 6 deletions(-) diff --git a/action.yml b/action.yml index 1315f24..27afd38 100644 --- a/action.yml +++ b/action.yml @@ -13,6 +13,9 @@ inputs: export_to: description: 'Comma-separated list of targets to export variables to. log, env and output are valid values.' default: 'log,env' + mode: + description: 'Controls the way key and map are matched. first_match, overwrite and fill are valid values.' + default: 'first_match' runs: using: 'node12' main: 'dist/index.js' diff --git a/dist/index.js b/dist/index.js index 5ab0f16..0764d2d 100644 --- a/dist/index.js +++ b/dist/index.js @@ -476,6 +476,9 @@ class JSONMapper extends Mapper { constructor(rawJSON, mode) { super(); switch (mode) { + case 'first_match': + this.matcher = new FirstMatch(); + break; case 'overwrite': this.matcher = new Overwrite(); break; @@ -483,8 +486,7 @@ class JSONMapper extends Mapper { this.matcher = new Fill(); break; default: - this.matcher = new FirstMatch(); - break; + throw new Error(`Unexpected mode: ${mode}`); } const parsed = JSON.parse(rawJSON); this.validate(parsed); @@ -711,7 +713,8 @@ function run() { const map = core.getInput('map'); const key = core.getInput('key'); const to = core.getInput('export_to'); - const params = new mapper_1.JSONMapper(map, 'first_match'); + const mode = core.getInput('mode'); + const params = new mapper_1.JSONMapper(map, mode); const matched = params.match(key); if (!matched) { core.info(`No match for the ${key}`); diff --git a/src/main.ts b/src/main.ts index be11bb4..6443e1a 100644 --- a/src/main.ts +++ b/src/main.ts @@ -7,8 +7,9 @@ function run(): void { const map: string = core.getInput('map') const key: string = core.getInput('key') const to: string = core.getInput('export_to') + const mode: string = core.getInput('mode') - const params = new JSONMapper(map, 'first_match') + const params = new JSONMapper(map, mode) const matched = params.match(key) if (!matched) { core.info(`No match for the ${key}`) diff --git a/src/mapper.ts b/src/mapper.ts index 3282e04..2f8bd12 100644 --- a/src/mapper.ts +++ b/src/mapper.ts @@ -109,6 +109,9 @@ export class JSONMapper extends Mapper { super() switch (mode) { + case 'first_match': + this.matcher = new FirstMatch() + break case 'overwrite': this.matcher = new Overwrite() break @@ -116,8 +119,7 @@ export class JSONMapper extends Mapper { this.matcher = new Fill() break default: - this.matcher = new FirstMatch() - break + throw new Error(`Unexpected mode: ${mode}`) } const parsed = JSON.parse(rawJSON) From 4ae1352deeac3ffa74dcfaf5fb8d4b78a760b491 Mon Sep 17 00:00:00 2001 From: kanga333 Date: Sun, 15 Nov 2020 20:56:48 +0900 Subject: [PATCH 5/6] Update description --- action.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/action.yml b/action.yml index 27afd38..0c8fc77 100644 --- a/action.yml +++ b/action.yml @@ -14,7 +14,7 @@ inputs: description: 'Comma-separated list of targets to export variables to. log, env and output are valid values.' default: 'log,env' mode: - description: 'Controls the way key and map are matched. first_match, overwrite and fill are valid values.' + description: 'Specify the behavior of getting the variable. first_match, overwrite and fill are valid values.' default: 'first_match' runs: using: 'node12' From f7e9750b22bd2e05f6c50982bcf1a4d33bc8c38d Mon Sep 17 00:00:00 2001 From: kanga333 Date: Sun, 15 Nov 2020 21:22:02 +0900 Subject: [PATCH 6/6] Update README and example test --- .github/workflows/test.yml | 78 +++++++++++++++++++++++++ README.md | 116 +++++++++++++++++++++++++++++++++++++ 2 files changed, 194 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ad54895..2014a71 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -114,3 +114,81 @@ jobs: run: | echo ${{ env.environment }} echo ${{ steps.export.outputs.environment }} + test-readme-example3: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1 + - uses: ./ + id: export + with: + key: "first" + map: | + { + "first": { + "env1": "value1", + "env2": "value2" + }, + ".*": { + "env1": "value1_overwrite", + "env3": "value3" + } + } + export_to: env + mode: first_match + - name: Echo environment and output + run: | + test "${{ env.env1 }}" = "value1" + test "${{ env.env2 }}" = "value2" + test "${{ env.env3 }}" = "" + test-readme-example4: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1 + - uses: ./ + id: export + with: + key: "first" + map: | + { + "first": { + "env1": "value1", + "env2": "value2" + }, + ".*": { + "env1": "value1_overwrite", + "env3": "value3" + } + } + export_to: env + mode: overwrite + - name: Echo environment and output + run: | + test "${{ env.env1 }}" = "value1_overwrite" + test "${{ env.env2 }}" = "value2" + test "${{ env.env3 }}" = "value3" + test-readme-example5: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1 + - uses: ./ + id: export + with: + key: "first" + map: | + { + "first": { + "env1": "value1", + "env2": "value2" + }, + ".*": { + "env1": "value1_overwrite", + "env3": "value3" + } + } + export_to: env + mode: fill + - name: Echo environment and output + run: | + test "${{ env.env1 }}" = "value1" + test "${{ env.env2 }}" = "value2" + test "${{ env.env3 }}" = "value3" diff --git a/README.md b/README.md index 9cba1cf..b1ccc27 100644 --- a/README.md +++ b/README.md @@ -77,3 +77,119 @@ jobs: ``` The variables can be exported to log, env and output. (Default is `log,env`) + +### Switching the behavior of getting the variable + +The `mode` option can be used to change the behavior of getting variables. +`first_match`, `overwrite` and `fill` are valid values. + +#### first_match mode (default) + +`first_match` evaluates the regular expression of a key in order from the top and gets the variable for the first key to be matched. + +```yaml +on: [push] +name: Export variables to output and environment and log +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: kanga333/variable-mapper@master + id: export + with: + key: "first" + map: | + { + "first": { + "env1": "value1", + "env2": "value2" + }, + ".*": { + "env1": "value1_overwrite", + "env3": "value3" + } + } + export_to: env + mode: first_match + - name: Echo environment and output + run: | + echo ${{ env.env1 }} + echo ${{ env.env2 }} + echo ${{ env.env3 }} +``` + +In this workflow, only `env1:value1` and `env2:value2` are exported as env. + +#### overwrite mode + +`overwrite` evaluates the regular expression of the keys in order from the top, and then merges the variables associated with the matched keys in turn. If the same variable is defined, the later evaluated value is overwritten. + +```yaml +on: [push] +name: Export variables to output and environment and log +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: kanga333/variable-mapper@master + id: export + with: + key: "first" + map: | + { + "first": { + "env1": "value1", + "env2": "value2" + }, + ".*": { + "env1": "value1_overwrite", + "env3": "value3" + } + } + export_to: env + mode: overwrite + - name: Echo environment and output + run: | + echo ${{ env.env1 }} + echo ${{ env.env2 }} + echo ${{ env.env3 }} +``` + +In this workflow, `env1:value1_overwrite`, `env2:value2` and `env2:value2` export as env. + +#### fill mode + +`fill` evaluates the regular expression of the keys in order from the top, and then merges the variables associated with the matched keys in turn. If the same variable is defined, later evaluated values are ignored and the first evaluated value takes precedence. + +```yaml +on: [push] +name: Export variables to output and environment and log +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: kanga333/variable-mapper@master + id: export + with: + key: "first" + map: | + { + "first": { + "env1": "value1", + "env2": "value2" + }, + ".*": { + "env1": "value1_overwrite", + "env3": "value3" + } + } + export_to: env + mode: overwrite + - name: Echo environment and output + run: | + echo ${{ env.env1 }} + echo ${{ env.env2 }} + echo ${{ env.env3 }} +``` + +In this workflow, `env1:value1`, `env2:value2` and `env2:value2` export as env.