Skip to content

Instantly share code, notes, and snippets.

@scinscinscin
Created July 20, 2023 13:10
Show Gist options
  • Save scinscinscin/27dd8cd65b65dd87af1982e19e7cf9f5 to your computer and use it in GitHub Desktop.
Save scinscinscin/27dd8cd65b65dd87af1982e19e7cf9f5 to your computer and use it in GitHub Desktop.
A copy paste library for managing bitfields with full typesafety in TypeScript
interface GandalfT<T extends Readonly<string[]>> {
addBit(bitfield: number, permission: T[number] | number): number;
removeBit(bitfield: number, permission: T[number] | number): number;
hasBit(bitfield: number, permission: T[number] | number): boolean;
create(permissions: readonly (T[number] | number)[]): number;
toggleBit(bitfield: number, permission: T[number] | number): number;
getConstants(bitfield: number): string[];
enum: Record<T[number], number>;
$enumConstants: T;
}
/**
* A function for manipulating bitfields with full typesafety
* @example
* ```ts
* // The bits are based on the indexes of the array, so "modify_post" is controlled by the 0th bit, or LSB
* const rbac = Gandalf(["modify_post", "modify_media"] as const);
* const adminBitfield = rbac.create(["modify_post", "modify_media"]);
* console.log(rbac.hasBit(adminBitfield, "modify_media"));
* console.log(rbac.removeBit(adminBitfield, "modify_media"));
* ```
*/
export function Gandalf<T extends Readonly<string[]>>(enumConstants: T): GandalfT<T> {
const permissions = {} as Record<T[number], number>;
for (let i = 0; i < enumConstants.length; i++) {
permissions[enumConstants[i] as T[number]] = i;
}
const getIndex = (permission: T[number] | number) => {
const index = typeof permission === "number" ? permission : enumConstants.indexOf(permission);
if (index >= 0) return index;
else throw new Error("Was not able to find permission with that index");
};
return {
addBit(bitfield, permission) {
return bitfield | (0b1 << getIndex(permission));
},
removeBit(bitfield, permission) {
// Get a bitmask with a 1 in the position that we want to remove
// Then invert it using Bitwise XOR to get a 0 mask instead
// then AND it with the existing bitfield to set the bit to 0
return bitfield & ((0b1 << getIndex(permission)) ^ (Math.pow(2, enumConstants.length) - 1));
},
hasBit(bitfield, permission) {
// the value is 0 if the bitfield does not have that property bit
return (bitfield & (0b1 << getIndex(permission))) !== 0;
},
toggleBit(bitfield, permission) {
if (this.hasBit(bitfield, permission)) return this.removeBit(bitfield, permission);
else return this.addBit(bitfield, permission);
},
/**
* Create a bitfield from an array of permissions
* @param permissions The list of permissions to create the bitfield with
* @returns a bitfield
*/
create(permissions) {
let bitfield = 0;
for (const permission of permissions) bitfield = bitfield | (0b1 << getIndex(permission));
return bitfield;
},
/**
* Get the string constants that the bitfield has enabled
*/
getConstants(bitfield) {
return enumConstants.filter((_, i) => this.hasBit(bitfield, i));
},
enum: permissions,
$enumConstants: enumConstants,
};
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment