diff --git a/docs/api/cautious-journey.md b/docs/api/cautious-journey.md index b7a95ff..a8f5418 100644 --- a/docs/api/cautious-journey.md +++ b/docs/api/cautious-journey.md @@ -15,7 +15,7 @@ | Function | Description | | --- | --- | -| [resolveLabels(options)](./cautious-journey.resolvelabels.md) | Resolve the desired set of labels, given a starting set and the flags/states to be applied. | +| [resolveProject(options)](./cautious-journey.resolveproject.md) | | | [syncIssueLabels(options)](./cautious-journey.syncissuelabels.md) | goes through and resolves each issue in the project. if there are changes and no errors, then updates the issue. | | [syncProjectLabels(options)](./cautious-journey.syncprojectlabels.md) | | diff --git a/docs/api/cautious-journey.resolvelabels.md b/docs/api/cautious-journey.resolveproject.md similarity index 58% rename from docs/api/cautious-journey.resolvelabels.md rename to docs/api/cautious-journey.resolveproject.md index 9a0820c..4c5f737 100644 --- a/docs/api/cautious-journey.resolvelabels.md +++ b/docs/api/cautious-journey.resolveproject.md @@ -1,15 +1,13 @@ -[Home](./index.md) > [cautious-journey](./cautious-journey.md) > [resolveLabels](./cautious-journey.resolvelabels.md) +[Home](./index.md) > [cautious-journey](./cautious-journey.md) > [resolveProject](./cautious-journey.resolveproject.md) -## resolveLabels() function - -Resolve the desired set of labels, given a starting set and the flags/states to be applied. +## resolveProject() function Signature: ```typescript -export declare function resolveLabels(options: ResolveInput): ResolveResult; +export declare function resolveProject(options: ResolveInput): ResolveResult; ``` ## Parameters diff --git a/src/index.ts b/src/index.ts index 7d6c919..770f64f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,7 +4,7 @@ export { ChangeSet, FlagLabel, StateLabel, StateValue } from './labels'; export { Remote, RemoteOptions } from './remote'; export { GithubRemote } from './remote/github'; export { GitlabRemote } from './remote/gitlab'; -export { ResolveInput, ResolveResult, resolveLabels } from './resolve'; +export { ResolveInput, ResolveResult, resolveProject } from './resolve'; export { syncIssueLabels, SyncOptions, syncProjectLabels } from './sync'; const STATUS_ERROR = 1; diff --git a/src/main.ts b/src/main.ts index 550e60e..9a3be92 100644 --- a/src/main.ts +++ b/src/main.ts @@ -16,7 +16,7 @@ export { FlagLabel, StateLabel } from './labels'; export { Remote, RemoteOptions } from './remote'; export { GithubRemote } from './remote/github'; export { GitlabRemote } from './remote/gitlab'; -export { resolveLabels } from './resolve'; +export { resolveProject } from './resolve'; export { syncIssueLabels, syncProjectLabels } from './sync'; const ARGS_START = 2; diff --git a/src/resolve.ts b/src/resolve.ts index 1dc05f4..9a419b5 100644 --- a/src/resolve.ts +++ b/src/resolve.ts @@ -1,6 +1,6 @@ import { doesExist } from '@apextoaster/js-utils'; -import { BaseLabel, FlagLabel, getValueName, prioritySort, StateLabel } from './labels'; +import { BaseLabel, FlagLabel, getValueName, prioritySort, StateLabel, StateValue } from './labels'; import { defaultUntil } from './utils'; /** @@ -61,131 +61,142 @@ export interface ResolveResult { * Resolve the desired set of labels, given a starting set and the flags/states to be * applied. */ -/* eslint-disable-next-line sonarjs/cognitive-complexity */ -export function resolveLabels(options: ResolveInput): ResolveResult { - const activeLabels = new Set(options.labels); - const changes: Array = []; - const errors: Array = []; +function resolveBaseLabel(label: BaseLabel, anticipatedResult: ResolveResult, activeLabels: Set) { + if (activeLabels.has(label.name) === false) { + return true; + } + + for (const requiredLabel of label.requires) { + if (!activeLabels.has(requiredLabel.name)) { + if (activeLabels.delete(label.name)) { + anticipatedResult.changes.push({ + cause: requiredLabel.name, + effect: ChangeVerb.REQUIRED, + label: label.name, + }); + } - function checkLabelRules(label: BaseLabel) { - if (activeLabels.has(label.name) === false) { return true; } - - for (const requiredLabel of label.requires) { - if (!activeLabels.has(requiredLabel.name)) { - if (activeLabels.delete(label.name)) { - changes.push({ - cause: requiredLabel.name, - effect: ChangeVerb.REQUIRED, - label: label.name, - }); - } - - return true; - } - } - - for (const addedLabel of label.adds) { - // Set.add does not return a boolean, unlike the other methods - if (!activeLabels.has(addedLabel.name)) { - activeLabels.add(addedLabel.name); - changes.push({ - cause: label.name, - effect: ChangeVerb.CREATED, - label: addedLabel.name, - }); - } - } - - for (const removedLabel of label.removes) { - if (activeLabels.delete(removedLabel.name)) { - changes.push({ - cause: label.name, - effect: ChangeVerb.REMOVED, - label: removedLabel.name, - }); - } - } - - return false; } + for (const addedLabel of label.adds) { + // Set.add does not return a boolean, unlike the other methods + if (!activeLabels.has(addedLabel.name)) { + activeLabels.add(addedLabel.name); + anticipatedResult.changes.push({ + cause: label.name, + effect: ChangeVerb.CREATED, + label: addedLabel.name, + }); + } + } + + for (const removedLabel of label.removes) { + if (activeLabels.delete(removedLabel.name)) { + anticipatedResult.changes.push({ + cause: label.name, + effect: ChangeVerb.REMOVED, + label: removedLabel.name, + }); + } + } + + return false; +} + +function resolveBecomes(label: BaseLabel, anticipatedResult: ResolveResult, activeLabels: Set, value: StateValue): boolean { + for (const become of value.becomes) { + const matches = become.matches.every((l) => activeLabels.has(l.name)); + + if (matches) { + resolveBaseLabel({ + ...label, + adds: become.adds, + removes: [...become.matches, ...become.removes], + requires: [], + }, anticipatedResult, activeLabels); + + if (activeLabels.delete(name)) { + anticipatedResult.changes.push({ + cause: name, + effect: ChangeVerb.REMOVED, + label: name, + }); + } + return true; + } + } + return false; +} + +/** + * Need to ensure that there is only 1 active value for the state + * If no, remove any lower priority active values for the state + * Need to run the normal (add, remove) rules + * Need to run the becomes rules + */ +function resolveState(state: StateLabel, anticipatedResult: ResolveResult, activeLabels: Set) { + let activeValue; + + const sortedValues = prioritySort(state.values); + for (const value of sortedValues) { + const name = getValueName(state, value); + if (!activeLabels.has(name)) { + continue; + } + + if (doesExist(activeValue)) { // there is already an active value + if (activeLabels.delete(name)) { + anticipatedResult.changes.push({ + cause: name, + effect: ChangeVerb.CONFLICTED, + label: name, + }); + } + + continue; + } + + const combinedValue: BaseLabel = { + adds: [...state.adds, ...value.adds], + name, + priority: defaultUntil(value.priority, state.priority, 0), + removes: [...state.removes, ...value.removes], + requires: [...state.requires, ...value.requires], + }; + + if (resolveBaseLabel(combinedValue, anticipatedResult, activeLabels)) { + continue; + } + + if (resolveBecomes(combinedValue, anticipatedResult, activeLabels, value)) { + continue; + } + + activeValue = name; + } +} + +export function resolveProject(options: ResolveInput): ResolveResult { + const result: ResolveResult = { + changes: [], + errors: [], + labels: [], + }; + const activeLabels = new Set(options.labels); + const sortedFlags = prioritySort(options.flags); for (const flag of sortedFlags) { - checkLabelRules(flag); + resolveBaseLabel(flag, result, activeLabels); } const sortedStates = prioritySort(options.states); for (const state of sortedStates) { - let activeValue; - - const sortedValues = prioritySort(state.values); - for (const value of sortedValues) { - const name = getValueName(state, value); - if (activeLabels.has(name)) { - if (doesExist(activeValue)) { - if (activeLabels.delete(name)) { - changes.push({ - cause: name, - effect: ChangeVerb.CONFLICTED, - label: name, - }); - } - - continue; - } - - const combinedValue: BaseLabel = { - adds: [...state.adds, ...value.adds], - name, - priority: defaultUntil(value.priority, state.priority, 0), - removes: [...state.removes, ...value.removes], - requires: [...state.requires, ...value.requires], - }; - - if (checkLabelRules(combinedValue)) { - continue; - } - - // TODO: flatten this bit and remove the mutable boolean - let removed = false; - for (const become of value.becomes) { - const matches = become.matches.every((l) => activeLabels.has(l.name)); - - if (matches) { - checkLabelRules({ - ...combinedValue, - adds: become.adds, - removes: [...become.matches, ...become.removes], - requires: [], - }); - - if (activeLabels.delete(name)) { - changes.push({ - cause: name, - effect: ChangeVerb.REMOVED, - label: name, - }); - removed = true; - } - - break; - } - - if (removed) { - continue; - } - - activeValue = name; - } - } - } + resolveState(state, result, activeLabels); } - return { - changes, - errors, - labels: Array.from(activeLabels), - }; + result.labels = Array.from(activeLabels); + + return result; } diff --git a/src/sync.ts b/src/sync.ts index 05f10c9..fdf0edc 100644 --- a/src/sync.ts +++ b/src/sync.ts @@ -5,7 +5,7 @@ import { prng } from 'seedrandom'; import { ProjectConfig } from './config'; import { getLabelColor, getLabelNames, getValueName } from './labels'; import { LabelUpdate, Remote } from './remote'; -import { resolveLabels } from './resolve'; +import { resolveProject } from './resolve'; import { compareItems, defaultTo, defaultUntil } from './utils'; export interface SyncOptions { @@ -28,7 +28,7 @@ export async function syncIssueLabels(options: SyncOptions): Promise { for (const issue of issues) { logger.info({ issue }, 'project issue'); - const { changes, errors, labels } = resolveLabels({ + const { changes, errors, labels } = resolveProject({ flags: project.flags, labels: issue.labels, states: project.states, diff --git a/test/TestResolve.ts b/test/TestResolve.ts index 05d946b..ce92f49 100644 --- a/test/TestResolve.ts +++ b/test/TestResolve.ts @@ -1,6 +1,6 @@ import { expect } from 'chai'; -import { resolveLabels } from '../src/resolve'; +import { resolveProject } from '../src/resolve'; import { TEST_CASES } from './resolve/cases'; const TEST_LABELS = ['foo', 'bar']; @@ -8,7 +8,7 @@ const TEST_LABELS = ['foo', 'bar']; describe('resolve labels', () => { describe('with empty rule set', () => { it('should return the existing labels', () => { - const result = resolveLabels({ + const result = resolveProject({ flags: [], labels: TEST_LABELS, states: [], @@ -18,7 +18,7 @@ describe('resolve labels', () => { }); it('should not make any changes', () => { - const result = resolveLabels({ + const result = resolveProject({ flags: [], labels: TEST_LABELS, states: [], @@ -32,7 +32,7 @@ describe('resolve labels', () => { describe('resolver test cases', () => { for (const test of TEST_CASES) { it(`should resolve ${test.name}`, () => { - const actualResult = resolveLabels(test.input); + const actualResult = resolveProject(test.input); expect(actualResult).to.deep.equal(test.result); }); } diff --git a/test/resolve/TestResolveIssueLabels.ts b/test/resolve/TestResolveIssueLabels.ts index 0599fe9..f101337 100644 --- a/test/resolve/TestResolveIssueLabels.ts +++ b/test/resolve/TestResolveIssueLabels.ts @@ -1,13 +1,13 @@ import { expect } from 'chai'; -import { resolveLabels } from '../../src/resolve'; +import { resolveProject } from '../../src/resolve'; const TEST_LABELS = ['foo', 'bar']; describe('resolve labels', () => { describe('flags with unfulfilled requires rule', () => { it('should be removed when required label is missing', () => { - const result = resolveLabels({ + const result = resolveProject({ flags: [{ adds: [], name: 'gayle', @@ -27,7 +27,7 @@ describe('resolve labels', () => { describe('flags with fulfilled requires rule', () => { it('should make no changes', () => { - const result = resolveLabels({ + const result = resolveProject({ flags: [{ adds: [], name: 'gayle', @@ -47,7 +47,7 @@ describe('resolve labels', () => { describe('flags with add rules', () => { it('should add the labels', () => { - const result = resolveLabels({ + const result = resolveProject({ flags: [{ adds: [{ name: 'linda', @@ -67,7 +67,7 @@ describe('resolve labels', () => { describe('flags with remove rules', () => { it('should remove labels', () => { - const result = resolveLabels({ + const result = resolveProject({ flags: [{ adds: [], name: 'bob',