Skip to content

Commit

Permalink
feat(core): add recursive config option (#61)
Browse files Browse the repository at this point in the history
  • Loading branch information
h2xd authored Apr 20, 2023
1 parent 5f1caed commit 6abdb5c
Show file tree
Hide file tree
Showing 20 changed files with 304 additions and 99 deletions.
5 changes: 5 additions & 0 deletions .github/pr_labeler.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
"📚 documentation": ['docs/*', 'documentation/*']
"🐛 bug": ['fix/*', 'bugfix/*', 'hotfix/*']
"✨ enhancement": ["feature/*", "feat/*"]
"🤖 automation": ["ci/*"]
"⚙️ refactoring": ["refactor/*"]
17 changes: 0 additions & 17 deletions .github/release-drafter.yaml → .github/release-drafter.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,20 +34,3 @@ template: |
<!-- Optional: Add in custom description before the release -->
$CHANGES
autolabeler:
- label: '📚 documentation'
branch:
- '/^docs\/.+$/'
- label: '🐛 bug'
branch:
- '/^fix\/.+$/'
- label: '✨ enhancement'
branch:
- '/^feature\/.+$/'
- '/^feat\/.+$/'
- label: '🤖 automation'
branch:
- '/^ci\/.+$/'
- label: '🤖 refactoring'
branch:
- '/^refactor\/.+$/'
20 changes: 20 additions & 0 deletions .github/workflows/pr_management.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
name: 🎨 Pull Request Management
on:
pull_request:
types: [opened]

permissions:
contents: read

jobs:
pr-labeler:
name: '➕ Add labels'
permissions:
contents: read
pull-requests: write
runs-on: ubuntu-latest
steps:
- uses: TimonVS/pr-labeler-action@v4
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
configuration-path: .github/pr_labeler.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,18 @@
# https://github.com/release-drafter/release-drafter

name: ✍️ Release Drafter
description: Create a new release draft each time once a PR is merged into main

on:
push:
branches:
- main
pull_request:
types: [opened, reopened, synchronize]

permissions:
contents: read

jobs:
update_release_draft:
name: '📝 Update Draft'
permissions:
# write permission is required to create a github release
contents: write
Expand Down
49 changes: 27 additions & 22 deletions packages/core/src/functions/Exposition.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,20 @@ describe('Exposition', () => {
'thunderstorm',
],
},
group1: {
item1: {
options: [
'1', '2', '3',
],
},
group1_1: {
item1_1: {
options: [
'1.1', '2.1', '3.1',
],
},
},
},
} as const

