unacy 0.6.0 → 0.8.0
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/README.md +193 -47
- package/dist/converters.d.ts +28 -4
- package/dist/converters.d.ts.map +1 -1
- package/dist/errors.js.map +1 -1
- package/dist/index.d.ts +5 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/registry.d.ts +39 -41
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +58 -14
- package/dist/registry.js.map +1 -1
- package/dist/types.d.ts +159 -18
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/utils/validation.d.ts +106 -0
- package/dist/utils/validation.d.ts.map +1 -1
- package/dist/utils/validation.js +317 -0
- package/dist/utils/validation.js.map +1 -1
- package/package.json +16 -8
- package/dist/__tests__/converters.test.d.ts +0 -2
- package/dist/__tests__/converters.test.d.ts.map +0 -1
- package/dist/__tests__/converters.test.js +0 -128
- package/dist/__tests__/converters.test.js.map +0 -1
- package/dist/__tests__/errors.test.d.ts +0 -2
- package/dist/__tests__/errors.test.d.ts.map +0 -1
- package/dist/__tests__/errors.test.js +0 -93
- package/dist/__tests__/errors.test.js.map +0 -1
- package/dist/__tests__/formatters.test.d.ts +0 -2
- package/dist/__tests__/formatters.test.d.ts.map +0 -1
- package/dist/__tests__/formatters.test.js +0 -244
- package/dist/__tests__/formatters.test.js.map +0 -1
- package/dist/__tests__/registry.test.d.ts +0 -2
- package/dist/__tests__/registry.test.d.ts.map +0 -1
- package/dist/__tests__/registry.test.js +0 -403
- package/dist/__tests__/registry.test.js.map +0 -1
- package/dist/__tests__/types.test.d.ts +0 -2
- package/dist/__tests__/types.test.d.ts.map +0 -1
- package/dist/__tests__/types.test.js +0 -115
- package/dist/__tests__/types.test.js.map +0 -1
|
@@ -1,244 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, expectTypeOf } from 'vitest';
|
|
2
|
-
import { ParseError } from '../errors.js';
|
|
3
|
-
import { z } from 'zod';
|
|
4
|
-
describe('Formatter Type', () => {
|
|
5
|
-
it('formatter converts tagged value to string', () => {
|
|
6
|
-
const formatISO = (date) => date.toISOString();
|
|
7
|
-
const now = new Date('2026-01-06T12:00:00.000Z');
|
|
8
|
-
const str = formatISO(now);
|
|
9
|
-
expect(typeof str).toBe('string');
|
|
10
|
-
expect(str).toBe('2026-01-06T12:00:00.000Z');
|
|
11
|
-
});
|
|
12
|
-
it('formatter maintains format identity through transformation', () => {
|
|
13
|
-
const formatHex = (color) => color.toUpperCase();
|
|
14
|
-
const color = '#ff5733';
|
|
15
|
-
const formatted = formatHex(color);
|
|
16
|
-
expect(formatted).toBe('#FF5733');
|
|
17
|
-
expectTypeOf(formatted).toEqualTypeOf();
|
|
18
|
-
});
|
|
19
|
-
it('formatter works with different base types', () => {
|
|
20
|
-
const formatTimestamp = (ts) => String(ts);
|
|
21
|
-
const timestamp = Date.now();
|
|
22
|
-
const str = formatTimestamp(timestamp);
|
|
23
|
-
expect(typeof str).toBe('string');
|
|
24
|
-
expect(str).toMatch(/^\d+$/);
|
|
25
|
-
});
|
|
26
|
-
});
|
|
27
|
-
describe('Parser Type', () => {
|
|
28
|
-
it('parser converts string to tagged value with Zod validation', () => {
|
|
29
|
-
const parseISO = (input) => {
|
|
30
|
-
const schema = z.string().datetime();
|
|
31
|
-
const validated = schema.parse(input);
|
|
32
|
-
return new Date(validated);
|
|
33
|
-
};
|
|
34
|
-
const str = '2026-01-06T12:00:00.000Z';
|
|
35
|
-
const date = parseISO(str);
|
|
36
|
-
expectTypeOf(date).toEqualTypeOf();
|
|
37
|
-
expect(date).toBeInstanceOf(Date);
|
|
38
|
-
expect(date.toISOString()).toBe(str);
|
|
39
|
-
});
|
|
40
|
-
it('invalid string input throws ParseError with context', () => {
|
|
41
|
-
const parseHex = (input) => {
|
|
42
|
-
const schema = z.string().regex(/^#[0-9A-Fa-f]{6}$/);
|
|
43
|
-
try {
|
|
44
|
-
return schema.parse(input);
|
|
45
|
-
}
|
|
46
|
-
catch {
|
|
47
|
-
throw new ParseError('HexColor', input, 'Expected #RRGGBB format');
|
|
48
|
-
}
|
|
49
|
-
};
|
|
50
|
-
expect(() => {
|
|
51
|
-
parseHex('not-a-color');
|
|
52
|
-
}).toThrow(ParseError);
|
|
53
|
-
try {
|
|
54
|
-
parseHex('invalid');
|
|
55
|
-
}
|
|
56
|
-
catch (err) {
|
|
57
|
-
expect(err).toBeInstanceOf(ParseError);
|
|
58
|
-
if (err instanceof ParseError) {
|
|
59
|
-
expect(err.format).toBe('HexColor');
|
|
60
|
-
expect(err.input).toBe('invalid');
|
|
61
|
-
expect(err.reason).toBe('Expected #RRGGBB format');
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
});
|
|
65
|
-
it('parser rejects input before tagging (no invalid tagged values)', () => {
|
|
66
|
-
const parsePositive = (input) => {
|
|
67
|
-
const schema = z
|
|
68
|
-
.string()
|
|
69
|
-
.transform(parseFloat)
|
|
70
|
-
.refine((n) => n > 0);
|
|
71
|
-
try {
|
|
72
|
-
return schema.parse(input);
|
|
73
|
-
}
|
|
74
|
-
catch {
|
|
75
|
-
throw new ParseError('Positive', input, 'Must be a positive number');
|
|
76
|
-
}
|
|
77
|
-
};
|
|
78
|
-
expect(() => {
|
|
79
|
-
parsePositive('-5');
|
|
80
|
-
}).toThrow(ParseError);
|
|
81
|
-
expect(() => {
|
|
82
|
-
parsePositive('not-a-number');
|
|
83
|
-
}).toThrow(ParseError);
|
|
84
|
-
// Valid input should work
|
|
85
|
-
const result = parsePositive('10');
|
|
86
|
-
expect(result).toBe(10);
|
|
87
|
-
});
|
|
88
|
-
it('handles empty input', () => {
|
|
89
|
-
const parseNonEmpty = (input) => {
|
|
90
|
-
if (input === '') {
|
|
91
|
-
throw new ParseError('NonEmpty', input, 'Cannot be empty');
|
|
92
|
-
}
|
|
93
|
-
return input;
|
|
94
|
-
};
|
|
95
|
-
expect(() => {
|
|
96
|
-
parseNonEmpty('');
|
|
97
|
-
}).toThrow(ParseError);
|
|
98
|
-
try {
|
|
99
|
-
parseNonEmpty('');
|
|
100
|
-
}
|
|
101
|
-
catch (err) {
|
|
102
|
-
if (err instanceof ParseError) {
|
|
103
|
-
expect(err.message).toContain('""');
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
});
|
|
107
|
-
it('compile-time: wrong format type to parser causes error', () => {
|
|
108
|
-
const parseISO = (input) => {
|
|
109
|
-
return new Date(input);
|
|
110
|
-
};
|
|
111
|
-
const str = '2026-01-06T12:00:00.000Z';
|
|
112
|
-
const date = parseISO(str);
|
|
113
|
-
// @ts-expect-error - Cannot assign ISO8601 to HexColor
|
|
114
|
-
const color = date;
|
|
115
|
-
expect(color).toBeDefined();
|
|
116
|
-
});
|
|
117
|
-
});
|
|
118
|
-
describe('FormatterParser Type', () => {
|
|
119
|
-
it('round-trip (format then parse) produces equivalent value', () => {
|
|
120
|
-
const iso8601 = {
|
|
121
|
-
format: (date) => date.toISOString(),
|
|
122
|
-
parse: (input) => {
|
|
123
|
-
const schema = z.string().datetime();
|
|
124
|
-
const validated = schema.parse(input);
|
|
125
|
-
return new Date(validated);
|
|
126
|
-
}
|
|
127
|
-
};
|
|
128
|
-
const original = new Date('2026-01-06T12:00:00.000Z');
|
|
129
|
-
const formatted = iso8601.format(original);
|
|
130
|
-
const parsed = iso8601.parse(formatted);
|
|
131
|
-
expect(parsed.getTime()).toBe(original.getTime());
|
|
132
|
-
});
|
|
133
|
-
it('integration test: full formatter/parser pair for ISO8601 dates', () => {
|
|
134
|
-
const iso8601 = {
|
|
135
|
-
format: (date) => date.toISOString(),
|
|
136
|
-
parse: (input) => {
|
|
137
|
-
const schema = z.string().datetime();
|
|
138
|
-
try {
|
|
139
|
-
const validated = schema.parse(input);
|
|
140
|
-
return new Date(validated);
|
|
141
|
-
}
|
|
142
|
-
catch {
|
|
143
|
-
throw new ParseError('ISO8601', input, 'Invalid ISO8601 date format');
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
};
|
|
147
|
-
// Test formatting
|
|
148
|
-
const date = new Date('2026-01-06T12:34:56.789Z');
|
|
149
|
-
const str = iso8601.format(date);
|
|
150
|
-
expect(str).toBe('2026-01-06T12:34:56.789Z');
|
|
151
|
-
// Test parsing
|
|
152
|
-
const parsed = iso8601.parse('2026-12-31T23:59:59.999Z');
|
|
153
|
-
expect(parsed).toBeInstanceOf(Date);
|
|
154
|
-
expect(parsed.toISOString()).toBe('2026-12-31T23:59:59.999Z');
|
|
155
|
-
// Test round-trip
|
|
156
|
-
const roundTrip = iso8601.parse(iso8601.format(date));
|
|
157
|
-
expect(roundTrip.getTime()).toBe(date.getTime());
|
|
158
|
-
// Test error handling
|
|
159
|
-
expect(() => {
|
|
160
|
-
iso8601.parse('invalid-date');
|
|
161
|
-
}).toThrow(ParseError);
|
|
162
|
-
});
|
|
163
|
-
it('works with different format types', () => {
|
|
164
|
-
const hexColor = {
|
|
165
|
-
format: (color) => color.toLowerCase(),
|
|
166
|
-
parse: (input) => {
|
|
167
|
-
const schema = z.string().regex(/^#[0-9a-fA-F]{6}$/);
|
|
168
|
-
try {
|
|
169
|
-
return schema.parse(input);
|
|
170
|
-
}
|
|
171
|
-
catch {
|
|
172
|
-
throw new ParseError('HexColor', input, 'Expected #RRGGBB format');
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
};
|
|
176
|
-
const color = '#FF5733';
|
|
177
|
-
const formatted = hexColor.format(color);
|
|
178
|
-
expect(formatted).toBe('#ff5733');
|
|
179
|
-
const parsed = hexColor.parse('#00FF00');
|
|
180
|
-
expectTypeOf(parsed).toEqualTypeOf();
|
|
181
|
-
expect(parsed).toBe('#00FF00');
|
|
182
|
-
// Round-trip with case normalization
|
|
183
|
-
const original = '#ABCDEF';
|
|
184
|
-
const roundTrip = hexColor.parse(hexColor.format(original));
|
|
185
|
-
expect(roundTrip.toLowerCase()).toBe(original.toLowerCase());
|
|
186
|
-
});
|
|
187
|
-
it('timestamp formatter/parser preserves value', () => {
|
|
188
|
-
const unixTimestamp = {
|
|
189
|
-
format: (ts) => String(ts),
|
|
190
|
-
parse: (input) => {
|
|
191
|
-
const schema = z
|
|
192
|
-
.string()
|
|
193
|
-
.regex(/^\d+$/)
|
|
194
|
-
.transform((s) => parseInt(s, 10));
|
|
195
|
-
try {
|
|
196
|
-
return schema.parse(input);
|
|
197
|
-
}
|
|
198
|
-
catch {
|
|
199
|
-
throw new ParseError('UnixTimestamp', input, 'Expected Unix timestamp');
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
};
|
|
203
|
-
const timestamp = Date.now();
|
|
204
|
-
const str = unixTimestamp.format(timestamp);
|
|
205
|
-
const parsed = unixTimestamp.parse(str);
|
|
206
|
-
expect(parsed).toBe(timestamp);
|
|
207
|
-
});
|
|
208
|
-
});
|
|
209
|
-
describe('Formatter/Parser Examples', () => {
|
|
210
|
-
it('YYYY-MM-DD format with validation', () => {
|
|
211
|
-
const yyyymmdd = {
|
|
212
|
-
format: (dateStr) => dateStr,
|
|
213
|
-
parse: (input) => {
|
|
214
|
-
const schema = z.string().regex(/^\d{4}-\d{2}-\d{2}$/);
|
|
215
|
-
try {
|
|
216
|
-
const validated = schema.parse(input);
|
|
217
|
-
// Additional validation: check if it's a valid date
|
|
218
|
-
const parts = validated.split('-');
|
|
219
|
-
const year = parseInt(parts[0], 10);
|
|
220
|
-
const month = parseInt(parts[1], 10);
|
|
221
|
-
const day = parseInt(parts[2], 10);
|
|
222
|
-
const date = new Date(year, month - 1, day);
|
|
223
|
-
if (date.getFullYear() !== year ||
|
|
224
|
-
date.getMonth() !== month - 1 ||
|
|
225
|
-
date.getDate() !== day) {
|
|
226
|
-
throw new Error('Invalid date');
|
|
227
|
-
}
|
|
228
|
-
return validated;
|
|
229
|
-
}
|
|
230
|
-
catch {
|
|
231
|
-
throw new ParseError('YYYY-MM-DD', input, 'Invalid date format');
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
};
|
|
235
|
-
// Valid dates
|
|
236
|
-
expect(yyyymmdd.parse('2026-01-06')).toBe('2026-01-06');
|
|
237
|
-
expect(yyyymmdd.parse('2026-12-31')).toBe('2026-12-31');
|
|
238
|
-
// Invalid dates
|
|
239
|
-
expect(() => yyyymmdd.parse('2026-13-01')).toThrow(ParseError); // Invalid month
|
|
240
|
-
expect(() => yyyymmdd.parse('2026-02-30')).toThrow(ParseError); // Invalid day
|
|
241
|
-
expect(() => yyyymmdd.parse('not-a-date')).toThrow(ParseError);
|
|
242
|
-
});
|
|
243
|
-
});
|
|
244
|
-
//# sourceMappingURL=formatters.test.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"formatters.test.js","sourceRoot":"","sources":["../../src/__tests__/formatters.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"registry.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/registry.test.ts"],"names":[],"mappings":""}
|
|
@@ -1,403 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, expectTypeOf } from 'vitest';
|
|
2
|
-
import { createRegistry } from '../registry.js';
|
|
3
|
-
import { CycleError, MaxDepthError, ConversionError } from '../errors.js';
|
|
4
|
-
const getConverter = (registry, from, to) => registry.getConverter(from, to);
|
|
5
|
-
const convert = (registry, value, from) => registry.convert(value, from);
|
|
6
|
-
describe('Registry - Basic Operations', () => {
|
|
7
|
-
it('register and retrieve direct converter', () => {
|
|
8
|
-
const registry = createRegistry().register('Celsius', 'Fahrenheit', (c) => ((c * 9) / 5 + 32));
|
|
9
|
-
const converter = getConverter(registry, 'Celsius', 'Fahrenheit');
|
|
10
|
-
expect(converter).toBeDefined();
|
|
11
|
-
if (converter) {
|
|
12
|
-
const result = converter(0);
|
|
13
|
-
expect(result).toBe(32);
|
|
14
|
-
}
|
|
15
|
-
});
|
|
16
|
-
it('registerBidirectional registers both directions', () => {
|
|
17
|
-
const registry = createRegistry().register('meters', 'kilometers', {
|
|
18
|
-
to: (m) => (m / 1000),
|
|
19
|
-
from: (km) => (km * 1000)
|
|
20
|
-
});
|
|
21
|
-
const m2km = getConverter(registry, 'meters', 'kilometers');
|
|
22
|
-
const km2m = getConverter(registry, 'kilometers', 'meters');
|
|
23
|
-
expect(m2km).toBeDefined();
|
|
24
|
-
expect(km2m).toBeDefined();
|
|
25
|
-
if (m2km && km2m) {
|
|
26
|
-
expect(m2km(1000)).toBe(1);
|
|
27
|
-
expect(km2m(1)).toBe(1000);
|
|
28
|
-
}
|
|
29
|
-
});
|
|
30
|
-
it('getConverter finds direct converter in O(1)', () => {
|
|
31
|
-
const registry = createRegistry()
|
|
32
|
-
.register('A', 'B', (a) => (a * 2))
|
|
33
|
-
.register('B', 'C', (b) => (b * 3));
|
|
34
|
-
const startTime = performance.now();
|
|
35
|
-
const converter = getConverter(registry, 'A', 'B');
|
|
36
|
-
const endTime = performance.now();
|
|
37
|
-
expect(converter).toBeDefined();
|
|
38
|
-
// O(1) lookup should be fast (< 1ms even in slow environments)
|
|
39
|
-
expect(endTime - startTime).toBeLessThan(5);
|
|
40
|
-
});
|
|
41
|
-
});
|
|
42
|
-
describe('Registry - Multi-Hop Composition', () => {
|
|
43
|
-
it('auto-compose 2-hop conversion (A→B→C)', () => {
|
|
44
|
-
const registry = createRegistry()
|
|
45
|
-
.register('A', 'B', (a) => (a * 2))
|
|
46
|
-
.register('B', 'C', (b) => (b * 3));
|
|
47
|
-
const converter = getConverter(registry, 'A', 'C');
|
|
48
|
-
expect(converter).toBeDefined();
|
|
49
|
-
if (converter) {
|
|
50
|
-
// A→B→C: 10 * 2 = 20, 20 * 3 = 60
|
|
51
|
-
expect(converter(10)).toBe(60);
|
|
52
|
-
}
|
|
53
|
-
});
|
|
54
|
-
it('auto-compose 3-hop conversion (A→B→C→D)', () => {
|
|
55
|
-
const registry = createRegistry()
|
|
56
|
-
.register('A', 'B', (a) => (a * 2))
|
|
57
|
-
.register('B', 'C', (b) => (b * 3))
|
|
58
|
-
.register('C', 'D', (c) => (c * 5));
|
|
59
|
-
const converter = getConverter(registry, 'A', 'D');
|
|
60
|
-
expect(converter).toBeDefined();
|
|
61
|
-
if (converter) {
|
|
62
|
-
// A→B→C→D: 10 * 2 * 3 * 5 = 300
|
|
63
|
-
expect(converter(10)).toBe(300);
|
|
64
|
-
}
|
|
65
|
-
});
|
|
66
|
-
it('integration test: 3-unit distance conversion (m→km→mi)', () => {
|
|
67
|
-
const registry = createRegistry()
|
|
68
|
-
.register('meters', 'kilometers', {
|
|
69
|
-
to: (m) => (m / 1000),
|
|
70
|
-
from: (km) => (km * 1000)
|
|
71
|
-
})
|
|
72
|
-
.register('kilometers', 'miles', {
|
|
73
|
-
to: (km) => (km * 0.621371),
|
|
74
|
-
from: (mi) => (mi / 0.621371)
|
|
75
|
-
});
|
|
76
|
-
const converter = getConverter(registry, 'meters', 'miles');
|
|
77
|
-
expect(converter).toBeDefined();
|
|
78
|
-
if (converter) {
|
|
79
|
-
const meters = 5000;
|
|
80
|
-
const miles = converter(meters);
|
|
81
|
-
// 5000m → 5km → 3.106855mi
|
|
82
|
-
expect(miles).toBeCloseTo(3.106855, 5);
|
|
83
|
-
}
|
|
84
|
-
});
|
|
85
|
-
it('chooses shortest path when multiple paths exist', () => {
|
|
86
|
-
// Create diamond shape: A→B→D and A→C→D
|
|
87
|
-
// But also add direct A→D
|
|
88
|
-
const registry = createRegistry()
|
|
89
|
-
.register('A', 'B', (a) => (a + 100)) // Long path
|
|
90
|
-
.register('B', 'D', (b) => (b + 100))
|
|
91
|
-
.register('A', 'C', (a) => (a + 200)) // Long path
|
|
92
|
-
.register('C', 'D', (c) => (c + 200))
|
|
93
|
-
.register('A', 'D', (a) => (a * 10)); // Direct (shortest)
|
|
94
|
-
const converter = getConverter(registry, 'A', 'D');
|
|
95
|
-
if (converter) {
|
|
96
|
-
// Should use direct path: 5 * 10 = 50
|
|
97
|
-
expect(converter(5)).toBe(50);
|
|
98
|
-
}
|
|
99
|
-
});
|
|
100
|
-
});
|
|
101
|
-
describe('Registry - Error Handling', () => {
|
|
102
|
-
it('cycle detection throws CycleError with path', () => {
|
|
103
|
-
// Create cycle: A→B→C→A
|
|
104
|
-
const registry = createRegistry()
|
|
105
|
-
.register('A', 'B', (a) => (a * 2))
|
|
106
|
-
.register('B', 'C', (b) => (b * 3))
|
|
107
|
-
.register('C', 'A', (c) => (c * 5));
|
|
108
|
-
// Trying to convert from C to A would complete a cycle
|
|
109
|
-
// But the actual cycle is detected when finding a path
|
|
110
|
-
// Let me test a different scenario: trying to get a converter that would require going in a circle
|
|
111
|
-
// Since we register C→A, we can actually get A to C via A→B→C
|
|
112
|
-
// But trying to get C to C should trigger cycle detection
|
|
113
|
-
expect(() => {
|
|
114
|
-
getConverter(registry, 'C', 'C'); // Cast needed due to type constraint
|
|
115
|
-
}).toThrow(CycleError);
|
|
116
|
-
});
|
|
117
|
-
it('max depth (>5 hops) throws MaxDepthError', () => {
|
|
118
|
-
// Create chain: A→B→C→D→E→F→G (6 hops)
|
|
119
|
-
const registry = createRegistry()
|
|
120
|
-
.register('A', 'B', (a) => (a * 2))
|
|
121
|
-
.register('B', 'C', (b) => (b * 2))
|
|
122
|
-
.register('C', 'D', (c) => (c * 2))
|
|
123
|
-
.register('D', 'E', (d) => (d * 2))
|
|
124
|
-
.register('E', 'F', (e) => (e * 2))
|
|
125
|
-
.register('F', 'G', (f) => (f * 2));
|
|
126
|
-
expect(() => {
|
|
127
|
-
getConverter(registry, 'A', 'G');
|
|
128
|
-
}).toThrow(MaxDepthError);
|
|
129
|
-
});
|
|
130
|
-
it('missing converter path throws ConversionError', () => {
|
|
131
|
-
const registry = createRegistry().register('A', 'B', (a) => (a * 2));
|
|
132
|
-
// No path from A to C
|
|
133
|
-
const result = getConverter(registry, 'A', 'C');
|
|
134
|
-
expect(result).toBeUndefined();
|
|
135
|
-
});
|
|
136
|
-
});
|
|
137
|
-
describe('Registry - Fluent API', () => {
|
|
138
|
-
it('compile-time: wrong unit to convert() causes error', () => {
|
|
139
|
-
const registry = createRegistry().register('Celsius', 'Fahrenheit', (c) => ((c * 9) / 5 + 32));
|
|
140
|
-
const temp = 25;
|
|
141
|
-
// Valid conversion
|
|
142
|
-
const fahrenheit = convert(registry, temp, 'Celsius').to('Fahrenheit');
|
|
143
|
-
expect(fahrenheit).toBe(77);
|
|
144
|
-
// Type safety test: This tests that wrong units are caught at compile-time
|
|
145
|
-
// We can't easily test this at runtime, so we document the expected behavior
|
|
146
|
-
});
|
|
147
|
-
it('fluent API performs direct conversion', () => {
|
|
148
|
-
const registry = createRegistry().register('Celsius', 'Fahrenheit', (c) => ((c * 9) / 5 + 32));
|
|
149
|
-
const temp = 0;
|
|
150
|
-
const result = convert(registry, temp, 'Celsius').to('Fahrenheit');
|
|
151
|
-
expect(result).toBe(32);
|
|
152
|
-
// Type is WithUnits<any, 'Fahrenheit'> which is compatible with Fahrenheit
|
|
153
|
-
});
|
|
154
|
-
it('fluent API performs multi-hop conversion', () => {
|
|
155
|
-
const registry = createRegistry()
|
|
156
|
-
.register('Celsius', 'Kelvin', (c) => (c + 273.15))
|
|
157
|
-
.register('Kelvin', 'Fahrenheit', (k) => (((k - 273.15) * 9) / 5 + 32));
|
|
158
|
-
const temp = 0;
|
|
159
|
-
const fahrenheit = convert(registry, temp, 'Celsius').to('Fahrenheit');
|
|
160
|
-
expect(fahrenheit).toBeCloseTo(32, 5);
|
|
161
|
-
});
|
|
162
|
-
it('fluent API throws error for missing path', () => {
|
|
163
|
-
const registry = createRegistry().register('A', 'B', (a) => (a * 2));
|
|
164
|
-
expect(() => {
|
|
165
|
-
convert(registry, 10, 'A').to('C');
|
|
166
|
-
}).toThrow(ConversionError);
|
|
167
|
-
});
|
|
168
|
-
});
|
|
169
|
-
describe('Registry - Immutability', () => {
|
|
170
|
-
it('register returns new registry instance', () => {
|
|
171
|
-
const registry1 = createRegistry();
|
|
172
|
-
const registry2 = registry1.register('A', 'B', (a) => (a * 2));
|
|
173
|
-
expect(registry1).not.toBe(registry2);
|
|
174
|
-
});
|
|
175
|
-
it('original registry unchanged after register', () => {
|
|
176
|
-
const registry1 = createRegistry();
|
|
177
|
-
const registry2 = registry1.register('A', 'B', (a) => (a * 2));
|
|
178
|
-
// registry1 should not have the converter
|
|
179
|
-
expect(getConverter(registry1, 'A', 'B')).toBeUndefined();
|
|
180
|
-
// registry2 should have the converter
|
|
181
|
-
expect(getConverter(registry2, 'A', 'B')).toBeDefined();
|
|
182
|
-
});
|
|
183
|
-
});
|
|
184
|
-
describe('Registry - Performance', () => {
|
|
185
|
-
it('caches composed paths for repeated conversions', () => {
|
|
186
|
-
const registry = createRegistry()
|
|
187
|
-
.register('A', 'B', (a) => (a * 2))
|
|
188
|
-
.register('B', 'C', (b) => (b * 3));
|
|
189
|
-
// First call - may be slower due to BFS
|
|
190
|
-
const start1 = performance.now();
|
|
191
|
-
const converter1 = getConverter(registry, 'A', 'C');
|
|
192
|
-
const end1 = performance.now();
|
|
193
|
-
const time1 = end1 - start1;
|
|
194
|
-
// Second call - should be faster (cached)
|
|
195
|
-
const start2 = performance.now();
|
|
196
|
-
const converter2 = getConverter(registry, 'A', 'C');
|
|
197
|
-
const end2 = performance.now();
|
|
198
|
-
const time2 = end2 - start2;
|
|
199
|
-
expect(converter1).toBeDefined();
|
|
200
|
-
expect(converter2).toBeDefined();
|
|
201
|
-
// Second call should be as fast or faster (cached)
|
|
202
|
-
expect(time2).toBeLessThanOrEqual(time1 * 2); // Allow some variance
|
|
203
|
-
});
|
|
204
|
-
});
|
|
205
|
-
describe('Registry - Unit Accessor API', () => {
|
|
206
|
-
it('provides unit-based accessor API', () => {
|
|
207
|
-
const registry = createRegistry().register('Celsius', 'Fahrenheit', (c) => ((c * 9) / 5 + 32));
|
|
208
|
-
const temp = 0;
|
|
209
|
-
// Use the unit accessor API: registry.celsius.to.fahrenheit(value)
|
|
210
|
-
const result = registry.Celsius.to.Fahrenheit(temp);
|
|
211
|
-
expect(result).toBe(32);
|
|
212
|
-
// Type checking note: result is 'any' due to registry cast, but runtime value is correct
|
|
213
|
-
});
|
|
214
|
-
it('unit accessor is callable to create branded values', () => {
|
|
215
|
-
const registry = createRegistry().register('Celsius', 'Fahrenheit', (c) => ((c * 9) / 5 + 32));
|
|
216
|
-
// Call the unit accessor as a function to brand a value
|
|
217
|
-
const temp = registry.Celsius(25);
|
|
218
|
-
// Verify it's treated as a branded Celsius value
|
|
219
|
-
expect(temp).toBe(25);
|
|
220
|
-
// And can be converted
|
|
221
|
-
const fahrenheit = registry.Celsius.to.Fahrenheit(temp);
|
|
222
|
-
expect(fahrenheit).toBe(77);
|
|
223
|
-
});
|
|
224
|
-
it('callable unit accessor works for unit-centric workflow', () => {
|
|
225
|
-
const registry = createRegistry().register('meters', 'kilometers', {
|
|
226
|
-
to: (m) => (m / 1000),
|
|
227
|
-
from: (km) => (km * 1000)
|
|
228
|
-
});
|
|
229
|
-
// Create branded values by calling the unit accessor
|
|
230
|
-
const distance = registry.meters(5000);
|
|
231
|
-
const km = registry.meters.to.kilometers(distance);
|
|
232
|
-
expect(km).toBe(5);
|
|
233
|
-
});
|
|
234
|
-
it('unit accessor API works with bidirectional converters', () => {
|
|
235
|
-
const registry = createRegistry().register('meters', 'kilometers', {
|
|
236
|
-
to: (m) => (m / 1000),
|
|
237
|
-
from: (km) => (km * 1000)
|
|
238
|
-
});
|
|
239
|
-
const meters = 5000;
|
|
240
|
-
const km = 5;
|
|
241
|
-
// Both directions should work
|
|
242
|
-
const toKm = registry.meters.to.kilometers(meters);
|
|
243
|
-
const toM = registry.kilometers.to.meters(km);
|
|
244
|
-
expect(toKm).toBe(5);
|
|
245
|
-
expect(toM).toBe(5000);
|
|
246
|
-
});
|
|
247
|
-
it('unit accessor API works with multi-hop conversions', () => {
|
|
248
|
-
const registry = createRegistry()
|
|
249
|
-
.register('meters', 'kilometers', {
|
|
250
|
-
to: (m) => (m / 1000),
|
|
251
|
-
from: (km) => (km * 1000)
|
|
252
|
-
})
|
|
253
|
-
.register('kilometers', 'miles', {
|
|
254
|
-
to: (km) => (km * 0.621371),
|
|
255
|
-
from: (mi) => (mi / 0.621371)
|
|
256
|
-
});
|
|
257
|
-
const meters = 5000;
|
|
258
|
-
// Should auto-compose: meters → kilometers → miles
|
|
259
|
-
const miles = registry.meters.to.miles(meters);
|
|
260
|
-
expect(miles).toBeCloseTo(3.106855, 5);
|
|
261
|
-
});
|
|
262
|
-
it('unit accessor API throws error when no converter exists', () => {
|
|
263
|
-
const registry = createRegistry().register('A', 'B', (a) => (a * 2));
|
|
264
|
-
// Try to convert from A to C (C not in registry at all)
|
|
265
|
-
expect(() => {
|
|
266
|
-
registry.A.to.C(10);
|
|
267
|
-
}).toThrow(ConversionError);
|
|
268
|
-
});
|
|
269
|
-
});
|
|
270
|
-
describe('Registry - Metadata Support', () => {
|
|
271
|
-
it('addMetadata attaches metadata to a unit', () => {
|
|
272
|
-
const registry = createRegistry()
|
|
273
|
-
.register('Celsius', 'Fahrenheit', (c) => ((c * 9) / 5 + 32))
|
|
274
|
-
.Celsius.addMetadata({
|
|
275
|
-
abbreviation: '°C',
|
|
276
|
-
format: '${value}°C',
|
|
277
|
-
description: 'Temperature in Celsius'
|
|
278
|
-
});
|
|
279
|
-
expect(registry.Celsius.abbreviation).toBe('°C');
|
|
280
|
-
expect(registry.Celsius.format).toBe('${value}°C');
|
|
281
|
-
expect(registry.Celsius.description).toBe('Temperature in Celsius');
|
|
282
|
-
});
|
|
283
|
-
it('metadata properties are accessible on unit accessors', () => {
|
|
284
|
-
const registry = createRegistry()
|
|
285
|
-
.register('meters', 'kilometers', (m) => (m / 1000))
|
|
286
|
-
.meters.addMetadata({ abbreviation: 'm', symbol: 'm' });
|
|
287
|
-
expect(registry.meters.abbreviation).toBe('m');
|
|
288
|
-
expect(registry.meters.symbol).toBe('m');
|
|
289
|
-
});
|
|
290
|
-
it('addMetadata supports arbitrary custom properties', () => {
|
|
291
|
-
const registry = createRegistry()
|
|
292
|
-
.register('Kelvin', 'Celsius', (k) => (k - 273.15))
|
|
293
|
-
.Kelvin.addMetadata({
|
|
294
|
-
abbreviation: 'K',
|
|
295
|
-
customProp: 'custom value',
|
|
296
|
-
numericProp: 42
|
|
297
|
-
});
|
|
298
|
-
expect(registry.Kelvin.abbreviation).toBe('K');
|
|
299
|
-
expect(registry.Kelvin.customProp).toBe('custom value');
|
|
300
|
-
expect(registry.Kelvin.numericProp).toBe(42);
|
|
301
|
-
});
|
|
302
|
-
it('metadata persists across register operations', () => {
|
|
303
|
-
const registry = createRegistry()
|
|
304
|
-
.register('A', 'B', (a) => (a * 2))
|
|
305
|
-
.A.addMetadata({ abbreviation: 'A' })
|
|
306
|
-
.register('B', 'C', (b) => (b * 3));
|
|
307
|
-
expect(registry.A.abbreviation).toBe('A');
|
|
308
|
-
});
|
|
309
|
-
it('addMetadata can update existing metadata', () => {
|
|
310
|
-
const registry = createRegistry()
|
|
311
|
-
.register('meters', 'feet', (m) => (m * 3.28084))
|
|
312
|
-
.meters.addMetadata({ abbreviation: 'm' })
|
|
313
|
-
.meters.addMetadata({ description: 'Length in meters' });
|
|
314
|
-
expect(registry.meters.abbreviation).toBe('m');
|
|
315
|
-
expect(registry.meters.description).toBe('Length in meters');
|
|
316
|
-
});
|
|
317
|
-
it('addMetadata overwrites existing properties', () => {
|
|
318
|
-
const registry = createRegistry()
|
|
319
|
-
.register('grams', 'kilograms', (g) => (g / 1000))
|
|
320
|
-
.grams.addMetadata({ abbreviation: 'g' })
|
|
321
|
-
.grams.addMetadata({ abbreviation: 'gram' });
|
|
322
|
-
expect(registry.grams.abbreviation).toBe('gram');
|
|
323
|
-
});
|
|
324
|
-
it('multiple units can have independent metadata', () => {
|
|
325
|
-
const registry = createRegistry()
|
|
326
|
-
.register('Celsius', 'Fahrenheit', (c) => ((c * 9) / 5 + 32))
|
|
327
|
-
.register('Fahrenheit', 'Celsius', (f) => (((f - 32) * 5) / 9))
|
|
328
|
-
.Celsius.addMetadata({ abbreviation: '°C' })
|
|
329
|
-
.Fahrenheit.addMetadata({ abbreviation: '°F' });
|
|
330
|
-
expect(registry.Celsius.abbreviation).toBe('°C');
|
|
331
|
-
expect(registry.Fahrenheit.abbreviation).toBe('°F');
|
|
332
|
-
});
|
|
333
|
-
it('metadata returns undefined for non-existent properties', () => {
|
|
334
|
-
const registry = createRegistry()
|
|
335
|
-
.register('meters', 'feet', (m) => (m * 3.28084))
|
|
336
|
-
.meters.addMetadata({ abbreviation: 'm' });
|
|
337
|
-
expect(registry.meters.abbreviation).toBe('m');
|
|
338
|
-
expect(registry.meters.nonExistent).toBeUndefined();
|
|
339
|
-
});
|
|
340
|
-
it('addMetadata returns new registry instance (immutable)', () => {
|
|
341
|
-
const registry1 = createRegistry().register('A', 'B', (a) => (a * 2));
|
|
342
|
-
const registry2 = registry1.A.addMetadata({ abbreviation: 'A' });
|
|
343
|
-
expect(registry1.A.abbreviation).toBeUndefined();
|
|
344
|
-
expect(registry2.A.abbreviation).toBe('A');
|
|
345
|
-
});
|
|
346
|
-
});
|
|
347
|
-
describe('Registry - Unit Accessor Registration', () => {
|
|
348
|
-
it('register method on unit accessor registers converter', () => {
|
|
349
|
-
const registry = createRegistry().Celsius.register('Fahrenheit', (c) => ((c * 9) / 5 + 32));
|
|
350
|
-
const converter = getConverter(registry, 'Celsius', 'Fahrenheit');
|
|
351
|
-
expect(converter).toBeDefined();
|
|
352
|
-
if (converter) {
|
|
353
|
-
expect(converter(0)).toBe(32);
|
|
354
|
-
}
|
|
355
|
-
});
|
|
356
|
-
it('unit accessor register supports bidirectional converters', () => {
|
|
357
|
-
const registry = createRegistry().meters.register('kilometers', {
|
|
358
|
-
to: (m) => (m / 1000),
|
|
359
|
-
from: (km) => (km * 1000)
|
|
360
|
-
});
|
|
361
|
-
const m2km = getConverter(registry, 'meters', 'kilometers');
|
|
362
|
-
const km2m = getConverter(registry, 'kilometers', 'meters');
|
|
363
|
-
expect(m2km).toBeDefined();
|
|
364
|
-
expect(km2m).toBeDefined();
|
|
365
|
-
if (m2km && km2m) {
|
|
366
|
-
expect(m2km(1000)).toBe(1);
|
|
367
|
-
expect(km2m(1)).toBe(1000);
|
|
368
|
-
}
|
|
369
|
-
});
|
|
370
|
-
it('unit accessor register can be chained', () => {
|
|
371
|
-
const registry = createRegistry()
|
|
372
|
-
.Celsius.register('Fahrenheit', (c) => ((c * 9) / 5 + 32))
|
|
373
|
-
.Fahrenheit.register('Kelvin', (f) => ((f - 32) * (5 / 9) + 273.15));
|
|
374
|
-
const c2f = getConverter(registry, 'Celsius', 'Fahrenheit');
|
|
375
|
-
const f2k = getConverter(registry, 'Fahrenheit', 'Kelvin');
|
|
376
|
-
expect(c2f).toBeDefined();
|
|
377
|
-
expect(f2k).toBeDefined();
|
|
378
|
-
});
|
|
379
|
-
it('unit accessor register preserves existing converters', () => {
|
|
380
|
-
const registry = createRegistry()
|
|
381
|
-
.register('A', 'B', (a) => (a * 2))
|
|
382
|
-
.C.register('D', (c) => (c * 3));
|
|
383
|
-
const ab = getConverter(registry, 'A', 'B');
|
|
384
|
-
const cd = getConverter(registry, 'C', 'D');
|
|
385
|
-
expect(ab).toBeDefined();
|
|
386
|
-
expect(cd).toBeDefined();
|
|
387
|
-
});
|
|
388
|
-
it('unit accessor register works with unit accessor API', () => {
|
|
389
|
-
const registry = createRegistry().Celsius.register('Fahrenheit', (c) => ((c * 9) / 5 + 32));
|
|
390
|
-
const temp = 0;
|
|
391
|
-
const result = registry.Celsius.to.Fahrenheit(temp);
|
|
392
|
-
expect(result).toBe(32);
|
|
393
|
-
});
|
|
394
|
-
it('unit accessor register and addMetadata work together', () => {
|
|
395
|
-
const registry = createRegistry()
|
|
396
|
-
.Celsius.addMetadata({ abbreviation: '°C' })
|
|
397
|
-
.Celsius.register('Fahrenheit', (c) => ((c * 9) / 5 + 32));
|
|
398
|
-
expect(registry.Celsius.abbreviation).toBe('°C');
|
|
399
|
-
const converter = getConverter(registry, 'Celsius', 'Fahrenheit');
|
|
400
|
-
expect(converter).toBeDefined();
|
|
401
|
-
});
|
|
402
|
-
});
|
|
403
|
-
//# sourceMappingURL=registry.test.js.map
|