@digitaldefiance/branded-enum 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +756 -0
- package/dist/index.js +44 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/accessors.js +75 -0
- package/dist/lib/accessors.js.map +1 -0
- package/dist/lib/advanced.js +1589 -0
- package/dist/lib/advanced.js.map +1 -0
- package/dist/lib/branded-enum.js +5 -0
- package/dist/lib/branded-enum.js.map +1 -0
- package/dist/lib/decorators.js +284 -0
- package/dist/lib/decorators.js.map +1 -0
- package/dist/lib/factory.js +77 -0
- package/dist/lib/factory.js.map +1 -0
- package/dist/lib/guards.js +329 -0
- package/dist/lib/guards.js.map +1 -0
- package/dist/lib/merge.js +91 -0
- package/dist/lib/merge.js.map +1 -0
- package/dist/lib/registry.js +133 -0
- package/dist/lib/registry.js.map +1 -0
- package/dist/lib/types.js +23 -0
- package/dist/lib/types.js.map +1 -0
- package/dist/lib/utils.js +161 -0
- package/dist/lib/utils.js.map +1 -0
- package/dist/package.json +120 -0
- package/package.json +110 -0
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type guards for branded enums.
|
|
3
|
+
*
|
|
4
|
+
* Provides runtime type checking and assertion functions
|
|
5
|
+
* for validating values against branded enums.
|
|
6
|
+
*/ import { ENUM_ID, ENUM_VALUES } from './types.js';
|
|
7
|
+
/**
|
|
8
|
+
* Checks if an object is a branded enum (has Symbol metadata).
|
|
9
|
+
*
|
|
10
|
+
* @param obj - The object to check
|
|
11
|
+
* @returns true if obj is a branded enum
|
|
12
|
+
*/ function isBrandedEnum(obj) {
|
|
13
|
+
return obj !== null && typeof obj === 'object' && ENUM_ID in obj && ENUM_VALUES in obj && typeof obj[ENUM_ID] === 'string' && obj[ENUM_VALUES] instanceof Set;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Checks if a value belongs to a specific branded enum.
|
|
17
|
+
*
|
|
18
|
+
* Returns true if and only if:
|
|
19
|
+
* - enumObj is a valid branded enum (has Symbol metadata)
|
|
20
|
+
* - value is a string
|
|
21
|
+
* - value exists in the enum's value Set
|
|
22
|
+
*
|
|
23
|
+
* This function provides TypeScript type narrowing - when it returns true,
|
|
24
|
+
* the value's type is narrowed to the enum's value type.
|
|
25
|
+
*
|
|
26
|
+
* @template E - The branded enum type
|
|
27
|
+
* @param value - The value to check. Can be any type; non-strings return false.
|
|
28
|
+
* @param enumObj - The branded enum to check against
|
|
29
|
+
* @returns `true` if value is in the enum (with type narrowing), `false` otherwise.
|
|
30
|
+
* Returns `false` for non-string values, null, undefined, or if enumObj
|
|
31
|
+
* is not a branded enum.
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* // Basic type guard usage
|
|
35
|
+
* const Status = createBrandedEnum('status', { Active: 'active', Inactive: 'inactive' } as const);
|
|
36
|
+
*
|
|
37
|
+
* function handleStatus(value: unknown) {
|
|
38
|
+
* if (isFromEnum(value, Status)) {
|
|
39
|
+
* // value is narrowed to 'active' | 'inactive'
|
|
40
|
+
* console.log('Valid status:', value);
|
|
41
|
+
* } else {
|
|
42
|
+
* console.log('Invalid status');
|
|
43
|
+
* }
|
|
44
|
+
* }
|
|
45
|
+
*
|
|
46
|
+
* @example
|
|
47
|
+
* // Returns false for non-string values
|
|
48
|
+
* isFromEnum(123, Status); // false
|
|
49
|
+
* isFromEnum(null, Status); // false
|
|
50
|
+
* isFromEnum(undefined, Status); // false
|
|
51
|
+
*
|
|
52
|
+
* @example
|
|
53
|
+
* // Returns false for non-branded enum objects
|
|
54
|
+
* isFromEnum('active', { Active: 'active' }); // false (not a branded enum)
|
|
55
|
+
*/ export function isFromEnum(value, enumObj) {
|
|
56
|
+
// Return false for non-string values
|
|
57
|
+
if (typeof value !== 'string') {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
// Return false if enumObj is not a branded enum
|
|
61
|
+
if (!isBrandedEnum(enumObj)) {
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
// Check if value exists in enum's value Set
|
|
65
|
+
return enumObj[ENUM_VALUES].has(value);
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Asserts that a value belongs to a branded enum, throwing if not.
|
|
69
|
+
*
|
|
70
|
+
* Use this function when you want to validate a value and throw an error
|
|
71
|
+
* if it's invalid, rather than handling the false case manually.
|
|
72
|
+
*
|
|
73
|
+
* @template E - The branded enum type
|
|
74
|
+
* @param value - The value to check. Can be any type.
|
|
75
|
+
* @param enumObj - The branded enum to check against
|
|
76
|
+
* @returns The value with narrowed type if valid
|
|
77
|
+
* @throws {Error} Throws `Error` with message `Second argument is not a branded enum`
|
|
78
|
+
* if enumObj is not a valid branded enum.
|
|
79
|
+
* @throws {Error} Throws `Error` with message `Value "${value}" is not a member of enum "${enumId}"`
|
|
80
|
+
* if the value is not found in the enum.
|
|
81
|
+
*
|
|
82
|
+
* @example
|
|
83
|
+
* // Successful assertion
|
|
84
|
+
* const Status = createBrandedEnum('status', { Active: 'active', Inactive: 'inactive' } as const);
|
|
85
|
+
* const validated = assertFromEnum('active', Status);
|
|
86
|
+
* // validated is typed as 'active' | 'inactive'
|
|
87
|
+
*
|
|
88
|
+
* @example
|
|
89
|
+
* // Throws for invalid value
|
|
90
|
+
* try {
|
|
91
|
+
* assertFromEnum('unknown', Status);
|
|
92
|
+
* } catch (e) {
|
|
93
|
+
* console.log(e.message); // 'Value "unknown" is not a member of enum "status"'
|
|
94
|
+
* }
|
|
95
|
+
*
|
|
96
|
+
* @example
|
|
97
|
+
* // Throws for non-branded enum
|
|
98
|
+
* try {
|
|
99
|
+
* assertFromEnum('active', { Active: 'active' });
|
|
100
|
+
* } catch (e) {
|
|
101
|
+
* console.log(e.message); // 'Second argument is not a branded enum'
|
|
102
|
+
* }
|
|
103
|
+
*/ export function assertFromEnum(value, enumObj) {
|
|
104
|
+
// Check if enumObj is a branded enum
|
|
105
|
+
if (!isBrandedEnum(enumObj)) {
|
|
106
|
+
throw new Error('Second argument is not a branded enum');
|
|
107
|
+
}
|
|
108
|
+
// Check if value is in the enum
|
|
109
|
+
if (!isFromEnum(value, enumObj)) {
|
|
110
|
+
const enumId = enumObj[ENUM_ID];
|
|
111
|
+
throw new Error(`Value "${value}" is not a member of enum "${enumId}"`);
|
|
112
|
+
}
|
|
113
|
+
return value;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Safely parses a value against a branded enum, returning a default if invalid.
|
|
117
|
+
*
|
|
118
|
+
* This is a non-throwing alternative to `assertFromEnum`. Instead of throwing
|
|
119
|
+
* an error when the value is not in the enum, it returns the provided default value.
|
|
120
|
+
*
|
|
121
|
+
* Use this function when you want to handle invalid values gracefully without
|
|
122
|
+
* try/catch blocks, such as when parsing user input or external data.
|
|
123
|
+
*
|
|
124
|
+
* @template E - The branded enum type
|
|
125
|
+
* @param value - The value to parse. Can be any type; non-strings will return the default.
|
|
126
|
+
* @param enumObj - The branded enum to validate against
|
|
127
|
+
* @param defaultValue - The value to return if parsing fails. Must be a valid enum value.
|
|
128
|
+
* @returns The original value if it exists in the enum, otherwise the default value.
|
|
129
|
+
* The return type is narrowed to the enum's value type.
|
|
130
|
+
*
|
|
131
|
+
* @example
|
|
132
|
+
* // Basic usage with default fallback
|
|
133
|
+
* const Status = createBrandedEnum('status', {
|
|
134
|
+
* Active: 'active',
|
|
135
|
+
* Inactive: 'inactive',
|
|
136
|
+
* Pending: 'pending',
|
|
137
|
+
* } as const);
|
|
138
|
+
*
|
|
139
|
+
* // Valid value returns as-is
|
|
140
|
+
* parseEnum('active', Status, Status.Pending); // 'active'
|
|
141
|
+
*
|
|
142
|
+
* // Invalid value returns default
|
|
143
|
+
* parseEnum('unknown', Status, Status.Pending); // 'pending'
|
|
144
|
+
*
|
|
145
|
+
* // Non-string value returns default
|
|
146
|
+
* parseEnum(null, Status, Status.Inactive); // 'inactive'
|
|
147
|
+
* parseEnum(123, Status, Status.Inactive); // 'inactive'
|
|
148
|
+
*
|
|
149
|
+
* @example
|
|
150
|
+
* // Parsing user input safely
|
|
151
|
+
* function handleUserStatus(input: unknown): void {
|
|
152
|
+
* const status = parseEnum(input, Status, Status.Pending);
|
|
153
|
+
* // status is guaranteed to be 'active' | 'inactive' | 'pending'
|
|
154
|
+
* console.log('Processing status:', status);
|
|
155
|
+
* }
|
|
156
|
+
*
|
|
157
|
+
* @example
|
|
158
|
+
* // Parsing API response with fallback
|
|
159
|
+
* interface ApiResponse {
|
|
160
|
+
* status?: string;
|
|
161
|
+
* }
|
|
162
|
+
*
|
|
163
|
+
* function processResponse(response: ApiResponse) {
|
|
164
|
+
* const status = parseEnum(response.status, Status, Status.Inactive);
|
|
165
|
+
* // Handles undefined, null, or invalid status values gracefully
|
|
166
|
+
* return { status };
|
|
167
|
+
* }
|
|
168
|
+
*
|
|
169
|
+
* @example
|
|
170
|
+
* // Chaining with optional values
|
|
171
|
+
* const userStatus = parseEnum(
|
|
172
|
+
* localStorage.getItem('userStatus'),
|
|
173
|
+
* Status,
|
|
174
|
+
* Status.Active
|
|
175
|
+
* );
|
|
176
|
+
*/ export function parseEnum(value, enumObj, defaultValue) {
|
|
177
|
+
// If the value is valid, return it
|
|
178
|
+
if (isFromEnum(value, enumObj)) {
|
|
179
|
+
return value;
|
|
180
|
+
}
|
|
181
|
+
// Otherwise return the default
|
|
182
|
+
return defaultValue;
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Safely parses a value against a branded enum, returning a result object.
|
|
186
|
+
*
|
|
187
|
+
* This function provides validated deserialization with detailed error information.
|
|
188
|
+
* Unlike `parseEnum` which returns a default value on failure, or `assertFromEnum`
|
|
189
|
+
* which throws an error, `safeParseEnum` returns a discriminated union result
|
|
190
|
+
* that allows for explicit success/failure handling.
|
|
191
|
+
*
|
|
192
|
+
* The result is either:
|
|
193
|
+
* - `{ success: true, value: T }` - The value is valid and typed correctly
|
|
194
|
+
* - `{ success: false, error: SafeParseError }` - The value is invalid with details
|
|
195
|
+
*
|
|
196
|
+
* Error codes:
|
|
197
|
+
* - `INVALID_ENUM_OBJECT` - The enumObj is not a valid branded enum
|
|
198
|
+
* - `INVALID_VALUE_TYPE` - The value is not a string
|
|
199
|
+
* - `VALUE_NOT_IN_ENUM` - The value is a string but not in the enum
|
|
200
|
+
*
|
|
201
|
+
* @template E - The branded enum type
|
|
202
|
+
* @param value - The value to parse. Can be any type.
|
|
203
|
+
* @param enumObj - The branded enum to validate against
|
|
204
|
+
* @returns A SafeParseResult containing either the validated value or error details
|
|
205
|
+
*
|
|
206
|
+
* @example
|
|
207
|
+
* // Basic usage with success
|
|
208
|
+
* const Status = createBrandedEnum('status', {
|
|
209
|
+
* Active: 'active',
|
|
210
|
+
* Inactive: 'inactive',
|
|
211
|
+
* } as const);
|
|
212
|
+
*
|
|
213
|
+
* const result = safeParseEnum('active', Status);
|
|
214
|
+
* if (result.success) {
|
|
215
|
+
* console.log('Valid status:', result.value); // 'active'
|
|
216
|
+
* } else {
|
|
217
|
+
* console.log('Error:', result.error.message);
|
|
218
|
+
* }
|
|
219
|
+
*
|
|
220
|
+
* @example
|
|
221
|
+
* // Handling invalid value
|
|
222
|
+
* const result = safeParseEnum('unknown', Status);
|
|
223
|
+
* if (!result.success) {
|
|
224
|
+
* console.log(result.error.code); // 'VALUE_NOT_IN_ENUM'
|
|
225
|
+
* console.log(result.error.message); // 'Value "unknown" is not a member of enum "status"'
|
|
226
|
+
* console.log(result.error.validValues); // ['active', 'inactive']
|
|
227
|
+
* }
|
|
228
|
+
*
|
|
229
|
+
* @example
|
|
230
|
+
* // Handling non-string input
|
|
231
|
+
* const result = safeParseEnum(123, Status);
|
|
232
|
+
* if (!result.success) {
|
|
233
|
+
* console.log(result.error.code); // 'INVALID_VALUE_TYPE'
|
|
234
|
+
* console.log(result.error.message); // 'Expected a string value, received number'
|
|
235
|
+
* }
|
|
236
|
+
*
|
|
237
|
+
* @example
|
|
238
|
+
* // Parsing API response
|
|
239
|
+
* interface ApiResponse {
|
|
240
|
+
* status?: string;
|
|
241
|
+
* }
|
|
242
|
+
*
|
|
243
|
+
* function processResponse(response: ApiResponse) {
|
|
244
|
+
* const result = safeParseEnum(response.status, Status);
|
|
245
|
+
* if (result.success) {
|
|
246
|
+
* return { status: result.value };
|
|
247
|
+
* } else {
|
|
248
|
+
* // Log detailed error for debugging
|
|
249
|
+
* console.error('Invalid status:', result.error);
|
|
250
|
+
* return { status: Status.Inactive }; // fallback
|
|
251
|
+
* }
|
|
252
|
+
* }
|
|
253
|
+
*
|
|
254
|
+
* @example
|
|
255
|
+
* // Form validation with detailed errors
|
|
256
|
+
* function validateForm(data: Record<string, unknown>) {
|
|
257
|
+
* const statusResult = safeParseEnum(data.status, Status);
|
|
258
|
+
* const errors: string[] = [];
|
|
259
|
+
*
|
|
260
|
+
* if (!statusResult.success) {
|
|
261
|
+
* errors.push(`Status: ${statusResult.error.message}`);
|
|
262
|
+
* }
|
|
263
|
+
*
|
|
264
|
+
* return {
|
|
265
|
+
* isValid: errors.length === 0,
|
|
266
|
+
* errors,
|
|
267
|
+
* data: statusResult.success ? { status: statusResult.value } : null,
|
|
268
|
+
* };
|
|
269
|
+
* }
|
|
270
|
+
*
|
|
271
|
+
* @example
|
|
272
|
+
* // Type narrowing with result
|
|
273
|
+
* const result = safeParseEnum(userInput, Status);
|
|
274
|
+
* if (result.success) {
|
|
275
|
+
* // result.value is typed as 'active' | 'inactive'
|
|
276
|
+
* handleStatus(result.value);
|
|
277
|
+
* } else {
|
|
278
|
+
* // result.error is typed as SafeParseError
|
|
279
|
+
* showError(result.error.message);
|
|
280
|
+
* }
|
|
281
|
+
*/ export function safeParseEnum(value, enumObj) {
|
|
282
|
+
// Check if enumObj is a branded enum
|
|
283
|
+
if (!isBrandedEnum(enumObj)) {
|
|
284
|
+
return {
|
|
285
|
+
success: false,
|
|
286
|
+
error: {
|
|
287
|
+
message: 'Second argument is not a branded enum',
|
|
288
|
+
code: 'INVALID_ENUM_OBJECT',
|
|
289
|
+
input: value
|
|
290
|
+
}
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
const enumId = enumObj[ENUM_ID];
|
|
294
|
+
const validValues = Array.from(enumObj[ENUM_VALUES]).sort();
|
|
295
|
+
// Check if value is a string
|
|
296
|
+
if (typeof value !== 'string') {
|
|
297
|
+
const valueType = value === null ? 'null' : typeof value;
|
|
298
|
+
return {
|
|
299
|
+
success: false,
|
|
300
|
+
error: {
|
|
301
|
+
message: `Expected a string value, received ${valueType}`,
|
|
302
|
+
code: 'INVALID_VALUE_TYPE',
|
|
303
|
+
input: value,
|
|
304
|
+
enumId,
|
|
305
|
+
validValues
|
|
306
|
+
}
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
// Check if value is in the enum
|
|
310
|
+
if (!enumObj[ENUM_VALUES].has(value)) {
|
|
311
|
+
return {
|
|
312
|
+
success: false,
|
|
313
|
+
error: {
|
|
314
|
+
message: `Value "${value}" is not a member of enum "${enumId}"`,
|
|
315
|
+
code: 'VALUE_NOT_IN_ENUM',
|
|
316
|
+
input: value,
|
|
317
|
+
enumId,
|
|
318
|
+
validValues
|
|
319
|
+
}
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
// Success case
|
|
323
|
+
return {
|
|
324
|
+
success: true,
|
|
325
|
+
value: value
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
//# sourceMappingURL=guards.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/lib/guards.ts"],"sourcesContent":["/**\n * Type guards for branded enums.\n *\n * Provides runtime type checking and assertion functions\n * for validating values against branded enums.\n */\n\nimport {\n AnyBrandedEnum,\n BrandedEnum,\n BrandedEnumValue,\n ENUM_ID,\n ENUM_VALUES,\n EnumValues,\n} from './types.js';\n\n/**\n * Checks if an object is a branded enum (has Symbol metadata).\n *\n * @param obj - The object to check\n * @returns true if obj is a branded enum\n */\nfunction isBrandedEnum(obj: unknown): obj is AnyBrandedEnum {\n return (\n obj !== null &&\n typeof obj === 'object' &&\n ENUM_ID in obj &&\n ENUM_VALUES in obj &&\n typeof (obj as AnyBrandedEnum)[ENUM_ID] === 'string' &&\n (obj as AnyBrandedEnum)[ENUM_VALUES] instanceof Set\n );\n}\n\n/**\n * Checks if a value belongs to a specific branded enum.\n *\n * Returns true if and only if:\n * - enumObj is a valid branded enum (has Symbol metadata)\n * - value is a string\n * - value exists in the enum's value Set\n *\n * This function provides TypeScript type narrowing - when it returns true,\n * the value's type is narrowed to the enum's value type.\n *\n * @template E - The branded enum type\n * @param value - The value to check. Can be any type; non-strings return false.\n * @param enumObj - The branded enum to check against\n * @returns `true` if value is in the enum (with type narrowing), `false` otherwise.\n * Returns `false` for non-string values, null, undefined, or if enumObj\n * is not a branded enum.\n *\n * @example\n * // Basic type guard usage\n * const Status = createBrandedEnum('status', { Active: 'active', Inactive: 'inactive' } as const);\n *\n * function handleStatus(value: unknown) {\n * if (isFromEnum(value, Status)) {\n * // value is narrowed to 'active' | 'inactive'\n * console.log('Valid status:', value);\n * } else {\n * console.log('Invalid status');\n * }\n * }\n *\n * @example\n * // Returns false for non-string values\n * isFromEnum(123, Status); // false\n * isFromEnum(null, Status); // false\n * isFromEnum(undefined, Status); // false\n *\n * @example\n * // Returns false for non-branded enum objects\n * isFromEnum('active', { Active: 'active' }); // false (not a branded enum)\n */\nexport function isFromEnum<E extends AnyBrandedEnum>(\n value: unknown,\n enumObj: E\n): value is EnumValues<E> {\n // Return false for non-string values\n if (typeof value !== 'string') {\n return false;\n }\n\n // Return false if enumObj is not a branded enum\n if (!isBrandedEnum(enumObj)) {\n return false;\n }\n\n // Check if value exists in enum's value Set\n return enumObj[ENUM_VALUES].has(value);\n}\n\n/**\n * Asserts that a value belongs to a branded enum, throwing if not.\n *\n * Use this function when you want to validate a value and throw an error\n * if it's invalid, rather than handling the false case manually.\n *\n * @template E - The branded enum type\n * @param value - The value to check. Can be any type.\n * @param enumObj - The branded enum to check against\n * @returns The value with narrowed type if valid\n * @throws {Error} Throws `Error` with message `Second argument is not a branded enum`\n * if enumObj is not a valid branded enum.\n * @throws {Error} Throws `Error` with message `Value \"${value}\" is not a member of enum \"${enumId}\"`\n * if the value is not found in the enum.\n *\n * @example\n * // Successful assertion\n * const Status = createBrandedEnum('status', { Active: 'active', Inactive: 'inactive' } as const);\n * const validated = assertFromEnum('active', Status);\n * // validated is typed as 'active' | 'inactive'\n *\n * @example\n * // Throws for invalid value\n * try {\n * assertFromEnum('unknown', Status);\n * } catch (e) {\n * console.log(e.message); // 'Value \"unknown\" is not a member of enum \"status\"'\n * }\n *\n * @example\n * // Throws for non-branded enum\n * try {\n * assertFromEnum('active', { Active: 'active' });\n * } catch (e) {\n * console.log(e.message); // 'Second argument is not a branded enum'\n * }\n */\nexport function assertFromEnum<E extends AnyBrandedEnum>(\n value: unknown,\n enumObj: E\n): EnumValues<E> {\n // Check if enumObj is a branded enum\n if (!isBrandedEnum(enumObj)) {\n throw new Error('Second argument is not a branded enum');\n }\n\n // Check if value is in the enum\n if (!isFromEnum(value, enumObj)) {\n const enumId = enumObj[ENUM_ID];\n throw new Error(`Value \"${value}\" is not a member of enum \"${enumId}\"`);\n }\n\n return value as EnumValues<E>;\n}\n\n/**\n * Safely parses a value against a branded enum, returning a default if invalid.\n *\n * This is a non-throwing alternative to `assertFromEnum`. Instead of throwing\n * an error when the value is not in the enum, it returns the provided default value.\n *\n * Use this function when you want to handle invalid values gracefully without\n * try/catch blocks, such as when parsing user input or external data.\n *\n * @template E - The branded enum type\n * @param value - The value to parse. Can be any type; non-strings will return the default.\n * @param enumObj - The branded enum to validate against\n * @param defaultValue - The value to return if parsing fails. Must be a valid enum value.\n * @returns The original value if it exists in the enum, otherwise the default value.\n * The return type is narrowed to the enum's value type.\n *\n * @example\n * // Basic usage with default fallback\n * const Status = createBrandedEnum('status', {\n * Active: 'active',\n * Inactive: 'inactive',\n * Pending: 'pending',\n * } as const);\n *\n * // Valid value returns as-is\n * parseEnum('active', Status, Status.Pending); // 'active'\n *\n * // Invalid value returns default\n * parseEnum('unknown', Status, Status.Pending); // 'pending'\n *\n * // Non-string value returns default\n * parseEnum(null, Status, Status.Inactive); // 'inactive'\n * parseEnum(123, Status, Status.Inactive); // 'inactive'\n *\n * @example\n * // Parsing user input safely\n * function handleUserStatus(input: unknown): void {\n * const status = parseEnum(input, Status, Status.Pending);\n * // status is guaranteed to be 'active' | 'inactive' | 'pending'\n * console.log('Processing status:', status);\n * }\n *\n * @example\n * // Parsing API response with fallback\n * interface ApiResponse {\n * status?: string;\n * }\n *\n * function processResponse(response: ApiResponse) {\n * const status = parseEnum(response.status, Status, Status.Inactive);\n * // Handles undefined, null, or invalid status values gracefully\n * return { status };\n * }\n *\n * @example\n * // Chaining with optional values\n * const userStatus = parseEnum(\n * localStorage.getItem('userStatus'),\n * Status,\n * Status.Active\n * );\n */\nexport function parseEnum<E extends AnyBrandedEnum>(\n value: unknown,\n enumObj: E,\n defaultValue: EnumValues<E>\n): EnumValues<E> {\n // If the value is valid, return it\n if (isFromEnum(value, enumObj)) {\n return value;\n }\n\n // Otherwise return the default\n return defaultValue;\n}\n\n// =============================================================================\n// Safe Parse Result Types\n// =============================================================================\n\n/**\n * Represents a successful parse result from safeParseEnum.\n *\n * @template T - The type of the successfully parsed value\n */\nexport interface SafeParseSuccess<T> {\n /** Indicates the parse was successful */\n readonly success: true;\n /** The validated enum value */\n readonly value: T;\n}\n\n/**\n * Represents a failed parse result from safeParseEnum.\n *\n * Contains detailed error information for debugging and user feedback.\n */\nexport interface SafeParseFailure {\n /** Indicates the parse failed */\n readonly success: false;\n /** Detailed error information */\n readonly error: SafeParseError;\n}\n\n/**\n * Detailed error information for a failed parse.\n */\nexport interface SafeParseError {\n /** Human-readable error message */\n readonly message: string;\n /** The code identifying the type of error */\n readonly code: SafeParseErrorCode;\n /** The input value that failed validation */\n readonly input: unknown;\n /** The enum ID (if available) */\n readonly enumId?: string;\n /** The valid values for the enum (if available) */\n readonly validValues?: readonly string[];\n}\n\n/**\n * Error codes for safe parse failures.\n */\nexport type SafeParseErrorCode =\n | 'INVALID_ENUM_OBJECT'\n | 'INVALID_VALUE_TYPE'\n | 'VALUE_NOT_IN_ENUM';\n\n/**\n * Union type representing the result of safeParseEnum.\n *\n * @template T - The type of the successfully parsed value\n */\nexport type SafeParseResult<T> = SafeParseSuccess<T> | SafeParseFailure;\n\n/**\n * Safely parses a value against a branded enum, returning a result object.\n *\n * This function provides validated deserialization with detailed error information.\n * Unlike `parseEnum` which returns a default value on failure, or `assertFromEnum`\n * which throws an error, `safeParseEnum` returns a discriminated union result\n * that allows for explicit success/failure handling.\n *\n * The result is either:\n * - `{ success: true, value: T }` - The value is valid and typed correctly\n * - `{ success: false, error: SafeParseError }` - The value is invalid with details\n *\n * Error codes:\n * - `INVALID_ENUM_OBJECT` - The enumObj is not a valid branded enum\n * - `INVALID_VALUE_TYPE` - The value is not a string\n * - `VALUE_NOT_IN_ENUM` - The value is a string but not in the enum\n *\n * @template E - The branded enum type\n * @param value - The value to parse. Can be any type.\n * @param enumObj - The branded enum to validate against\n * @returns A SafeParseResult containing either the validated value or error details\n *\n * @example\n * // Basic usage with success\n * const Status = createBrandedEnum('status', {\n * Active: 'active',\n * Inactive: 'inactive',\n * } as const);\n *\n * const result = safeParseEnum('active', Status);\n * if (result.success) {\n * console.log('Valid status:', result.value); // 'active'\n * } else {\n * console.log('Error:', result.error.message);\n * }\n *\n * @example\n * // Handling invalid value\n * const result = safeParseEnum('unknown', Status);\n * if (!result.success) {\n * console.log(result.error.code); // 'VALUE_NOT_IN_ENUM'\n * console.log(result.error.message); // 'Value \"unknown\" is not a member of enum \"status\"'\n * console.log(result.error.validValues); // ['active', 'inactive']\n * }\n *\n * @example\n * // Handling non-string input\n * const result = safeParseEnum(123, Status);\n * if (!result.success) {\n * console.log(result.error.code); // 'INVALID_VALUE_TYPE'\n * console.log(result.error.message); // 'Expected a string value, received number'\n * }\n *\n * @example\n * // Parsing API response\n * interface ApiResponse {\n * status?: string;\n * }\n *\n * function processResponse(response: ApiResponse) {\n * const result = safeParseEnum(response.status, Status);\n * if (result.success) {\n * return { status: result.value };\n * } else {\n * // Log detailed error for debugging\n * console.error('Invalid status:', result.error);\n * return { status: Status.Inactive }; // fallback\n * }\n * }\n *\n * @example\n * // Form validation with detailed errors\n * function validateForm(data: Record<string, unknown>) {\n * const statusResult = safeParseEnum(data.status, Status);\n * const errors: string[] = [];\n *\n * if (!statusResult.success) {\n * errors.push(`Status: ${statusResult.error.message}`);\n * }\n *\n * return {\n * isValid: errors.length === 0,\n * errors,\n * data: statusResult.success ? { status: statusResult.value } : null,\n * };\n * }\n *\n * @example\n * // Type narrowing with result\n * const result = safeParseEnum(userInput, Status);\n * if (result.success) {\n * // result.value is typed as 'active' | 'inactive'\n * handleStatus(result.value);\n * } else {\n * // result.error is typed as SafeParseError\n * showError(result.error.message);\n * }\n */\nexport function safeParseEnum<E extends AnyBrandedEnum>(\n value: unknown,\n enumObj: E\n): SafeParseResult<EnumValues<E>> {\n // Check if enumObj is a branded enum\n if (!isBrandedEnum(enumObj)) {\n return {\n success: false,\n error: {\n message: 'Second argument is not a branded enum',\n code: 'INVALID_ENUM_OBJECT',\n input: value,\n },\n };\n }\n\n const enumId = enumObj[ENUM_ID];\n const validValues = Array.from(enumObj[ENUM_VALUES]).sort();\n\n // Check if value is a string\n if (typeof value !== 'string') {\n const valueType = value === null ? 'null' : typeof value;\n return {\n success: false,\n error: {\n message: `Expected a string value, received ${valueType}`,\n code: 'INVALID_VALUE_TYPE',\n input: value,\n enumId,\n validValues,\n },\n };\n }\n\n // Check if value is in the enum\n if (!enumObj[ENUM_VALUES].has(value)) {\n return {\n success: false,\n error: {\n message: `Value \"${value}\" is not a member of enum \"${enumId}\"`,\n code: 'VALUE_NOT_IN_ENUM',\n input: value,\n enumId,\n validValues,\n },\n };\n }\n\n // Success case\n return {\n success: true,\n value: value as EnumValues<E>,\n };\n}\n"],"names":["ENUM_ID","ENUM_VALUES","isBrandedEnum","obj","Set","isFromEnum","value","enumObj","has","assertFromEnum","Error","enumId","parseEnum","defaultValue","safeParseEnum","success","error","message","code","input","validValues","Array","from","sort","valueType"],"rangeMappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;","mappings":"AAAA;;;;;CAKC,GAED,SAIEA,OAAO,EACPC,WAAW,QAEN,aAAa;AAEpB;;;;;CAKC,GACD,SAASC,cAAcC,GAAY;IACjC,OACEA,QAAQ,QACR,OAAOA,QAAQ,YACfH,WAAWG,OACXF,eAAeE,OACf,OAAO,AAACA,GAAsB,CAACH,QAAQ,KAAK,YAC5C,AAACG,GAAsB,CAACF,YAAY,YAAYG;AAEpD;AAEA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAwCC,GACD,OAAO,SAASC,WACdC,KAAc,EACdC,OAAU;IAEV,qCAAqC;IACrC,IAAI,OAAOD,UAAU,UAAU;QAC7B,OAAO;IACT;IAEA,gDAAgD;IAChD,IAAI,CAACJ,cAAcK,UAAU;QAC3B,OAAO;IACT;IAEA,4CAA4C;IAC5C,OAAOA,OAAO,CAACN,YAAY,CAACO,GAAG,CAACF;AAClC;AAEA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAoCC,GACD,OAAO,SAASG,eACdH,KAAc,EACdC,OAAU;IAEV,qCAAqC;IACrC,IAAI,CAACL,cAAcK,UAAU;QAC3B,MAAM,IAAIG,MAAM;IAClB;IAEA,gCAAgC;IAChC,IAAI,CAACL,WAAWC,OAAOC,UAAU;QAC/B,MAAMI,SAASJ,OAAO,CAACP,QAAQ;QAC/B,MAAM,IAAIU,MAAM,CAAC,OAAO,EAAEJ,MAAM,2BAA2B,EAAEK,OAAO,CAAC,CAAC;IACxE;IAEA,OAAOL;AACT;AAEA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA6DC,GACD,OAAO,SAASM,UACdN,KAAc,EACdC,OAAU,EACVM,YAA2B;IAE3B,mCAAmC;IACnC,IAAIR,WAAWC,OAAOC,UAAU;QAC9B,OAAOD;IACT;IAEA,+BAA+B;IAC/B,OAAOO;AACT;AA6DA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAiGC,GACD,OAAO,SAASC,cACdR,KAAc,EACdC,OAAU;IAEV,qCAAqC;IACrC,IAAI,CAACL,cAAcK,UAAU;QAC3B,OAAO;YACLQ,SAAS;YACTC,OAAO;gBACLC,SAAS;gBACTC,MAAM;gBACNC,OAAOb;YACT;QACF;IACF;IAEA,MAAMK,SAASJ,OAAO,CAACP,QAAQ;IAC/B,MAAMoB,cAAcC,MAAMC,IAAI,CAACf,OAAO,CAACN,YAAY,EAAEsB,IAAI;IAEzD,6BAA6B;IAC7B,IAAI,OAAOjB,UAAU,UAAU;QAC7B,MAAMkB,YAAYlB,UAAU,OAAO,SAAS,OAAOA;QACnD,OAAO;YACLS,SAAS;YACTC,OAAO;gBACLC,SAAS,CAAC,kCAAkC,EAAEO,UAAU,CAAC;gBACzDN,MAAM;gBACNC,OAAOb;gBACPK;gBACAS;YACF;QACF;IACF;IAEA,gCAAgC;IAChC,IAAI,CAACb,OAAO,CAACN,YAAY,CAACO,GAAG,CAACF,QAAQ;QACpC,OAAO;YACLS,SAAS;YACTC,OAAO;gBACLC,SAAS,CAAC,OAAO,EAAEX,MAAM,2BAA2B,EAAEK,OAAO,CAAC,CAAC;gBAC/DO,MAAM;gBACNC,OAAOb;gBACPK;gBACAS;YACF;QACF;IACF;IAEA,eAAe;IACf,OAAO;QACLL,SAAS;QACTT,OAAOA;IACT;AACF"}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Enum composition functions for branded enums.
|
|
3
|
+
*
|
|
4
|
+
* Enables merging multiple branded enums into a new combined enum
|
|
5
|
+
* while maintaining type safety and registry tracking.
|
|
6
|
+
*/ import { ENUM_ID, ENUM_VALUES } from './types.js';
|
|
7
|
+
import { createBrandedEnum } from './factory.js';
|
|
8
|
+
/**
|
|
9
|
+
* Checks if an object is a branded enum.
|
|
10
|
+
*/ function isBrandedEnum(obj) {
|
|
11
|
+
return typeof obj === 'object' && obj !== null && ENUM_ID in obj && ENUM_VALUES in obj;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Merges multiple branded enums into a new branded enum.
|
|
15
|
+
*
|
|
16
|
+
* Creates a new branded enum that contains all key-value pairs from all
|
|
17
|
+
* source enums. The merged enum is registered in the global registry
|
|
18
|
+
* as a new independent enum.
|
|
19
|
+
*
|
|
20
|
+
* Key collision handling:
|
|
21
|
+
* - Duplicate keys (same key in multiple enums) throw an error
|
|
22
|
+
* - Duplicate values (same value in multiple enums) are allowed
|
|
23
|
+
*
|
|
24
|
+
* @template T - Tuple of branded enum types being merged
|
|
25
|
+
* @param newId - Unique identifier for the merged enum. Must not already
|
|
26
|
+
* be registered.
|
|
27
|
+
* @param enums - One or more branded enums to merge
|
|
28
|
+
* @returns A new branded enum containing all values from source enums
|
|
29
|
+
* @throws {Error} Throws `Error` with message
|
|
30
|
+
* `Cannot merge enums: duplicate key "${key}" found in enums "${enumId1}" and "${enumId2}"`
|
|
31
|
+
* if the same key exists in multiple source enums.
|
|
32
|
+
* @throws {Error} Throws `Error` with message
|
|
33
|
+
* `Branded enum with ID "${newId}" already exists` if newId is already registered.
|
|
34
|
+
* @throws {Error} Throws `Error` with message `All arguments must be branded enums`
|
|
35
|
+
* if any argument is not a valid branded enum.
|
|
36
|
+
*
|
|
37
|
+
* @example
|
|
38
|
+
* // Basic merge
|
|
39
|
+
* const Colors = createBrandedEnum('colors', { Red: 'red', Blue: 'blue' } as const);
|
|
40
|
+
* const Sizes = createBrandedEnum('sizes', { Small: 'small', Large: 'large' } as const);
|
|
41
|
+
*
|
|
42
|
+
* const Combined = mergeEnums('combined', Colors, Sizes);
|
|
43
|
+
* // Combined has: Red, Blue, Small, Large
|
|
44
|
+
*
|
|
45
|
+
* Combined.Red; // 'red'
|
|
46
|
+
* Combined.Small; // 'small'
|
|
47
|
+
*
|
|
48
|
+
* @example
|
|
49
|
+
* // Duplicate values are allowed
|
|
50
|
+
* const Status1 = createBrandedEnum('status1', { Active: 'active' } as const);
|
|
51
|
+
* const Status2 = createBrandedEnum('status2', { Enabled: 'active' } as const);
|
|
52
|
+
*
|
|
53
|
+
* const Merged = mergeEnums('merged', Status1, Status2);
|
|
54
|
+
* // Both Active and Enabled have value 'active' - this is allowed
|
|
55
|
+
*
|
|
56
|
+
* @example
|
|
57
|
+
* // Duplicate keys throw an error
|
|
58
|
+
* const Enum1 = createBrandedEnum('enum1', { Key: 'value1' } as const);
|
|
59
|
+
* const Enum2 = createBrandedEnum('enum2', { Key: 'value2' } as const);
|
|
60
|
+
*
|
|
61
|
+
* try {
|
|
62
|
+
* mergeEnums('merged', Enum1, Enum2);
|
|
63
|
+
* } catch (e) {
|
|
64
|
+
* console.log(e.message);
|
|
65
|
+
* // 'Cannot merge enums: duplicate key "Key" found in enums "enum1" and "enum2"'
|
|
66
|
+
* }
|
|
67
|
+
*/ export function mergeEnums(newId, ...enums) {
|
|
68
|
+
// Collect all key-value pairs, checking for duplicate keys
|
|
69
|
+
const mergedValues = {};
|
|
70
|
+
const seenKeys = new Map(); // key -> source enumId
|
|
71
|
+
for (const enumObj of enums){
|
|
72
|
+
if (!isBrandedEnum(enumObj)) {
|
|
73
|
+
throw new Error('All arguments must be branded enums');
|
|
74
|
+
}
|
|
75
|
+
const sourceEnumId = enumObj[ENUM_ID];
|
|
76
|
+
// Iterate over enumerable properties (user-defined keys only)
|
|
77
|
+
for (const [key, value] of Object.entries(enumObj)){
|
|
78
|
+
// Check for duplicate keys
|
|
79
|
+
if (seenKeys.has(key)) {
|
|
80
|
+
const originalEnumId = seenKeys.get(key);
|
|
81
|
+
throw new Error(`Cannot merge enums: duplicate key "${key}" found in enums "${originalEnumId}" and "${sourceEnumId}"`);
|
|
82
|
+
}
|
|
83
|
+
seenKeys.set(key, sourceEnumId);
|
|
84
|
+
mergedValues[key] = value;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
// Create and return the new branded enum (this handles registration)
|
|
88
|
+
return createBrandedEnum(newId, mergedValues);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
//# sourceMappingURL=merge.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/lib/merge.ts"],"sourcesContent":["/**\n * Enum composition functions for branded enums.\n *\n * Enables merging multiple branded enums into a new combined enum\n * while maintaining type safety and registry tracking.\n */\n\nimport { AnyBrandedEnum, BrandedEnum, ENUM_ID, ENUM_VALUES } from './types.js';\nimport { createBrandedEnum } from './factory.js';\n\n/**\n * Type helper to extract the values type from a branded enum.\n */\ntype ExtractValues<E> = E extends BrandedEnum<infer T> ? T : never;\n\n/**\n * Type helper to merge multiple branded enum value types into one.\n */\ntype MergedValues<T extends AnyBrandedEnum[]> = {\n [K in keyof T]: ExtractValues<T[K]>;\n}[number];\n\n/**\n * Checks if an object is a branded enum.\n */\nfunction isBrandedEnum(obj: unknown): obj is AnyBrandedEnum {\n return (\n typeof obj === 'object' &&\n obj !== null &&\n ENUM_ID in obj &&\n ENUM_VALUES in obj\n );\n}\n\n/**\n * Merges multiple branded enums into a new branded enum.\n *\n * Creates a new branded enum that contains all key-value pairs from all\n * source enums. The merged enum is registered in the global registry\n * as a new independent enum.\n *\n * Key collision handling:\n * - Duplicate keys (same key in multiple enums) throw an error\n * - Duplicate values (same value in multiple enums) are allowed\n *\n * @template T - Tuple of branded enum types being merged\n * @param newId - Unique identifier for the merged enum. Must not already\n * be registered.\n * @param enums - One or more branded enums to merge\n * @returns A new branded enum containing all values from source enums\n * @throws {Error} Throws `Error` with message\n * `Cannot merge enums: duplicate key \"${key}\" found in enums \"${enumId1}\" and \"${enumId2}\"`\n * if the same key exists in multiple source enums.\n * @throws {Error} Throws `Error` with message\n * `Branded enum with ID \"${newId}\" already exists` if newId is already registered.\n * @throws {Error} Throws `Error` with message `All arguments must be branded enums`\n * if any argument is not a valid branded enum.\n *\n * @example\n * // Basic merge\n * const Colors = createBrandedEnum('colors', { Red: 'red', Blue: 'blue' } as const);\n * const Sizes = createBrandedEnum('sizes', { Small: 'small', Large: 'large' } as const);\n *\n * const Combined = mergeEnums('combined', Colors, Sizes);\n * // Combined has: Red, Blue, Small, Large\n *\n * Combined.Red; // 'red'\n * Combined.Small; // 'small'\n *\n * @example\n * // Duplicate values are allowed\n * const Status1 = createBrandedEnum('status1', { Active: 'active' } as const);\n * const Status2 = createBrandedEnum('status2', { Enabled: 'active' } as const);\n *\n * const Merged = mergeEnums('merged', Status1, Status2);\n * // Both Active and Enabled have value 'active' - this is allowed\n *\n * @example\n * // Duplicate keys throw an error\n * const Enum1 = createBrandedEnum('enum1', { Key: 'value1' } as const);\n * const Enum2 = createBrandedEnum('enum2', { Key: 'value2' } as const);\n *\n * try {\n * mergeEnums('merged', Enum1, Enum2);\n * } catch (e) {\n * console.log(e.message);\n * // 'Cannot merge enums: duplicate key \"Key\" found in enums \"enum1\" and \"enum2\"'\n * }\n */\nexport function mergeEnums<T extends readonly AnyBrandedEnum[]>(\n newId: string,\n ...enums: T\n): BrandedEnum<Record<string, string>> {\n // Collect all key-value pairs, checking for duplicate keys\n const mergedValues: Record<string, string> = {};\n const seenKeys = new Map<string, string>(); // key -> source enumId\n\n for (const enumObj of enums) {\n if (!isBrandedEnum(enumObj)) {\n throw new Error('All arguments must be branded enums');\n }\n\n const sourceEnumId = enumObj[ENUM_ID];\n\n // Iterate over enumerable properties (user-defined keys only)\n for (const [key, value] of Object.entries(enumObj)) {\n // Check for duplicate keys\n if (seenKeys.has(key)) {\n const originalEnumId = seenKeys.get(key);\n throw new Error(\n `Cannot merge enums: duplicate key \"${key}\" found in enums \"${originalEnumId}\" and \"${sourceEnumId}\"`\n );\n }\n\n seenKeys.set(key, sourceEnumId);\n mergedValues[key] = value as string;\n }\n }\n\n // Create and return the new branded enum (this handles registration)\n return createBrandedEnum(newId, mergedValues) as BrandedEnum<MergedValues<[...T]>>;\n}\n"],"names":["ENUM_ID","ENUM_VALUES","createBrandedEnum","isBrandedEnum","obj","mergeEnums","newId","enums","mergedValues","seenKeys","Map","enumObj","Error","sourceEnumId","key","value","Object","entries","has","originalEnumId","get","set"],"rangeMappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;","mappings":"AAAA;;;;;CAKC,GAED,SAAsCA,OAAO,EAAEC,WAAW,QAAQ,aAAa;AAC/E,SAASC,iBAAiB,QAAQ,eAAe;AAcjD;;CAEC,GACD,SAASC,cAAcC,GAAY;IACjC,OACE,OAAOA,QAAQ,YACfA,QAAQ,QACRJ,WAAWI,OACXH,eAAeG;AAEnB;AAEA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAsDC,GACD,OAAO,SAASC,WACdC,KAAa,EACb,GAAGC,KAAQ;IAEX,2DAA2D;IAC3D,MAAMC,eAAuC,CAAC;IAC9C,MAAMC,WAAW,IAAIC,OAAuB,uBAAuB;IAEnE,KAAK,MAAMC,WAAWJ,MAAO;QAC3B,IAAI,CAACJ,cAAcQ,UAAU;YAC3B,MAAM,IAAIC,MAAM;QAClB;QAEA,MAAMC,eAAeF,OAAO,CAACX,QAAQ;QAErC,8DAA8D;QAC9D,KAAK,MAAM,CAACc,KAAKC,MAAM,IAAIC,OAAOC,OAAO,CAACN,SAAU;YAClD,2BAA2B;YAC3B,IAAIF,SAASS,GAAG,CAACJ,MAAM;gBACrB,MAAMK,iBAAiBV,SAASW,GAAG,CAACN;gBACpC,MAAM,IAAIF,MACR,CAAC,mCAAmC,EAAEE,IAAI,kBAAkB,EAAEK,eAAe,OAAO,EAAEN,aAAa,CAAC,CAAC;YAEzG;YAEAJ,SAASY,GAAG,CAACP,KAAKD;YAClBL,YAAY,CAACM,IAAI,GAAGC;QACtB;IACF;IAEA,qEAAqE;IACrE,OAAOb,kBAAkBI,OAAOE;AAClC"}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Global registry for branded enums.
|
|
3
|
+
*
|
|
4
|
+
* Uses globalThis to ensure cross-bundle compatibility - all instances
|
|
5
|
+
* of the library share the same registry regardless of how they're bundled.
|
|
6
|
+
*/ import { REGISTRY_KEY, ENUM_ID, ENUM_VALUES } from './types.js';
|
|
7
|
+
/**
|
|
8
|
+
* Gets the global registry, initializing it lazily if needed.
|
|
9
|
+
* Uses globalThis for cross-bundle compatibility.
|
|
10
|
+
*
|
|
11
|
+
* The registry is shared across all instances of the library, even when
|
|
12
|
+
* bundled separately or loaded as different module formats (ESM/CJS).
|
|
13
|
+
*
|
|
14
|
+
* @returns The global branded enum registry containing all registered enums
|
|
15
|
+
* and a value index for reverse lookups.
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* const registry = getRegistry();
|
|
19
|
+
* console.log(registry.enums.size); // Number of registered enums
|
|
20
|
+
*/ export function getRegistry() {
|
|
21
|
+
const global = globalThis;
|
|
22
|
+
if (!(REGISTRY_KEY in global) || !global[REGISTRY_KEY]) {
|
|
23
|
+
global[REGISTRY_KEY] = {
|
|
24
|
+
enums: new Map(),
|
|
25
|
+
valueIndex: new Map()
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
return global[REGISTRY_KEY];
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Registers a branded enum in the global registry.
|
|
32
|
+
* Also updates the value index for reverse lookups.
|
|
33
|
+
*
|
|
34
|
+
* @param enumObj - The branded enum to register
|
|
35
|
+
* @throws Error if an enum with the same ID is already registered
|
|
36
|
+
*/ export function registerEnum(enumObj) {
|
|
37
|
+
const registry = getRegistry();
|
|
38
|
+
const enumId = enumObj[ENUM_ID];
|
|
39
|
+
const values = enumObj[ENUM_VALUES];
|
|
40
|
+
// Check for duplicate ID
|
|
41
|
+
if (registry.enums.has(enumId)) {
|
|
42
|
+
throw new Error(`Branded enum with ID "${enumId}" already exists`);
|
|
43
|
+
}
|
|
44
|
+
// Create registry entry
|
|
45
|
+
const entry = {
|
|
46
|
+
enumId,
|
|
47
|
+
enumObj: enumObj,
|
|
48
|
+
values
|
|
49
|
+
};
|
|
50
|
+
// Add to enums map
|
|
51
|
+
registry.enums.set(enumId, entry);
|
|
52
|
+
// Update value index for reverse lookups
|
|
53
|
+
for (const value of values){
|
|
54
|
+
let enumIds = registry.valueIndex.get(value);
|
|
55
|
+
if (!enumIds) {
|
|
56
|
+
enumIds = new Set();
|
|
57
|
+
registry.valueIndex.set(value, enumIds);
|
|
58
|
+
}
|
|
59
|
+
enumIds.add(enumId);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Gets all registered enum IDs.
|
|
64
|
+
*
|
|
65
|
+
* Returns an array of all enum IDs that have been registered via
|
|
66
|
+
* `createBrandedEnum`. Useful for debugging or introspection.
|
|
67
|
+
*
|
|
68
|
+
* @returns Array of all registered enum IDs. Returns empty array if no
|
|
69
|
+
* enums have been registered.
|
|
70
|
+
*
|
|
71
|
+
* @example
|
|
72
|
+
* createBrandedEnum('colors', { Red: 'red' } as const);
|
|
73
|
+
* createBrandedEnum('sizes', { Small: 'small' } as const);
|
|
74
|
+
*
|
|
75
|
+
* getAllEnumIds(); // ['colors', 'sizes']
|
|
76
|
+
*/ export function getAllEnumIds() {
|
|
77
|
+
const registry = getRegistry();
|
|
78
|
+
return Array.from(registry.enums.keys());
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Gets a branded enum by its ID.
|
|
82
|
+
*
|
|
83
|
+
* Retrieves a previously registered branded enum from the global registry.
|
|
84
|
+
* Useful when you need to access an enum dynamically by its ID.
|
|
85
|
+
*
|
|
86
|
+
* @param enumId - The enum ID to look up
|
|
87
|
+
* @returns The branded enum object if found, or `undefined` if no enum
|
|
88
|
+
* with the given ID has been registered.
|
|
89
|
+
*
|
|
90
|
+
* @example
|
|
91
|
+
* const Status = createBrandedEnum('status', { Active: 'active' } as const);
|
|
92
|
+
*
|
|
93
|
+
* const retrieved = getEnumById('status');
|
|
94
|
+
* console.log(retrieved === Status); // true
|
|
95
|
+
*
|
|
96
|
+
* const notFound = getEnumById('nonexistent');
|
|
97
|
+
* console.log(notFound); // undefined
|
|
98
|
+
*/ export function getEnumById(enumId) {
|
|
99
|
+
const registry = getRegistry();
|
|
100
|
+
const entry = registry.enums.get(enumId);
|
|
101
|
+
return entry?.enumObj;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Finds all enum IDs that contain a given value.
|
|
105
|
+
*
|
|
106
|
+
* Performs a reverse lookup to find which enums contain a specific value.
|
|
107
|
+
* Useful for debugging value collisions or routing values to handlers.
|
|
108
|
+
*
|
|
109
|
+
* @param value - The string value to search for
|
|
110
|
+
* @returns Array of enum IDs that contain the value. Returns empty array
|
|
111
|
+
* if no enums contain the value.
|
|
112
|
+
*
|
|
113
|
+
* @example
|
|
114
|
+
* // Single enum containing value
|
|
115
|
+
* createBrandedEnum('colors', { Red: 'red', Blue: 'blue' } as const);
|
|
116
|
+
* findEnumSources('red'); // ['colors']
|
|
117
|
+
*
|
|
118
|
+
* @example
|
|
119
|
+
* // Multiple enums with same value (collision detection)
|
|
120
|
+
* createBrandedEnum('status1', { Active: 'active' } as const);
|
|
121
|
+
* createBrandedEnum('status2', { Enabled: 'active' } as const);
|
|
122
|
+
* findEnumSources('active'); // ['status1', 'status2']
|
|
123
|
+
*
|
|
124
|
+
* @example
|
|
125
|
+
* // Value not found
|
|
126
|
+
* findEnumSources('nonexistent'); // []
|
|
127
|
+
*/ export function findEnumSources(value) {
|
|
128
|
+
const registry = getRegistry();
|
|
129
|
+
const enumIds = registry.valueIndex.get(value);
|
|
130
|
+
return enumIds ? Array.from(enumIds) : [];
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
//# sourceMappingURL=registry.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/lib/registry.ts"],"sourcesContent":["/**\n * Global registry for branded enums.\n *\n * Uses globalThis to ensure cross-bundle compatibility - all instances\n * of the library share the same registry regardless of how they're bundled.\n */\n\nimport {\n BrandedEnum,\n BrandedEnumRegistry,\n RegistryEntry,\n REGISTRY_KEY,\n ENUM_ID,\n ENUM_VALUES,\n} from './types.js';\n\n/**\n * Gets the global registry, initializing it lazily if needed.\n * Uses globalThis for cross-bundle compatibility.\n *\n * The registry is shared across all instances of the library, even when\n * bundled separately or loaded as different module formats (ESM/CJS).\n *\n * @returns The global branded enum registry containing all registered enums\n * and a value index for reverse lookups.\n *\n * @example\n * const registry = getRegistry();\n * console.log(registry.enums.size); // Number of registered enums\n */\nexport function getRegistry(): BrandedEnumRegistry {\n const global = globalThis as typeof globalThis & {\n [REGISTRY_KEY]?: BrandedEnumRegistry;\n };\n\n if (!(REGISTRY_KEY in global) || !global[REGISTRY_KEY]) {\n global[REGISTRY_KEY] = {\n enums: new Map<string, RegistryEntry>(),\n valueIndex: new Map<string, Set<string>>(),\n };\n }\n\n return global[REGISTRY_KEY];\n}\n\n/**\n * Registers a branded enum in the global registry.\n * Also updates the value index for reverse lookups.\n *\n * @param enumObj - The branded enum to register\n * @throws Error if an enum with the same ID is already registered\n */\nexport function registerEnum<T extends Record<string, string>>(\n enumObj: BrandedEnum<T>\n): void {\n const registry = getRegistry();\n const enumId = enumObj[ENUM_ID];\n const values = enumObj[ENUM_VALUES];\n\n // Check for duplicate ID\n if (registry.enums.has(enumId)) {\n throw new Error(`Branded enum with ID \"${enumId}\" already exists`);\n }\n\n // Create registry entry\n const entry: RegistryEntry = {\n enumId,\n enumObj: enumObj as BrandedEnum<Record<string, string>>,\n values,\n };\n\n // Add to enums map\n registry.enums.set(enumId, entry);\n\n // Update value index for reverse lookups\n for (const value of values) {\n let enumIds = registry.valueIndex.get(value);\n if (!enumIds) {\n enumIds = new Set<string>();\n registry.valueIndex.set(value, enumIds);\n }\n enumIds.add(enumId);\n }\n}\n\n/**\n * Gets all registered enum IDs.\n *\n * Returns an array of all enum IDs that have been registered via\n * `createBrandedEnum`. Useful for debugging or introspection.\n *\n * @returns Array of all registered enum IDs. Returns empty array if no\n * enums have been registered.\n *\n * @example\n * createBrandedEnum('colors', { Red: 'red' } as const);\n * createBrandedEnum('sizes', { Small: 'small' } as const);\n *\n * getAllEnumIds(); // ['colors', 'sizes']\n */\nexport function getAllEnumIds(): string[] {\n const registry = getRegistry();\n return Array.from(registry.enums.keys());\n}\n\n/**\n * Gets a branded enum by its ID.\n *\n * Retrieves a previously registered branded enum from the global registry.\n * Useful when you need to access an enum dynamically by its ID.\n *\n * @param enumId - The enum ID to look up\n * @returns The branded enum object if found, or `undefined` if no enum\n * with the given ID has been registered.\n *\n * @example\n * const Status = createBrandedEnum('status', { Active: 'active' } as const);\n *\n * const retrieved = getEnumById('status');\n * console.log(retrieved === Status); // true\n *\n * const notFound = getEnumById('nonexistent');\n * console.log(notFound); // undefined\n */\nexport function getEnumById(\n enumId: string\n): BrandedEnum<Record<string, string>> | undefined {\n const registry = getRegistry();\n const entry = registry.enums.get(enumId);\n return entry?.enumObj;\n}\n\n/**\n * Finds all enum IDs that contain a given value.\n *\n * Performs a reverse lookup to find which enums contain a specific value.\n * Useful for debugging value collisions or routing values to handlers.\n *\n * @param value - The string value to search for\n * @returns Array of enum IDs that contain the value. Returns empty array\n * if no enums contain the value.\n *\n * @example\n * // Single enum containing value\n * createBrandedEnum('colors', { Red: 'red', Blue: 'blue' } as const);\n * findEnumSources('red'); // ['colors']\n *\n * @example\n * // Multiple enums with same value (collision detection)\n * createBrandedEnum('status1', { Active: 'active' } as const);\n * createBrandedEnum('status2', { Enabled: 'active' } as const);\n * findEnumSources('active'); // ['status1', 'status2']\n *\n * @example\n * // Value not found\n * findEnumSources('nonexistent'); // []\n */\nexport function findEnumSources(value: string): string[] {\n const registry = getRegistry();\n const enumIds = registry.valueIndex.get(value);\n return enumIds ? Array.from(enumIds) : [];\n}\n"],"names":["REGISTRY_KEY","ENUM_ID","ENUM_VALUES","getRegistry","global","globalThis","enums","Map","valueIndex","registerEnum","enumObj","registry","enumId","values","has","Error","entry","set","value","enumIds","get","Set","add","getAllEnumIds","Array","from","keys","getEnumById","findEnumSources"],"rangeMappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;","mappings":"AAAA;;;;;CAKC,GAED,SAIEA,YAAY,EACZC,OAAO,EACPC,WAAW,QACN,aAAa;AAEpB;;;;;;;;;;;;;CAaC,GACD,OAAO,SAASC;IACd,MAAMC,SAASC;IAIf,IAAI,CAAEL,CAAAA,gBAAgBI,MAAK,KAAM,CAACA,MAAM,CAACJ,aAAa,EAAE;QACtDI,MAAM,CAACJ,aAAa,GAAG;YACrBM,OAAO,IAAIC;YACXC,YAAY,IAAID;QAClB;IACF;IAEA,OAAOH,MAAM,CAACJ,aAAa;AAC7B;AAEA;;;;;;CAMC,GACD,OAAO,SAASS,aACdC,OAAuB;IAEvB,MAAMC,WAAWR;IACjB,MAAMS,SAASF,OAAO,CAACT,QAAQ;IAC/B,MAAMY,SAASH,OAAO,CAACR,YAAY;IAEnC,yBAAyB;IACzB,IAAIS,SAASL,KAAK,CAACQ,GAAG,CAACF,SAAS;QAC9B,MAAM,IAAIG,MAAM,CAAC,sBAAsB,EAAEH,OAAO,gBAAgB,CAAC;IACnE;IAEA,wBAAwB;IACxB,MAAMI,QAAuB;QAC3BJ;QACAF,SAASA;QACTG;IACF;IAEA,mBAAmB;IACnBF,SAASL,KAAK,CAACW,GAAG,CAACL,QAAQI;IAE3B,yCAAyC;IACzC,KAAK,MAAME,SAASL,OAAQ;QAC1B,IAAIM,UAAUR,SAASH,UAAU,CAACY,GAAG,CAACF;QACtC,IAAI,CAACC,SAAS;YACZA,UAAU,IAAIE;YACdV,SAASH,UAAU,CAACS,GAAG,CAACC,OAAOC;QACjC;QACAA,QAAQG,GAAG,CAACV;IACd;AACF;AAEA;;;;;;;;;;;;;;CAcC,GACD,OAAO,SAASW;IACd,MAAMZ,WAAWR;IACjB,OAAOqB,MAAMC,IAAI,CAACd,SAASL,KAAK,CAACoB,IAAI;AACvC;AAEA;;;;;;;;;;;;;;;;;;CAkBC,GACD,OAAO,SAASC,YACdf,MAAc;IAEd,MAAMD,WAAWR;IACjB,MAAMa,QAAQL,SAASL,KAAK,CAACc,GAAG,CAACR;IACjC,OAAOI,OAAON;AAChB;AAEA;;;;;;;;;;;;;;;;;;;;;;;;CAwBC,GACD,OAAO,SAASkB,gBAAgBV,KAAa;IAC3C,MAAMP,WAAWR;IACjB,MAAMgB,UAAUR,SAASH,UAAU,CAACY,GAAG,CAACF;IACxC,OAAOC,UAAUK,MAAMC,IAAI,CAACN,WAAW,EAAE;AAC3C"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core type definitions for branded-enum library
|
|
3
|
+
*
|
|
4
|
+
* These types enable runtime-identifiable enum-like objects in TypeScript
|
|
5
|
+
* with zero runtime overhead for value access.
|
|
6
|
+
*/ /**
|
|
7
|
+
* Symbol key for storing the enum ID metadata.
|
|
8
|
+
* Using a Symbol prevents collision with user-defined keys.
|
|
9
|
+
*/ export const ENUM_ID = Symbol('ENUM_ID');
|
|
10
|
+
/**
|
|
11
|
+
* Symbol key for storing the enum values Set.
|
|
12
|
+
* Using a Symbol prevents collision with user-defined keys.
|
|
13
|
+
*/ export const ENUM_VALUES = Symbol('ENUM_VALUES');
|
|
14
|
+
/**
|
|
15
|
+
* The key used to store the registry on globalThis.
|
|
16
|
+
* Namespaced to avoid collisions with other libraries.
|
|
17
|
+
*/ export const REGISTRY_KEY = '__brandedEnumRegistry__';
|
|
18
|
+
/**
|
|
19
|
+
* The key used to store the enum consumer registry on globalThis.
|
|
20
|
+
* Tracks which classes consume which branded enums.
|
|
21
|
+
*/ export const CONSUMER_REGISTRY_KEY = '__brandedEnumConsumerRegistry__';
|
|
22
|
+
|
|
23
|
+
//# sourceMappingURL=types.js.map
|