let exposition: Exposition<typeof expositionConfig>
Expand All @@ -34,36 +48,27 @@ describe('Exposition', () => {

describe('methods', () => {
it('should have a method to get the current values', () => {
expect(exposition.values).toMatchObject({ dream: 'NREM_stage_1', reality: 'heatwave' })
expect(exposition.values).toMatchObject({ dream: 'NREM_stage_1', reality: 'heatwave', group1: { item1: '1', group1_1: { item1_1: '1.1' } } })
})

it('should have a method to update the current values', () => {
exposition.update({ dream: 'NREM_stage_2' })
exposition.update({ dream: 'NREM_stage_2', group1: { item1: '2' } })

expect(exposition.values).toMatchObject({ dream: 'NREM_stage_2', reality: 'heatwave' })
expect(exposition.values).toMatchObject({ dream: 'NREM_stage_2', reality: 'heatwave', group1: { item1: '2', group1_1: { item1_1: '1.1' } } })
})

it('should have a method to get the initial values', () => {
exposition.update({ dream: 'NREM_stage_2' })
expect(exposition.initialValues).toMatchObject({ dream: 'NREM_stage_1', reality: 'heatwave' })
exposition.update({ dream: 'NREM_stage_2', group1: { group1_1: { item1_1: '3.1' } } })
expect(exposition.initialValues).toMatchObject({ dream: 'NREM_stage_1', reality: 'heatwave', group1: { item1: '1', group1_1: { item1_1: '1.1' } } })
})

it('should have a method to reset the current values', () => {
exposition.update({ dream: 'NREM_stage_2' })
exposition.update({ dream: 'NREM_stage_2', group1: { group1_1: { item1_1: '3.1' } } })

expect(exposition.values).toMatchObject({ dream: 'NREM_stage_2', reality: 'heatwave' })
expect(exposition.values).toMatchObject({ dream: 'NREM_stage_2', reality: 'heatwave', group1: { item1: '1', group1_1: { item1_1: '3.1' } } })

exposition.reset()
expect(exposition.values).toMatchObject({ dream: 'NREM_stage_1', reality: 'heatwave' })
})

it('should only reset a subset of scenarios', () => {
exposition.update({ dream: 'NREM_stage_2', reality: 'thunderstorm' })

expect(exposition.values).toMatchObject({ dream: 'NREM_stage_2', reality: 'thunderstorm' })

exposition.reset(['reality'])
expect(exposition.values).toMatchObject({ dream: 'NREM_stage_2', reality: 'heatwave' })
expect(exposition.values).toMatchObject({ dream: 'NREM_stage_1', reality: 'heatwave', group1: { item1: '1', group1_1: { item1_1: '1.1' } } })
})
})

Expand All @@ -75,7 +80,7 @@ describe('Exposition', () => {
exposition.reset()

expect(spy).toHaveBeenCalledTimes(1)
expect(spy).toHaveBeenCalledWith({ dream: 'NREM_stage_1', reality: 'heatwave' }, defaultSettings)
expect(spy).toHaveBeenCalledWith({ dream: 'NREM_stage_1', reality: 'heatwave', group1: { item1: '1', group1_1: { item1_1: '1.1' } } }, defaultSettings)
})

it('should emit an update event', () => {
Expand All @@ -85,7 +90,7 @@ describe('Exposition', () => {
exposition.update({ dream: 'NREM_stage_3' })

expect(spy).toHaveBeenCalledTimes(1)
expect(spy).toHaveBeenCalledWith({ dream: 'NREM_stage_3', reality: 'heatwave' }, defaultSettings)
expect(spy).toHaveBeenCalledWith({ dream: 'NREM_stage_3', reality: 'heatwave', group1: { item1: '1', group1_1: { item1_1: '1.1' } } }, defaultSettings)
})

it('should fire the initialized event', () => {
Expand All @@ -96,7 +101,7 @@ describe('Exposition', () => {
exposition.init()
exposition.init()
expect(spy).toHaveBeenCalledTimes(1)
expect(spy).toHaveBeenCalledWith({ dream: 'NREM_stage_1', reality: 'heatwave' }, defaultSettings)
expect(spy).toHaveBeenCalledWith({ dream: 'NREM_stage_1', reality: 'heatwave', group1: { item1: '1', group1_1: { item1_1: '1.1' } } }, defaultSettings)
})
})

Expand All @@ -113,7 +118,7 @@ describe('Exposition', () => {

exposition.init()
expect(spy).toHaveBeenCalled()
expect(spy).toHaveBeenCalledWith({ dream: 'NREM_stage_1', reality: 'heatwave' }, defaultSettings)
expect(spy).toHaveBeenCalledWith({ dream: 'NREM_stage_1', reality: 'heatwave', group1: { item1: '1', group1_1: { item1_1: '1.1' } } }, defaultSettings)
})
})

Expand Down Expand Up @@ -146,7 +151,7 @@ describe('Exposition', () => {
exposition.updateSettings({ active: false })

expect(spy).toHaveBeenCalledTimes(1)
expect(spy).toHaveBeenCalledWith({ dream: 'NREM_stage_1', reality: 'heatwave' }, { active: false, restoreState: true })
expect(spy).toHaveBeenCalledWith({ dream: 'NREM_stage_1', reality: 'heatwave', group1: { item1: '1', group1_1: { item1_1: '1.1' } } }, { active: false, restoreState: true })
})
})
})
Expand Down
11 changes: 5 additions & 6 deletions packages/core/src/functions/Exposition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,12 +74,11 @@ export class Exposition<T extends ExpositionConfig> {

/**
* Reset the `Scenario` elements of this instance
* @param scenariosToReset - pick what `Scenario` elements should be set to their `initialValue`
* @emits reset - after the values were reverted
* @returns `Exposition`
*/
public reset(scenariosToReset: (keyof ExpositionState<T>)[] = []): Exposition<T> {
Object.assign(this.state, resetExpositionValues(this.state, scenariosToReset))
public reset(): Exposition<T> {
Object.assign(this.state, resetExpositionValues(this.state))

this.emit(EventNames.RESET)

Expand All @@ -92,7 +91,7 @@ export class Exposition<T extends ExpositionConfig> {
* @emits update - after the `Scenario` elements were set to their new value
* @returns `Exposition`
*/
public update(newValues: Partial<ExpositionValues<ExpositionState<T>>>): Exposition<T> {
public update(newValues: PartialDeep<ExpositionValues<ExpositionState<T>>>): Exposition<T> {
Object.assign(this.state, updateExpositionValues(this.state, newValues))

this.emit(EventNames.UPDATE)
Expand All @@ -106,15 +105,15 @@ export class Exposition<T extends ExpositionConfig> {
* @emits updateSettings - after the settings were set to their new value
* @returns `Exposition`
*/
public updateSettings(newSettings: Partial<ExpositionSettings>): Exposition<T> {
public updateSettings(newSettings: PartialDeep<ExpositionSettings>): Exposition<T> {
this.assignNewSettings(newSettings)

this.emit(EventNames.UPDATE_SETTINGS)

return this
}

private assignNewSettings(newSettings: Partial<ExpositionSettings>): void {
private assignNewSettings(newSettings: PartialDeep<ExpositionSettings>): void {
Object.assign(this.settingsState, { ...this.settingsState, ...newSettings })
}

Expand Down
1 change: 1 addition & 0 deletions packages/sdk/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
"rollup": "^2.52.3",
"rollup-plugin-dts": "^4.2.2",
"rollup-plugin-esbuild": "^4.9.1",
"type-fest": "^2.17.0",
"typescript": "^4.7.3"
}
}
10 changes: 7 additions & 3 deletions packages/sdk/src/@types/exposition.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
import type { Scenario, ScenarioConfig } from './scenario'

export type ExpositionConfig = Readonly<Record<string, ScenarioConfig<string>>>
export interface ExpositionConfig {
readonly [key: string]: ExpositionConfigOption
}

export type ExpositionConfigOption = ScenarioConfig<string> | ExpositionConfig

export type ExpositionState<T extends ExpositionConfig> = {
[K in keyof T]: Scenario<T[K]['options'][number]>
[K in keyof T]: T[K] extends ScenarioConfig<string> ? Scenario<T[K]['options'][number]> : (T[K] extends ExpositionConfig ? ExpositionState<T[K]> : never)
}

export type ExpositionValues<T extends ExpositionState<ExpositionConfig>> = {
[K in keyof T]: T[K]['options'][number]
[K in keyof T]: T[K] extends Scenario<T[K]['options'][number]> ? T[K]['options'][number] : (T[K] extends ExpositionState<T[K]> ? ExpositionValues<T[K]> : never)
}
56 changes: 56 additions & 0 deletions packages/sdk/src/functions/createExpositionState.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,59 @@ it('should return a immutable exposition', () => {

expect(resultExposition).toMatchObject(expectedExposition)
})

it('should create grouped states', () => {
const expositionConfig = {
user: {
options: [
'well-behaved',
'User was send to the shadow realm',
],
},
group: {
item1: {
options: [
'option1',
'option2',
],
},
group2: {
item2: {
options: [
'option2.1',
'option2.2',
],
},
},
},
}

const resultExposition = createExpositionState(expositionConfig)

const expectedExposition = {
user: {
id: 'user',
initialValue: 'well-behaved',
value: 'well-behaved',
options: ['well-behaved', 'User was send to the shadow realm'],
},
group: {
item1: {
id: 'group.item1',
initialValue: 'option1',
value: 'option1',
options: ['option1', 'option2'],
},
group2: {
item2: {
id: 'group.group2.item2',
initialValue: 'option2.1',
value: 'option2.1',
options: ['option2.1', 'option2.2'],
},
},
},
}

expect(resultExposition).toMatchObject(expectedExposition)
})
31 changes: 22 additions & 9 deletions packages/sdk/src/functions/createExpositionState.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { ExpositionConfig, ExpositionState } from '../@types/exposition'
import { isScenarioConfig } from '../utils/guards'

/**
* Create an Exposition state with all necessary data. 🔮
Expand All @@ -7,6 +8,7 @@ import type { ExpositionConfig, ExpositionState } from '../@types/exposition'
* - The first `options` item will be set as the `initialValue` of the `Scenario`
*
* @param config
* @param prependKey
* @returns `ExpositionState`
* @example
const expositionState = createExpositionState({
Expand All @@ -15,20 +17,31 @@ import type { ExpositionConfig, ExpositionState } from '../@types/exposition'
}
} as const)
*/
export function createExpositionState<T extends ExpositionConfig>(config: T): ExpositionState<T> {
export function createExpositionState<T extends ExpositionConfig>(config: T, prependKey?: string): ExpositionState<T> {
return Object.keys(config).reduce((accumulator, key) => {
const { options } = config[key]
const configItem = config[key]

const firstOptionValue = options[0]
const combinedKey = prependKey ? `${prependKey}.${key}` : key

if (isScenarioConfig(configItem)) {
const { options } = configItem

const firstOptionValue = options[0]

return {
...accumulator,
[key]: {
id: combinedKey,
options,
initialValue: firstOptionValue,
value: firstOptionValue,
},
}
}

return {
...accumulator,
[key]: {
id: key,
options,
initialValue: firstOptionValue,
value: firstOptionValue,
},
[key]: createExpositionState(configItem, combinedKey),
}
}, {} as ExpositionState<T>)
}
12 changes: 11 additions & 1 deletion packages/sdk/src/functions/getExpositionValues.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,22 @@ it('should extract selected values from an Exposition', () => {
'🍝 Pasta - Mama Mia',
],
},
group1: {
item1: {
options: ['1', '2'],
},
},
} as const

const expositionState = createExpositionState(expositionConfig)
const expositionValues = getExpositionValues(expositionState)

expect(expositionValues.base).toBe('🍚 rice')
expect(expositionValues).toMatchObject({
base: '🍚 rice',
group1: {
item1: '1',
},
})
})

it('should return a map that wont mutate the given argument', () => {
Expand Down
Loading

0 comments on commit 6abdb5c

Please sign in to comment.