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. diff --git a/__tests__/mapper.test.ts b/__tests__/mapper.test.ts index 693f4d7..e9edb02 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,49 @@ 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'] + ]) + ) + }) + }) + + 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/action.yml b/action.yml index 1315f24..0c8fc77 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: 'Specify the behavior of getting the variable. 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 56cc952..0764d2d 100644 --- a/dist/index.js +++ b/dist/index.js @@ -404,6 +404,55 @@ 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) { + for (const param of pairs) { + const ok = param.match(key); + if (ok) { + return param; + } + } + } +} +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 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) { @@ -413,12 +462,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 = { @@ -429,8 +473,21 @@ Mapper.schema = { } }; class JSONMapper extends Mapper { - constructor(rawJSON) { + constructor(rawJSON, mode) { super(); + switch (mode) { + case 'first_match': + this.matcher = new FirstMatch(); + break; + case 'overwrite': + this.matcher = new Overwrite(); + break; + case 'fill': + this.matcher = new Fill(); + break; + default: + throw new Error(`Unexpected mode: ${mode}`); + } const parsed = JSON.parse(rawJSON); this.validate(parsed); const tmpPairs = new Array(); @@ -656,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); + 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 f6ae776..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) + 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 673b765..2f8bd12 100644 --- a/src/mapper.ts +++ b/src/mapper.ts @@ -21,6 +21,63 @@ 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 { + 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 + } + } + } +} + +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 + } +} + +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 { @@ -37,23 +94,34 @@ 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) { + constructor(rawJSON: string, mode: string) { super() + + switch (mode) { + case 'first_match': + this.matcher = new FirstMatch() + break + case 'overwrite': + this.matcher = new Overwrite() + break + case 'fill': + this.matcher = new Fill() + break + default: + throw new Error(`Unexpected mode: ${mode}`) + } + const parsed = JSON.parse(rawJSON) this.validate(parsed as object)