Skip to content

Commit 36af79d

Browse files
nullvariantclaude
andauthored
feat(errors): add unified validation error types (#228)
Add FieldValidationError and UnifiedValidationResult interfaces with readonly properties for type safety. Add deprecated toFieldError() helper function for backward compatibility during migration. - FieldValidationError: field-level error with field name and message - UnifiedValidationResult: validation result with isValid and errors - toFieldError(): parses "field: message" format strings (deprecated) Test coverage: 8 test cases including edge cases for colon handling, empty strings, whitespace trimming, and whitespace-only field names. 🖥️ IDE: [Cursor](https://cursor.sh) 🔌 Extension: [Claude Code](https://claude.ai/download) Model-Raw: claude-opus-4-5-20251101 Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
1 parent d0629e9 commit 36af79d

2 files changed

Lines changed: 148 additions & 0 deletions

File tree

extensions/git-id-switcher/src/core/errors.ts

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -293,3 +293,84 @@ export function getUserSafeMessage(error: unknown): string {
293293
// For non-SecurityError, return a generic message
294294
return 'An unexpected error occurred';
295295
}
296+
297+
// ============================================================================
298+
// Unified Validation Types (Phase 4)
299+
// ============================================================================
300+
301+
/**
302+
* Field-level validation error
303+
*
304+
* Represents a validation error for a specific field.
305+
* Properties are readonly to ensure immutability after creation.
306+
*/
307+
export interface FieldValidationError {
308+
/** Field name that failed validation */
309+
readonly field: string;
310+
/** Error message describing the validation failure */
311+
readonly message: string;
312+
}
313+
314+
/**
315+
* Unified validation result
316+
*
317+
* A consistent structure for validation results across the codebase.
318+
* Properties are readonly to ensure immutability after creation.
319+
*/
320+
export interface UnifiedValidationResult {
321+
/** Whether validation passed */
322+
readonly isValid: boolean;
323+
/** List of field-level errors (empty if isValid is true) */
324+
readonly errors: readonly FieldValidationError[];
325+
}
326+
327+
/**
328+
* Parse a string error message into a FieldValidationError
329+
*
330+
* @deprecated This function exists for backward compatibility during migration.
331+
* New code should use structured validation that returns FieldValidationError directly.
332+
*
333+
* @param error - Error string in format "field: message" or plain message
334+
* @returns FieldValidationError with parsed field and message
335+
*
336+
* @example
337+
* ```typescript
338+
* // Standard format with colon separator
339+
* toFieldError('email: Invalid format')
340+
* // Returns: { field: 'email', message: 'Invalid format' }
341+
*
342+
* // No colon - treated as unknown field
343+
* toFieldError('Unknown error')
344+
* // Returns: { field: 'unknown', message: 'Unknown error' }
345+
*
346+
* // Empty string
347+
* toFieldError('')
348+
* // Returns: { field: 'unknown', message: '' }
349+
*
350+
* // Empty field name (colon at start) - treated as unknown
351+
* toFieldError(': message only')
352+
* // Returns: { field: 'unknown', message: ': message only' }
353+
* ```
354+
*/
355+
export function toFieldError(error: string): FieldValidationError {
356+
const colonIndex = error.indexOf(':');
357+
if (colonIndex === -1) {
358+
return {
359+
field: 'unknown',
360+
message: error,
361+
};
362+
}
363+
364+
const field = error.slice(0, colonIndex).trim();
365+
const message = error.slice(colonIndex + 1).trim();
366+
367+
// If field is empty after trim, use 'unknown'
368+
if (!field) {
369+
return {
370+
field: 'unknown',
371+
message: error,
372+
};
373+
}
374+
375+
return { field, message };
376+
}

extensions/git-id-switcher/src/test/errors.test.ts

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
wrapError,
1414
isSecurityError,
1515
getUserSafeMessage,
16+
toFieldError,
1617
} from '../core/errors';
1718

1819
/**
@@ -263,6 +264,71 @@ function testGetUserSafeMessageFunction(): void {
263264
console.log('✅ getUserSafeMessage function tests passed!');
264265
}
265266

267+
/**
268+
* Test toFieldError function
269+
*/
270+
function testToFieldError(): void {
271+
console.log('Testing toFieldError...');
272+
273+
// Should parse colon-separated format
274+
{
275+
const result = toFieldError('email: Invalid format');
276+
assert.strictEqual(result.field, 'email');
277+
assert.strictEqual(result.message, 'Invalid format');
278+
}
279+
280+
// Should handle no colon (plain error message)
281+
{
282+
const result = toFieldError('Unknown error');
283+
assert.strictEqual(result.field, 'unknown');
284+
assert.strictEqual(result.message, 'Unknown error');
285+
}
286+
287+
// Should handle empty string
288+
{
289+
const result = toFieldError('');
290+
assert.strictEqual(result.field, 'unknown');
291+
assert.strictEqual(result.message, '');
292+
}
293+
294+
// Should handle multiple colons (only split on first)
295+
{
296+
const result = toFieldError('path: /home/user: invalid');
297+
assert.strictEqual(result.field, 'path');
298+
assert.strictEqual(result.message, '/home/user: invalid');
299+
}
300+
301+
// Should handle colon with no message
302+
{
303+
const result = toFieldError('field:');
304+
assert.strictEqual(result.field, 'field');
305+
assert.strictEqual(result.message, '');
306+
}
307+
308+
// Should handle colon at the beginning (empty field)
309+
{
310+
const result = toFieldError(': message only');
311+
assert.strictEqual(result.field, 'unknown');
312+
assert.strictEqual(result.message, ': message only');
313+
}
314+
315+
// Should trim whitespace
316+
{
317+
const result = toFieldError(' name : has spaces ');
318+
assert.strictEqual(result.field, 'name');
319+
assert.strictEqual(result.message, 'has spaces');
320+
}
321+
322+
// Should handle whitespace-only field name
323+
{
324+
const result = toFieldError(' : message');
325+
assert.strictEqual(result.field, 'unknown');
326+
assert.strictEqual(result.message, ' : message');
327+
}
328+
329+
console.log('✅ toFieldError tests passed!');
330+
}
331+
266332
/**
267333
* Run all error tests
268334
*/
@@ -276,6 +342,7 @@ export async function runErrorTests(): Promise<void> {
276342
testFactoryFunctions();
277343
testTypeGuards();
278344
testGetUserSafeMessageFunction();
345+
testToFieldError();
279346

280347
console.log('\n✅ All error tests passed!\n');
281348
} catch (error) {

0 commit comments

Comments
 (0)