@jahands/dagger-helpers 0.7.2 → 0.7.4

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.
@@ -0,0 +1,135 @@
1
+ // Helper to check if a character is lowercase A-Z
2
+ type IsLower<C extends string> =
3
+ Lowercase<C> extends Uppercase<C> ? false : C extends Lowercase<C> ? true : false
4
+ // Helper to check if a character is uppercase A-Z
5
+ type IsUpper<C extends string> =
6
+ Lowercase<C> extends Uppercase<C> ? false : C extends Uppercase<C> ? true : false
7
+ // Helper to check if a character is a digit 0-9
8
+ type IsDigit<C extends string> = C extends `${number}` ? true : false
9
+
10
+ // Recursive inner helper for CamelToSnakeCase
11
+ // PrevChar tracks the *previous* character from the original string
12
+ type CamelToSnakeInner<S extends string, PrevChar extends string = ''> =
13
+ // Base case: If the input string S is empty, return an empty string.
14
+ S extends ''
15
+ ? ''
16
+ : // Recursive step: If S has content...
17
+ S extends `${infer First}${infer Rest}`
18
+ ? // --- Logic based on the 'First' character ---
19
+
20
+ // 1. Is 'First' an uppercase letter?
21
+ IsUpper<First> extends true
22
+ ? // 1a. Was 'PrevChar' lowercase or a digit? (Requires underscore)
23
+ IsLower<PrevChar> extends true
24
+ ? `_${Uppercase<First>}${CamelToSnakeInner<Rest, First>}`
25
+ : IsDigit<PrevChar> extends true
26
+ ? `_${Uppercase<First>}${CamelToSnakeInner<Rest, First>}`
27
+ : // 1b. 'PrevChar' was uppercase, symbol, or start. Check for acronym ending.
28
+ Rest extends `${infer NextChar}${string}` // Look ahead
29
+ ? IsLower<NextChar> extends true // Is the *next* char lowercase?
30
+ ? `_${Uppercase<First>}${CamelToSnakeInner<Rest, First>}` // Yes (e.g., API_Key)
31
+ : `${Uppercase<First>}${CamelToSnakeInner<Rest, First>}` // No (e.g., HTTP)
32
+ : // 'First' is the last character or followed by non-lowercase.
33
+ `${Uppercase<First>}${CamelToSnakeInner<Rest, First>}`
34
+ : // 2. Is 'First' a digit? (Only checked if not uppercase)
35
+ IsDigit<First> extends true
36
+ ? // 2a. Was 'PrevChar' a lowercase letter? (Requires underscore)
37
+ IsLower<PrevChar> extends true
38
+ ? `_${First}${CamelToSnakeInner<Rest, First>}`
39
+ : // 'PrevChar' was not lowercase (digit, uppercase, symbol, start)
40
+ `${First}${CamelToSnakeInner<Rest, First>}`
41
+ : // 3. 'First' must be a lowercase letter or a symbol.
42
+ // (Symbols are handled like lowercase here - no underscore needed before them)
43
+ `${Uppercase<First>}${CamelToSnakeInner<Rest, First>}` // Uppercase letter, append symbol directly
44
+ : // This case should technically not be reachable if S is not '', but included for completeness.
45
+ ''
46
+
47
+ /**
48
+ * Type helper to convert a camelCase/PascalCase string literal type
49
+ * to an UPPER_SNAKE_CASE string literal type. Handles common cases
50
+ * including transitions from lower to upper, letters to digits, and basic acronyms.
51
+ *
52
+ * Examples:
53
+ * 'userId' -> 'USER_ID'
54
+ * 'userName' -> 'USER_NAME'
55
+ * 'isActive' -> 'IS_ACTIVE'
56
+ * 'APIKey' -> 'API_KEY'
57
+ * 'version10' -> 'VERSION_10'
58
+ * 'apiV2Client' -> 'API_V2_CLIENT'
59
+ */
60
+ type CamelToSnakeCase<S extends string> = S extends `${infer First}${infer Rest}`
61
+ ? // Uppercase the very first character, then process the rest using the inner helper
62
+ `${Uppercase<First>}${CamelToSnakeInner<Rest, First>}`
63
+ : // Handle single character or empty string
64
+ Uppercase<S>
65
+
66
+ /**
67
+ * Converts a camelCase or PascalCase string to UPPER_SNAKE_CASE.
68
+ * Handles acronyms and numbers within the string.
69
+ *
70
+ * **Examples:**
71
+ * ```
72
+ * 'helloWorld' -> 'HELLO_WORLD'
73
+ * 'HelloWorld' -> 'HELLO_WORLD'
74
+ * 'someAPIKey' -> 'SOME_API_KEY'
75
+ * 'getHttpResponseCode' -> 'GET_HTTP_RESPONSE_CODE'
76
+ * 'version10' -> 'VERSION_10' // Fixed
77
+ * 'version10Alpha' -> 'VERSION_10_ALPHA' // Fixed
78
+ * 'apiV2Client' -> 'API_V2_CLIENT'
79
+ * 'releaseV10' -> 'RELEASE_V10'
80
+ * 'version2Data' -> 'VERSION_2_DATA' // Fixed
81
+ * 'word' -> 'WORD'
82
+ *```
83
+ * @param str The input string in camelCase or PascalCase format.
84
+ * @returns The converted string in UPPER_SNAKE_CASE format.
85
+ */
86
+ export function camelToSnake(str: string): string {
87
+ if (!str) {
88
+ return ''
89
+ }
90
+
91
+ // The sequence matters:
92
+ const result = str
93
+ // 1. Add _ before an uppercase letter that is followed by a lowercase letter,
94
+ // but only if the uppercase letter is not the start of the string and
95
+ // is preceded by another uppercase letter (handles acronyms like APIKey -> API_Key).
96
+ .replace(/([A-Z])([A-Z][a-z])/g, '$1_$2')
97
+ // 2. Add _ before an uppercase letter that is preceded by a lowercase letter or a digit.
98
+ // (handles helloWorld -> hello_World, version10Update -> version10_Update, apiV2Client -> api_V2Client)
99
+ .replace(/([a-z\d])([A-Z])/g, '$1_$2')
100
+ // 3. Add _ between a lowercase letter and a number.
101
+ // (handles version10 -> version_10, but NOT V10 -> V_10 because V is uppercase)
102
+ .replace(/([a-z])(\d)/g, '$1_$2')
103
+
104
+ // Convert the entire result to uppercase
105
+ return result.toUpperCase()
106
+ }
107
+
108
+ /**
109
+ * Converts an object's keys from camelCase/PascalCase to UPPER_SNAKE_CASE.
110
+ * Provides compile-time type safety for the converted keys based on common patterns.
111
+ *
112
+ * @template T The type of the input object.
113
+ * @param input The object with camelCase/PascalCase keys.
114
+ * @returns A new object with the same values but UPPER_SNAKE_CASE keys,
115
+ * with an inferred type reflecting the key transformation.
116
+ */
117
+ export function convertToSnake<T extends Record<string, any>>(
118
+ input: T
119
+ // Apply the mapped type with key remapping using the type helper
120
+ ): { [K in keyof T as CamelToSnakeCase<K & string>]: T[K] } {
121
+ const output: any = {} // Initialize as 'any' or '{}'
122
+
123
+ for (const key in input) {
124
+ if (Object.prototype.hasOwnProperty.call(input, key)) {
125
+ const snakeCaseKey = camelToSnake(key) // Runtime conversion
126
+ output[snakeCaseKey] = input[key]
127
+ }
128
+ }
129
+
130
+ // Type assertion: We trust the runtime `camelToSnake` logic produces keys
131
+ // matching the structure defined by the `CamelToSnakeCase` type helper.
132
+ // This is needed because TS can't perfectly verify the runtime logic
133
+ // against the complex type-level logic for all possible inputs.
134
+ return output as { [K in keyof T as CamelToSnakeCase<K & string>]: T[K] }
135
+ }
@@ -0,0 +1,221 @@
1
+ import { describe, expect, it, test } from 'vitest'
2
+
3
+ import { convertToCamel, snakeToCamel } from './snake-to-camel.js'
4
+
5
+ // --- Tests for snakeToCamel ---
6
+ describe('snakeToCamel', () => {
7
+ // Test cases using test.each for various formats
8
+ test.each([
9
+ // Basic cases
10
+ { input: 'HELLO_WORLD', expected: 'helloWorld' },
11
+ { input: 'SOME_API_KEY', expected: 'someApiKey' },
12
+ { input: 'USER_ID', expected: 'userId' },
13
+ { input: 'REQUEST_URL_PATH', expected: 'requestUrlPath' },
14
+
15
+ // All caps single word
16
+ { input: 'WORD', expected: 'word' },
17
+ { input: 'TOKEN', expected: 'token' },
18
+
19
+ // Lowercase single word (should ideally remain lowercase, current logic lowercases first char)
20
+ { input: 'word', expected: 'word' },
21
+ { input: 'token', expected: 'token' },
22
+
23
+ // Already camelCase (should ideally remain camelCase, current logic lowercases first char)
24
+ { input: 'helloWorld', expected: 'helloWorld' },
25
+ { input: 'someApiKey', expected: 'someApiKey' },
26
+
27
+ // PascalCase (should convert to camelCase)
28
+ { input: 'HelloWorld', expected: 'helloWorld' },
29
+ { input: 'SomeApiKey', expected: 'someApiKey' },
30
+
31
+ // Leading underscores
32
+ { input: '_LEADING_UNDERSCORE', expected: 'leadingUnderscore' },
33
+ { input: '__DOUBLE_LEADING', expected: 'doubleLeading' },
34
+ { input: '_word', expected: 'word' }, // Single word with leading underscore
35
+
36
+ // Trailing underscores
37
+ { input: 'TRAILING_UNDERSCORE_', expected: 'trailingUnderscore' },
38
+ { input: 'DOUBLE_TRAILING__', expected: 'doubleTrailing' },
39
+ { input: 'word_', expected: 'word' }, // Single word with trailing underscore
40
+
41
+ // Consecutive underscores
42
+ { input: 'DOUBLE__UNDERSCORE', expected: 'doubleUnderscore' },
43
+ { input: 'TRIPLE___UNDERSCORE', expected: 'tripleUnderscore' },
44
+ { input: 'LEADING___MIDDLE__TRAILING_', expected: 'leadingMiddleTrailing' },
45
+
46
+ // Mixed case input parts
47
+ { input: 'Mixed_Case_Input', expected: 'mixedCaseInput' },
48
+ { input: 'another_Mixed_CASE', expected: 'anotherMixedCase' },
49
+
50
+ // Keys with numbers
51
+ { input: 'KEY_123', expected: 'key123' },
52
+ { input: 'VERSION_1_0', expected: 'version10' },
53
+ { input: 'API_V2_KEY', expected: 'apiV2Key' },
54
+ { input: '123_STARTING_NUMBER', expected: '123StartingNumber' }, // Starts with number
55
+
56
+ // Edge cases
57
+ { input: '', expected: '' }, // Empty string
58
+ { input: '_', expected: '' }, // Single underscore
59
+ { input: '__', expected: '' }, // Double underscore
60
+ { input: '___', expected: '' }, // Triple underscore
61
+ { input: '_A_', expected: 'a' }, // Underscores around single letter
62
+ { input: 'A', expected: 'a' }, // Single uppercase letter
63
+ { input: 'a', expected: 'a' }, // Single lowercase letter
64
+ { input: '1', expected: '1' }, // Single number as string
65
+ { input: '123', expected: '123' }, // Multiple numbers as string
66
+ ])('should convert "$input" to "$expected"', ({ input, expected }) => {
67
+ expect(snakeToCamel(input)).toBe(expected)
68
+ })
69
+ })
70
+
71
+ // --- Tests for convertToCamel ---
72
+ describe('convertToCamel', () => {
73
+ it('should convert keys of a basic object', () => {
74
+ const input = {
75
+ FIRST_NAME: 'John',
76
+ LAST_NAME: 'Doe',
77
+ USER_ID: 123,
78
+ }
79
+ const expected = {
80
+ firstName: 'John',
81
+ lastName: 'Doe',
82
+ userId: 123,
83
+ }
84
+ const result = convertToCamel(input)
85
+ expect(result).toStrictEqual(expected)
86
+ // Type check (compile time): result should have the correct inferred type
87
+ expect(result.firstName).toBe('John')
88
+ expect(result.userId).toBe(123)
89
+ // expect(result.FIRST_NAME).toBeUndefined(); // This would be a TS error
90
+ })
91
+
92
+ it('should handle various value types correctly', () => {
93
+ const input = {
94
+ STRING_VALUE: 'hello',
95
+ NUMBER_VALUE: 42,
96
+ BOOLEAN_TRUE: true,
97
+ BOOLEAN_FALSE: false,
98
+ NULL_VALUE: null,
99
+ UNDEFINED_VALUE: undefined,
100
+ ARRAY_VALUE: [1, 'two', { NESTED_KEY: true }],
101
+ NESTED_OBJECT: {
102
+ INNER_KEY_1: 'value1',
103
+ INNER_KEY_2: 99,
104
+ },
105
+ }
106
+ const expected = {
107
+ stringValue: 'hello',
108
+ numberValue: 42,
109
+ booleanTrue: true,
110
+ booleanFalse: false,
111
+ nullValue: null,
112
+ undefinedValue: undefined,
113
+ arrayValue: [1, 'two', { NESTED_KEY: true }], // Note: nested object keys are NOT converted
114
+ nestedObject: {
115
+ INNER_KEY_1: 'value1', // Note: nested object keys are NOT converted
116
+ INNER_KEY_2: 99,
117
+ },
118
+ }
119
+ const result = convertToCamel(input)
120
+ expect(result).toStrictEqual(expected)
121
+ // Check specific inferred types/values
122
+ expect(result.arrayValue).toEqual([1, 'two', { NESTED_KEY: true }])
123
+ expect(result.nestedObject.INNER_KEY_1).toBe('value1')
124
+ expect(result.nullValue).toBeNull()
125
+ expect(result.undefinedValue).toBeUndefined()
126
+ })
127
+
128
+ it('should return an empty object for an empty input object', () => {
129
+ const input = {}
130
+ const expected = {}
131
+ expect(convertToCamel(input)).toStrictEqual(expected)
132
+ })
133
+
134
+ it('should handle keys with leading/trailing/multiple underscores', () => {
135
+ const input = {
136
+ _LEADING_KEY: 'val1',
137
+ TRAILING_KEY_: 'val2',
138
+ DOUBLE__KEY: 'val3',
139
+ __BOTH__: 'val4',
140
+ }
141
+ const expected = {
142
+ leadingKey: 'val1',
143
+ trailingKey: 'val2',
144
+ doubleKey: 'val3',
145
+ both: 'val4',
146
+ }
147
+ const result = convertToCamel(input)
148
+ expect(result).toStrictEqual(expected)
149
+ })
150
+
151
+ it('should handle keys that are already camelCase or mixed', () => {
152
+ const input = {
153
+ alreadyCamel: 'value1',
154
+ PascalCaseKey: 'value2', // Will become pascalCaseKey
155
+ MIXED_key_STYLE: 'value3',
156
+ }
157
+ const expected = {
158
+ alreadyCamel: 'value1', // Stays camelCase as no underscores
159
+ pascalCaseKey: 'value2', // First letter lowercased
160
+ mixedKeyStyle: 'value3',
161
+ }
162
+ const result = convertToCamel(input)
163
+ expect(result).toStrictEqual(expected)
164
+ })
165
+
166
+ it('should handle keys with numbers', () => {
167
+ const input = {
168
+ KEY_1: 1,
169
+ KEY_2_PART: 2,
170
+ VERSION_10X: 'v10',
171
+ _1_START: 'start',
172
+ }
173
+ const expected = {
174
+ key1: 1,
175
+ key2Part: 2,
176
+ version10x: 'v10',
177
+ '1Start': 'start', // Note: snakeToCamel logic produces this
178
+ }
179
+ const result = convertToCamel(input)
180
+ expect(result).toStrictEqual(expected)
181
+ })
182
+
183
+ it('should not include properties from the prototype chain', () => {
184
+ const proto = { PROTO_KEY: 'protoValue' }
185
+ const input = Object.create(proto)
186
+ input.OWN_KEY = 'ownValue'
187
+ input.ANOTHER_OWN = 'another'
188
+
189
+ const expected = {
190
+ ownKey: 'ownValue',
191
+ anotherOwn: 'another',
192
+ }
193
+ const result = convertToCamel(input)
194
+ expect(result).toStrictEqual(expected)
195
+ // Ensure proto property isn't present (even in camelCase)
196
+ expect((result as any).protoKey).toBeUndefined()
197
+ })
198
+
199
+ it('should produce a correctly typed object (compile-time check)', () => {
200
+ const input = {
201
+ API_TOKEN: 'xyz',
202
+ USER_COUNT: 100,
203
+ } as const // Use 'as const' for more precise type inference if needed
204
+
205
+ const result = convertToCamel(input)
206
+
207
+ // These lines check the type inference at compile time.
208
+ // If you hover over `result` or try to access invalid properties,
209
+ // TypeScript/your IDE should show the correct inferred type/errors.
210
+ const token: string = result.apiToken
211
+ const count: number = result.userCount
212
+
213
+ expect(token).toBe('xyz')
214
+ expect(count).toBe(100)
215
+
216
+ // @ts-expect-error
217
+ const _invalid: string = result.API_TOKEN
218
+ // @ts-expect-error
219
+ const _nonExistent: number = result.someOtherKey
220
+ })
221
+ })
@@ -0,0 +1,101 @@
1
+ /**
2
+ * Helper type to capitalize the first letter of a string literal type.
3
+ * e.g., 'hello' -> 'Hello'
4
+ */
5
+ type CapitalizeString<S extends string> = S extends `${infer First}${infer Rest}`
6
+ ? `${Uppercase<First>}${Rest}`
7
+ : S
8
+
9
+ /**
10
+ * Recursively transforms a snake_case string literal type to camelCase.
11
+ * e.g., 'SOME_API_KEY' -> 'someApiKey'
12
+ * 'HELLO_WORLD' -> 'helloWorld'
13
+ * 'WORD' -> 'word'
14
+ */
15
+ type SnakeToCamelCase<S extends string> = S extends `${infer T}_${infer U}`
16
+ ? `${Lowercase<T>}${CapitalizeString<SnakeToCamelCase<U>>}` // Lowercase first part, capitalize and recurse on the rest
17
+ : Lowercase<S> // Base case: No underscore, just lowercase the whole thing
18
+
19
+ /**
20
+ * Converts a string from snake_case, UPPER_SNAKE_CASE, PascalCase,
21
+ * or ALL_CAPS to camelCase at runtime.
22
+ * If the string is already camelCase or lowercase, it remains unchanged.
23
+ *
24
+ * @param str The input string in snake_case format.
25
+ * @returns The converted string in camelCase format.
26
+ */
27
+ export function snakeToCamel(str: string): string {
28
+ if (!str) {
29
+ return ''
30
+ }
31
+
32
+ // Case 1: String contains underscores (snake_case)
33
+ if (str.includes('_')) {
34
+ let result = ''
35
+ const parts = str.split('_')
36
+ for (let i = 0; i < parts.length; i++) {
37
+ const part = parts[i]
38
+ if (part) {
39
+ // Skip empty parts from multiple underscores
40
+ if (result.length === 0) {
41
+ // First non-empty part: lowercase the whole part
42
+ result += part.toLowerCase()
43
+ } else {
44
+ // Subsequent non-empty parts: capitalize first letter, rest lowercase
45
+ result += part[0]!.toUpperCase() + part.slice(1).toLowerCase()
46
+ }
47
+ }
48
+ }
49
+ // Handle case like "_SOME_WORD_" -> "someWord"
50
+ // If the first character of the original string was '_' and result is empty,
51
+ // it means the string was only underscores, return ''
52
+ // If the first part was empty ('_WORD'), result starts lowercase correctly.
53
+ return result
54
+ }
55
+
56
+ // Case 2: String does NOT contain underscores
57
+ // Check if it's ALL_CAPS (allowing for numbers)
58
+ const isAllCaps = /^[A-Z0-9]+$/.test(str)
59
+ if (isAllCaps) {
60
+ // e.g., "WORD", "TOKEN123" -> "word", "token123"
61
+ return str.toLowerCase()
62
+ }
63
+
64
+ // Check if it's PascalCase or already camelCase/lowercase
65
+ // If the first letter is already lowercase, assume it's camelCase or lowercase
66
+ if (str[0] === str[0]!.toLowerCase()) {
67
+ // e.g., "helloWorld", "word" -> stays the same
68
+ return str
69
+ }
70
+
71
+ // Otherwise, assume it's PascalCase: Lowercase only the first character
72
+ // e.g., "HelloWorld", "SomeValue" -> "helloWorld", "someValue"
73
+ return str[0]!.toLowerCase() + str.slice(1)
74
+ }
75
+
76
+ /**
77
+ * Converts an object's keys from snake_case or UPPER_SNAKE_CASE to camelCase.
78
+ * The return type precisely maps the input keys to their camelCase versions.
79
+ *
80
+ * @template T The type of the input object, expected to have string keys.
81
+ * @param input The object with snake_case keys.
82
+ * @returns A new object with the same values but camelCase keys, with a specific inferred type.
83
+ */
84
+ export function convertToCamel<T extends Record<string, any>>(
85
+ input: T
86
+ ): { [K in keyof T as SnakeToCamelCase<K & string>]: T[K] } {
87
+ const output: any = {}
88
+
89
+ for (const key in input) {
90
+ if (Object.prototype.hasOwnProperty.call(input, key)) {
91
+ const camelCaseKey = snakeToCamel(key)
92
+ output[camelCaseKey] = input[key]
93
+ }
94
+ }
95
+
96
+ // We use a type assertion here. While our runtime logic creates the correct
97
+ // shape, TypeScript's analysis within the loop isn't powerful enough to
98
+ // automatically verify that 'output' perfectly matches the complex mapped type.
99
+ // The return type annotation guarantees the type for the caller.
100
+ return output as { [K in keyof T as SnakeToCamelCase<K & string>]: T[K] }
101
+ }