diff --git a/README.md b/README.md index 4ba3176..393314f 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # enum-xyz -JavaScript enums using proxies. +`enum-xyz` offers a way to generate enums in JavaScript leveraging the power of Proxies. It supports various casing styles, transformations, and other customization options. > Based on [this tweet](https://twitter.com/2ality/status/1486139713354448897) @@ -24,9 +24,25 @@ import Enum from "enum-xyz"; const { Summer, Autumn, Winter, Spring } = Enum.String(); console.log(Summer); // Outputs: "Summer" -console.log(Autumn); // Outputs: "Autumn" -console.log(Winter); // Outputs: "Winter" -console.log(Spring); // Outputs: "Spring" +``` + +#### Options for String Enums: + +- `casing`: Transforms the string based on the specified casing style. Available options are `snakeCase`, `camelCase`, `PascalCase`, `kebabCase`, `lowercase`, and `uppercase`. +- `transform`: Provide a custom function to transform the enum values. This function takes the original value and returns a transformed value. + +##### Example: + +``` +const { userId, userAddress } = Enum.String({ casing: 'kebabCase' }); +console.log(userId); // Outputs: "user-id" + +const options = { + casing: 'kebabCase', + transform: (value) => `https://api.example.com/${value}` +}; +const { userEndpoint, orderEndpoint } = Enum.String(options); +console.log(userEndpoint); // Outputs: "https://api.example.com/user-endpoint" ``` ### Numeric Enums @@ -39,16 +55,19 @@ import Enum from "enum-xyz"; const { A, B, C, D } = Enum.Numeric(); console.log(A); // Outputs: 0 -console.log(B); // Outputs: 1 -console.log(C); // Outputs: 2 -console.log(D); // Outputs: 3 ``` -To start from a different index: +#### Options for Numeric Enums: + +- `startIndex`: Start the numeric enum from a specific index. +- `step`: Increment the numeric values by a specific step (e.g., 2, 5, 10). + +##### Example: ``` -const { A, B, C, D } = Enum.Numeric(5); +const { A, B, C } = Enum.Numeric({ startIndex: 5, step: 2 }); console.log(A); // Outputs: 5 +console.log(B); // Outputs: 7 ``` ### Symbol Enums @@ -59,5 +78,15 @@ import Enum from "enum-xyz"; const { blue, red } = Enum.Symbol(); console.log(blue); // Outputs: Symbol(blue) -console.log(red); // Outputs: Symbol(red) +``` + +#### Options for Symbol Enums: + +- `global`: Create a global symbol using Symbol.for(). + +##### Example: + +``` +const { blueGlobal } = Enum.Symbol({ global: true }); +console.log(blueGlobal); // Outputs: Symbol.for('blueGlobal') ``` diff --git a/index.js b/index.js index 092064a..60f887c 100644 --- a/index.js +++ b/index.js @@ -1,29 +1,76 @@ -const createEnum = (type, startIndex = 0) => { - let currentIndex = startIndex +function toCamelCase(str) { + return str.charAt(0).toLowerCase() + str.slice(1).replace(/([-_]\w)/g, g => g[1].toUpperCase()); +} + +function toPascalCase(str) { + return str.charAt(0).toUpperCase() + str.slice(1).replace(/([-_]\w)/g, g => g[1].toUpperCase()); +} + +function toKebabCase(str) { + return str + .replace(/([a-z0-9]|(?=[A-Z]))([A-Z])/g, '$1-$2') + .toLowerCase() + .replace(/^-/, ''); // Remove leading hyphen if present +} + +function toSnakeCase(str) { + return str.replace(/([a-z0-9]|(?=[A-Z]))([A-Z])/g, '$1_$2').toLowerCase(); +} + +const createEnum = (type, options = {}) => { + let currentIndex = options.startIndex || 0; + + const transformValue = (value) => { + // Apply prefix and suffix + value = (options.prefix || '') + value + (options.suffix || ''); + + // Apply casing transformations + switch (options.casing) { + case 'camelCase': + value = toCamelCase(value); + break; + case 'PascalCase': + value = toPascalCase(value); + break; + case 'kebabCase': + value = toKebabCase(value); + break; + case 'snakeCase': + value = toSnakeCase(value); + break; + } + + // Apply custom transform function if provided + if (options.transform && typeof options.transform === 'function') { + value = options.transform(value); + } + + return value; + }; const handler = { get(_, name) { if (type === 'String') { - return name + return transformValue(name); } if (type === 'Numeric') { - const current = currentIndex - currentIndex++ - return current + const current = currentIndex; + currentIndex += options.step || 1; + return current; } if (type === 'Symbol') { - return Symbol(name) + return options.global ? Symbol.for(name) : Symbol(name); } // For grouping, return another proxy - return new Proxy({}, handler) + return new Proxy({}, handler); } - } + }; - return new Proxy({}, handler) + return new Proxy({}, handler); } export default { - String: () => createEnum('String'), - Numeric: (startIndex = 0) => createEnum('Numeric', startIndex), - Symbol: () => createEnum('Symbol') -} \ No newline at end of file + String: (options) => createEnum('String', options), + Numeric: (options = {}) => createEnum('Numeric', options), + Symbol: (options = {}) => createEnum('Symbol', options) +} diff --git a/index.test.js b/index.test.js index 7a53b47..1c3b685 100644 --- a/index.test.js +++ b/index.test.js @@ -1,56 +1,151 @@ import Enum from './index' -test('creates enum and assigns strings', () => { - const { Summer, Autumn, Winter, Spring } = Enum.String() +describe('String Enums', () => { + test('creates enum and assigns strings', () => { + const { Summer, Autumn, Winter, Spring } = Enum.String() + + expect(Summer).toEqual('Summer') + expect(Autumn).toEqual('Autumn') + expect(Winter).toEqual('Winter') + expect(Spring).toEqual('Spring') + }) + + test('creates enum with snakeCase casing', () => { + const { userId, userAddress, orderNumber } = Enum.String({ casing: 'snakeCase' }) + + expect(userId).toEqual('user_id') + expect(userAddress).toEqual('user_address') + expect(orderNumber).toEqual('order_number') + }) + + test('creates enum with camelCase casing', () => { + const { User_Id, User_Address, Order_Number } = Enum.String({ casing: 'camelCase' }) + + expect(User_Id).toEqual('userId') + expect(User_Address).toEqual('userAddress') + expect(Order_Number).toEqual('orderNumber') + }) + + test('creates enum with PascalCase casing', () => { + const { user_id, user_address, order_number } = Enum.String({ casing: 'PascalCase' }) + + expect(user_id).toEqual('UserId') + expect(user_address).toEqual('UserAddress') + expect(order_number).toEqual('OrderNumber') + }) + + test('creates enum with kebabCase casing', () => { + const { UserId, UserAddress, OrderNumber } = Enum.String({ casing: 'kebabCase' }) + + expect(UserId).toEqual('user-id') + expect(UserAddress).toEqual('user-address') + expect(OrderNumber).toEqual('order-number') + }) + + test('creates enum with prefix', () => { + const { Summer, Winter } = Enum.String({ prefix: 'Season_' }); + + expect(Summer).toEqual('Season_Summer'); + expect(Winter).toEqual('Season_Winter'); + }); + + test('creates enum with suffix', () => { + const { Summer, Winter } = Enum.String({ suffix: '_Season' }); + + expect(Summer).toEqual('Summer_Season'); + expect(Winter).toEqual('Winter_Season'); + }); + + test('creates enum with both prefix and suffix', () => { + const { Summer, Winter } = Enum.String({ prefix: 'Season_', suffix: '_Time' }); + + expect(Summer).toEqual('Season_Summer_Time'); + expect(Winter).toEqual('Season_Winter_Time'); + }); + + test('creates enum with transform function', () => { + const transformFn = (value) => `Transformed_${value}`; + const { Summer, Winter } = Enum.String({ transform: transformFn }); + + expect(Summer).toEqual('Transformed_Summer'); + expect(Winter).toEqual('Transformed_Winter'); + }); + + test('creates enum with transform function and casing', () => { + const transformFn = (value) => `Transformed_${value}`; + const { summer_time, winter_time } = Enum.String({ transform: transformFn, casing: 'PascalCase' }); + + expect(summer_time).toEqual('Transformed_SummerTime'); + expect(winter_time).toEqual('Transformed_WinterTime'); + }); +}); - expect(Summer).toEqual('Summer') - expect(Autumn).toEqual('Autumn') - expect(Winter).toEqual('Winter') - expect(Spring).toEqual('Spring') +describe('Numeric Enums', () => { + test('creates enum and assigns numeric value', () => { + const { A, B, C, D } = Enum.Numeric() + + expect(A).toBe(0) + expect(B).toBe(1) + expect(C).toBe(2) + expect(D).toBe(3) + }) + + test('creates enum and assigns numeric value starting at index of choice', () => { + const { A, B, C, D } = Enum.Numeric({ startIndex: 1 }) + + expect(A).toBe(1) + expect(B).toBe(2) + expect(C).toBe(3) + expect(D).toBe(4) + }) + + test('creates enum and assigns numeric value with a specific step', () => { + const { A, B, C, D } = Enum.Numeric({ startIndex: 0, step: 5 }); + + expect(A).toBe(0); + expect(B).toBe(5); + expect(C).toBe(10); + expect(D).toBe(15); + }); + + test('ensures numeric enums are stateless and start from the first accessed key', () => { + const { B, A, C } = Enum.Numeric() + const { D, E } = Enum.Numeric() + const { F, G } = Enum.Numeric({ startIndex: 5 }) + const { H, I } = Enum.Numeric() + + expect(B).toBe(0) + expect(A).toBe(1) + expect(C).toBe(2) + expect(D).toBe(0) + expect(E).toBe(1) + expect(F).toBe(5) + expect(G).toBe(6) + expect(H).toBe(0) + expect(I).toBe(1) + }) }) -test('creates enum and assigns numeric value', () => { - const { A, B, C, D } = Enum.Numeric() - - expect(A).toBe(0) - expect(B).toBe(1) - expect(C).toBe(2) - expect(D).toBe(3) +describe('Symbol Enums', () => { + test('creates enum and assigns symbol values', () => { + const { blue, red } = Enum.Symbol() + const { blue: blueMood, happy } = Enum.Symbol() + + expect(blue).toBe(blue) + expect(blue).not.toBe(red) + expect(blue).not.toBe(blueMood) + expect(blue).not.toBe('blue') + expect(blue).not.toBe(Symbol('blue')) + }) + + test('creates global symbol values', () => { + const { globalBlue, globalRed } = Enum.Symbol({ global: true }); + const { globalBlue: anotherGlobalBlue } = Enum.Symbol({ global: true }); + + expect(globalBlue).toBe(globalBlue); + expect(globalBlue).toBe(anotherGlobalBlue); // Both should reference the same global symbol + expect(globalBlue).not.toBe(globalRed); + expect(globalBlue).not.toBe('globalBlue'); + expect(globalBlue).toBe(Symbol.for('globalBlue')); // Should match the global symbol + }) }) - -test('creates enum and assigns numeric value starting at index of choice', () => { - const { A, B, C, D } = Enum.Numeric(1) - - expect(A).toBe(1) - expect(B).toBe(2) - expect(C).toBe(3) - expect(D).toBe(4) -}) - -test('ensures numeric enums are stateless and start from the first accessed key', () => { - const { B, A, C } = Enum.Numeric() - const { D, E } = Enum.Numeric() - const { F, G } = Enum.Numeric(5) - const { H, I } = Enum.Numeric() - - expect(B).toBe(0) - expect(A).toBe(1) - expect(C).toBe(2) - expect(D).toBe(0) - expect(E).toBe(1) - expect(F).toBe(5) - expect(G).toBe(6) - expect(H).toBe(0) - expect(I).toBe(1) -}) - -test('creates enum and assigns symbol values', () => { - const { blue, red } = Enum.Symbol() - const { blue: blueMood, happy } = Enum.Symbol() - - expect(blue).toBe(blue) - expect(blue).not.toBe(red) - expect(blue).not.toBe(blueMood) - expect(blue).not.toBe('blue') - expect(blue).not.toBe(Symbol('blue')) -}) \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index d4b0059..d06616b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "enum-xyz", - "version": "0.2.0", + "version": "0.3.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "enum-xyz", - "version": "0.2.0", + "version": "0.3.0", "license": "ISC", "devDependencies": { "jest": "^27.5.1", diff --git a/package.json b/package.json index 368b249..0927328 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "enum-xyz", "type": "module", - "version": "0.2.0", + "version": "0.3.0", "description": "JavaScript enums using proxies.", "homepage": "https://github.com/chasefleming/enum-xyz", "author": "Chase Fleming",