Created
January 2, 2023 14:04
-
-
Save kamilkisiela/751591acc126e1d72b736a262e8aef38 to your computer and use it in GitHub Desktop.
Checks - step by step
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { createCheck, runCheck } from '../providers/checks'; | |
// <step status> (<requirement>) -> <step status> (<requirement>) = <failed step index 1,2...n> (<result status>) | |
runTest('skipped (optional) -> skipped (optional) = (completed)'); | |
runTest('completed (optional) -> skipped (optional) = (completed)'); | |
runTest('skipped (optional) -> skipped (required) = 2 (failed)'); | |
runTest('skipped (required) -> skipped (required) = 1 (failed)'); | |
runTest('completed (required) -> skipped (required) = 2 (failed)'); | |
runTest('completed (optional) -> skipped (required) = 2 (failed)'); | |
runTest('completed (optional) -> completed (required) -> failed (required) = 3 (failed)'); | |
runTest('completed (optional) -> completed (required) -> failed (optional) = 3 (failed)'); | |
// | |
function parseResult(scenario: string) { | |
const parts = scenario.trim().split('('); | |
if (parts.length === 2) { | |
return { | |
index: parseInt(parts[0], 10) - 1, | |
status: parts[1].replace(')', ''), | |
}; | |
} | |
return { | |
index: null, | |
status: parts[0].replace(')', ''), | |
}; | |
} | |
function parseSteps(scenario: string) { | |
return scenario.split(' -> ').map((check, i) => { | |
const [status, requirement] = Array.from(check.match(/(\w+)\s+\((\w+)\)/)!).slice(1); | |
const stepId: string = `step-${i}` as const; | |
if (requirement !== 'required' && requirement !== 'optional') { | |
throw new Error(`Invalid requirement: ${requirement}`); | |
} | |
return { | |
stepId, | |
status: status as 'skipped' | 'completed' | 'failed', | |
requirement: requirement as 'required' | 'optional', | |
}; | |
}); | |
} | |
function runTest(scenario: string) { | |
const [stepsScenario, resultScenario] = scenario.split(' = '); | |
const steps = parseSteps(stepsScenario).map(step => { | |
return { | |
...step, | |
check: createCheck(step.stepId, async () => { | |
if (step.status === 'skipped') { | |
return { | |
status: 'skipped', | |
}; | |
} | |
if (step.status === 'completed') { | |
return { | |
status: 'completed', | |
result: null, | |
}; | |
} | |
if (step.status === 'failed') { | |
return { | |
status: 'failed', | |
reason: null, | |
}; | |
} | |
throw new Error(`Invalid status: ${step.status}`); | |
}), | |
}; | |
}); | |
const { index, status } = parseResult(resultScenario); | |
test(scenario, async () => { | |
const result = await runCheck( | |
steps.map(s => s.check), | |
steps.reduce((acc, s) => ({ ...acc, [s.check.id]: s.requirement }), {}), | |
); | |
const expectedState: any = {}; | |
for (const step of steps) { | |
expectedState[step.stepId] = { | |
id: step.stepId, | |
status: 'skipped', | |
}; | |
} | |
for await (const [i, step] of steps.entries()) { | |
if (index != null && i > index) { | |
break; | |
} | |
const r = await step.check.runner(); | |
expectedState[step.stepId] = { | |
...r, | |
id: step.stepId, | |
}; | |
} | |
if (index !== null) { | |
expectedState; | |
} | |
expect(result).toEqual({ | |
status, | |
state: expectedState, | |
...(index == null | |
? {} | |
: { | |
step: expectedState[`step-${index}`], | |
}), | |
}); | |
}); | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import type * as tst from 'ts-toolbelt'; | |
export async function runCheck<T extends Check<string, unknown, unknown>[]>( | |
checks: T, | |
rules: { | |
[K in keyof CheckListToObject<T>]: 'required' | 'optional'; | |
}, | |
): Promise< | |
| { status: 'completed'; state: NonFailedState<CheckListToObject<T>> } | |
| { | |
status: 'failed'; | |
state: CheckListToObject<T>; | |
step: NonCompleted<CheckResultOf<T[number]>> & { | |
id: keyof CheckListToObject<T>; | |
}; | |
} | |
> { | |
const state: CheckListToObject<T> = checks.reduce( | |
(acc, { id }) => ({ | |
...acc, | |
[id]: { | |
id, | |
status: 'skipped', | |
}, | |
}), | |
{} as any, | |
); | |
let status: 'completed' | 'failed' = 'completed'; | |
let failedStep: any | null = null; | |
for await (const step of checks) { | |
const r = await step.runner(); | |
const id = step.id as unknown as keyof typeof state; | |
state[id] = { | |
...(r as any), | |
id, | |
}; | |
const isRequired = rules[id] === 'required'; | |
if ((isRequired && !isCompleted(r)) || r.status === 'failed') { | |
failedStep = { | |
...r, | |
id, | |
}; | |
status = 'failed'; | |
break; | |
} | |
} | |
if (status === 'failed') { | |
return { | |
status, | |
step: failedStep, | |
state, | |
}; | |
} | |
return { | |
status, | |
state: state as NonFailedState<CheckListToObject<T>>, | |
}; | |
} | |
export function createCheck<K extends string, C, F>( | |
id: K, | |
runner: () => Promise<CheckResult<C, F>>, | |
) { | |
return { | |
id, | |
runner, | |
}; | |
} | |
// The reason why I'm using `result` and `reason` instead of just `data` for both: | |
// https://bit.ly/hive-check-result-data | |
export type CheckResult<C = unknown, F = unknown> = | |
| { | |
status: 'completed'; | |
result: C; | |
} | |
| { | |
status: 'failed'; | |
reason: F; | |
} | |
| { | |
status: 'skipped'; | |
}; | |
type CheckResultOf<T> = T extends Check<string, infer C, infer F> ? CheckResult<C, F> : never; | |
type Check<K extends string, C, F> = { | |
id: K; | |
runner: () => Promise<CheckResult<C, F>>; | |
}; | |
type CheckListToObject<T extends ReadonlyArray<Check<string, unknown, unknown>>> = tst.Union.Merge< | |
T extends ReadonlyArray<infer U> | |
? U extends Check<infer IK, unknown, unknown> | |
? { | |
[P in IK]: U['id'] extends P | |
? CheckResultOf<U> & { | |
id: P; | |
} | |
: never; | |
} | |
: never | |
: never | |
>; | |
function isCompleted<T extends CheckResult<unknown, unknown>>(step: T): step is Completed<T> { | |
return step.status === 'completed'; | |
} | |
type Completed<T> = T extends CheckResult<unknown, unknown> | |
? T extends { status: 'completed' } | |
? T | |
: never | |
: never; | |
type NonCompleted<T> = T extends CheckResult<unknown, unknown> | |
? T extends { status: 'completed' } | |
? never | |
: T | |
: never; | |
type NonFailed<T> = T extends CheckResult<unknown, unknown> | |
? T extends { status: 'failed' } | |
? never | |
: T | |
: never; | |
type WithId<T, K> = T & { | |
id: K; | |
}; | |
type NonFailedState<T> = T extends { | |
[K in keyof T]: WithId<CheckResult<unknown, unknown>, K>; | |
} | |
? { | |
[K in keyof T]: NonFailed<T[K]>; | |
} | |
: never; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment