Created
June 9, 2025 06:00
-
-
Save PatrickKennedy/76d4ff1048ba309872a20f931eb41f86 to your computer and use it in GitHub Desktop.
useBitwiseRules - A simple, typesafe multidimensional rules system
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 { useBitwiseRules } from './useBitwiseRules' | |
| enum TestFlags { | |
| TypeA = 1 << 0, | |
| TypeB = 1 << 1, | |
| StatusA = 1 << 2, | |
| StatusB = 1 << 3, | |
| StatusC = 1 << 4, | |
| AnyType = TypeA | TypeB, | |
| AnyStatus = StatusA | StatusB | StatusC, | |
| Unrestricted = TypeA | TypeB | StatusA | StatusB | StatusC, | |
| } | |
| enum ConflictingFlags { | |
| Any = 0, | |
| First = 1 << 0, | |
| Second = 1 << 1, | |
| Third = 1 << 2, | |
| } | |
| type TestRules = { | |
| show: TestFlags | |
| required?: TestFlags | |
| readonly?: TestFlags | |
| } | |
| describe('useBitwiseRules', context => { | |
| it('should not allow flags from different enums', () => { | |
| const { runLogic } = useBitwiseRules<TestFlags, TestRules>() | |
| // Setting a single flag from another enum will throw a type error because | |
| // it's still considered of the enum's type | |
| // @ts-expect-error - This comment will light up if there is no type error | |
| const singleFlag: TestFlags = ConflictingFlags.First | |
| // The bitwise operation on the enums converts the result to a number | |
| // and because the enums are numbers, it will not throw an error | |
| const flags: TestFlags = ConflictingFlags.First | TestFlags.TypeA | |
| const rule = { | |
| show: ConflictingFlags.Any, | |
| required: TestFlags.TypeA, | |
| } | |
| // @ts-expect-error - This comment will light up if there is no type error | |
| assertType(runLogic(flags, rule)) | |
| }) | |
| test('Unrestricted allows any flags', () => { | |
| const { runLogic } = useBitwiseRules<TestFlags, TestRules>() | |
| const flags = TestFlags.TypeA | TestFlags.StatusA | |
| const rule = { | |
| show: TestFlags.Unrestricted, | |
| required: TestFlags.Unrestricted, | |
| } | |
| const result = runLogic(flags, rule) | |
| expect(result.show).toBe(true) | |
| expect(result.required).toBe(true) | |
| }) | |
| test('Rule without a Course Type should accept any Course Type', () => { | |
| const { runLogic } = useBitwiseRules<TestFlags, TestRules>() | |
| const flags = TestFlags.TypeA | TestFlags.StatusA | |
| const rule = { | |
| show: TestFlags.AnyType | TestFlags.StatusA, | |
| required: TestFlags.StatusB, | |
| } | |
| const result = runLogic(flags, rule) | |
| expect(result.show).toBe(true) | |
| expect(result.required).toBe(false) | |
| }) | |
| test('Rule with multiple flags should return true if only one flag is set', () => { | |
| const { runLogic } = useBitwiseRules<TestFlags, TestRules>() | |
| const flags = TestFlags.TypeA | TestFlags.StatusA | |
| const rule = { | |
| show: TestFlags.AnyType | TestFlags.StatusA | TestFlags.StatusB, | |
| required: TestFlags.TypeB | TestFlags.AnyStatus | |
| } | |
| const result = runLogic(flags, rule) | |
| expect(result.show).toBe(true) | |
| expect(result.required).toBe(false) | |
| }) | |
| }) |
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
| /** | |
| * Provides a standardized way to run bitwise logic against flags and rules. | |
| * This makes it much easier to encode logic that depends on multiple attributes | |
| * | |
| * @see: https://shaky.sh/ts-bit-flags/ | |
| * | |
| * It differs from the article in that the flags are more restrictive than the | |
| * rules, so by flipping the conditional logic we're asking if all the flags are | |
| * present in the rule rather than if all the rules are present in the flags. | |
| * | |
| * This comes into play with courses where a course can only be one each of a | |
| * type and a status, it will never be both a Credit and Degree course, so | |
| * inorder for the rules to accept more than one credit status we compare in | |
| * reverse. | |
| */ | |
| export const useBitwiseRules = <Flags extends number, Rules extends Record<string, number>>() => { | |
| const runLogic = (flags: Flags, rules: Rules) => { | |
| const result = {} as Record<keyof Rules, boolean> | |
| Object.entries(rules).forEach( | |
| ([field, rule]) => | |
| // The question this is asking is "are all the flags present in the rule?" | |
| (result[field as keyof Rules] = (rule & flags) === flags) | |
| ) | |
| return result | |
| } | |
| return { | |
| runLogic, | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment