mirror of
https://github.com/waaraawa/ByteGrid.git
synced 2026-07-22 06:41:47 +00:00
test(core): implement Parser module with full test coverage
- Add parser.ts with YAML to ByteGridConfig conversion - Support all field properties including bitfields - Handle both string and number offset formats (YAML parses 12 as number) - Apply default layout value of 16 - Add comprehensive error handling with ParseError - Add 14 test cases covering all parsing scenarios - All tests passing (14/14)
This commit is contained in:
parent
8ad49736e0
commit
d43703912e
3 changed files with 398 additions and 2 deletions
|
|
@ -4,9 +4,9 @@
|
|||
|
||||
export * from './types';
|
||||
export * from './errors';
|
||||
export { parse } from './parser';
|
||||
|
||||
// TODO: Export parser, validator, layoutEngine, svgRenderer when implemented
|
||||
// export { parse } from './parser';
|
||||
// TODO: Export validator, layoutEngine, svgRenderer when implemented
|
||||
// export { validate } from './validator';
|
||||
// export { createLayout } from './layoutEngine';
|
||||
// export { renderSVG } from './svgRenderer';
|
||||
|
|
|
|||
162
packages/core/src/parser.ts
Normal file
162
packages/core/src/parser.ts
Normal file
|
|
@ -0,0 +1,162 @@
|
|||
/**
|
||||
* Parser module
|
||||
* Parses YAML input to ByteGridConfig
|
||||
*/
|
||||
|
||||
import * as yaml from 'js-yaml';
|
||||
import { ByteGridConfig, Field } from './types';
|
||||
import { ParseError } from './errors';
|
||||
|
||||
/**
|
||||
* Parse YAML string to ByteGridConfig
|
||||
* @param source YAML string
|
||||
* @returns Parsed ByteGridConfig
|
||||
* @throws ParseError if parsing fails or required fields are missing
|
||||
*/
|
||||
export function parse(source: string): ByteGridConfig {
|
||||
let parsed: unknown;
|
||||
|
||||
// Parse YAML
|
||||
try {
|
||||
parsed = yaml.load(source);
|
||||
} catch (error) {
|
||||
throw new ParseError(
|
||||
`Failed to parse YAML: ${error instanceof Error ? error.message : String(error)}`
|
||||
);
|
||||
}
|
||||
|
||||
// Validate parsed result is an object
|
||||
if (!parsed || typeof parsed !== 'object') {
|
||||
throw new ParseError('Invalid YAML: expected an object');
|
||||
}
|
||||
|
||||
const obj = parsed as Record<string, unknown>;
|
||||
|
||||
// Validate required top-level fields
|
||||
if (!obj.name || typeof obj.name !== 'string') {
|
||||
throw new ParseError('Missing or invalid required field: name');
|
||||
}
|
||||
|
||||
if (!obj.size || typeof obj.size !== 'number') {
|
||||
throw new ParseError('Missing or invalid required field: size');
|
||||
}
|
||||
|
||||
if (!obj.fields || !Array.isArray(obj.fields)) {
|
||||
throw new ParseError('Missing or invalid required field: fields');
|
||||
}
|
||||
|
||||
if (obj.fields.length === 0) {
|
||||
throw new ParseError('Fields array must contain at least one field');
|
||||
}
|
||||
|
||||
// Parse layout (optional, defaults to 16)
|
||||
const layout = obj.layout !== undefined ? Number(obj.layout) : 16;
|
||||
|
||||
// Parse fields
|
||||
const fields: Field[] = obj.fields.map((fieldObj, index) => {
|
||||
if (!fieldObj || typeof fieldObj !== 'object') {
|
||||
throw new ParseError(`Field at index ${index} is not an object`, index);
|
||||
}
|
||||
|
||||
const field = fieldObj as Record<string, unknown>;
|
||||
|
||||
// Validate required field properties
|
||||
if (field.offset === undefined) {
|
||||
throw new ParseError(`Field at index ${index} is missing required property: offset`, index);
|
||||
}
|
||||
|
||||
// offset can be string or number (YAML parses "12" as number)
|
||||
const offset = typeof field.offset === 'number' ? String(field.offset) : field.offset;
|
||||
if (typeof offset !== 'string') {
|
||||
throw new ParseError(`Field at index ${index} has invalid offset type`, index);
|
||||
}
|
||||
|
||||
if (!field.name || typeof field.name !== 'string') {
|
||||
throw new ParseError(`Field at index ${index} is missing required property: name`, index);
|
||||
}
|
||||
|
||||
if (!field.type || typeof field.type !== 'string') {
|
||||
throw new ParseError(`Field at index ${index} is missing required property: type`, index);
|
||||
}
|
||||
|
||||
// Build field object with required properties
|
||||
const parsedField: Field = {
|
||||
offset: offset,
|
||||
name: field.name,
|
||||
type: field.type,
|
||||
};
|
||||
|
||||
// Add optional properties if present
|
||||
if (field.value !== undefined) {
|
||||
parsedField.value = String(field.value);
|
||||
}
|
||||
|
||||
if (field.description !== undefined) {
|
||||
parsedField.description = String(field.description);
|
||||
}
|
||||
|
||||
if (field.color !== undefined && typeof field.color === 'string') {
|
||||
parsedField.color = field.color as import('./types').ColorName;
|
||||
}
|
||||
|
||||
if (field.endianness !== undefined && typeof field.endianness === 'string') {
|
||||
parsedField.endianness = field.endianness as 'little' | 'big';
|
||||
}
|
||||
|
||||
// Parse bitfields if present
|
||||
if (field.bitfields !== undefined) {
|
||||
if (!Array.isArray(field.bitfields)) {
|
||||
throw new ParseError(
|
||||
`Field at index ${index} has invalid bitfields: expected array`,
|
||||
index
|
||||
);
|
||||
}
|
||||
|
||||
parsedField.bitfields = field.bitfields.map((bf, bfIndex) => {
|
||||
if (!bf || typeof bf !== 'object') {
|
||||
throw new ParseError(
|
||||
`Field at index ${index}, bitfield at index ${bfIndex} is not an object`,
|
||||
index
|
||||
);
|
||||
}
|
||||
|
||||
const bitfield = bf as Record<string, unknown>;
|
||||
|
||||
if (!bitfield.name || typeof bitfield.name !== 'string') {
|
||||
throw new ParseError(
|
||||
`Field at index ${index}, bitfield at index ${bfIndex} is missing required property: name`,
|
||||
index
|
||||
);
|
||||
}
|
||||
|
||||
if (!bitfield.bits || typeof bitfield.bits !== 'string') {
|
||||
throw new ParseError(
|
||||
`Field at index ${index}, bitfield at index ${bfIndex} is missing required property: bits`,
|
||||
index
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
name: bitfield.name,
|
||||
bits: bitfield.bits,
|
||||
description: bitfield.description !== undefined ? String(bitfield.description) : undefined,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
return parsedField;
|
||||
});
|
||||
|
||||
// Parse colorScheme (optional)
|
||||
const colorScheme = obj.colorScheme && typeof obj.colorScheme === 'string'
|
||||
? (obj.colorScheme as 'default' | 'dark' | 'light')
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
name: obj.name,
|
||||
size: obj.size,
|
||||
layout,
|
||||
colorScheme,
|
||||
fields,
|
||||
};
|
||||
}
|
||||
234
packages/core/tests/parser.test.ts
Normal file
234
packages/core/tests/parser.test.ts
Normal file
|
|
@ -0,0 +1,234 @@
|
|||
/**
|
||||
* Parser module tests
|
||||
* Tests for parsing YAML input to ByteGridConfig
|
||||
*/
|
||||
|
||||
import { parse } from '../src/parser';
|
||||
import { ParseError } from '../src/errors';
|
||||
|
||||
describe('Parser', () => {
|
||||
describe('parse()', () => {
|
||||
it('should parse valid YAML with minimal fields', () => {
|
||||
const yaml = `
|
||||
name: Test Structure
|
||||
size: 16
|
||||
fields:
|
||||
- offset: 0-3
|
||||
name: Header
|
||||
type: uint32_t
|
||||
`;
|
||||
|
||||
const result = parse(yaml);
|
||||
|
||||
expect(result.name).toBe('Test Structure');
|
||||
expect(result.size).toBe(16);
|
||||
expect(result.fields).toHaveLength(1);
|
||||
expect(result.fields[0].offset).toBe('0-3');
|
||||
expect(result.fields[0].name).toBe('Header');
|
||||
expect(result.fields[0].type).toBe('uint32_t');
|
||||
});
|
||||
|
||||
it('should apply default layout value of 16', () => {
|
||||
const yaml = `
|
||||
name: Test
|
||||
size: 32
|
||||
fields:
|
||||
- offset: 0-3
|
||||
name: Field1
|
||||
type: uint32_t
|
||||
`;
|
||||
|
||||
const result = parse(yaml);
|
||||
|
||||
expect(result.layout).toBe(16);
|
||||
});
|
||||
|
||||
it('should use custom layout if provided', () => {
|
||||
const yaml = `
|
||||
name: Test
|
||||
size: 32
|
||||
layout: 8
|
||||
fields:
|
||||
- offset: 0-3
|
||||
name: Field1
|
||||
type: uint32_t
|
||||
`;
|
||||
|
||||
const result = parse(yaml);
|
||||
|
||||
expect(result.layout).toBe(8);
|
||||
});
|
||||
|
||||
it('should parse fields with all optional properties', () => {
|
||||
const yaml = `
|
||||
name: WAV Header
|
||||
size: 44
|
||||
fields:
|
||||
- offset: 0-3
|
||||
name: ChunkID
|
||||
type: char[4]
|
||||
value: "RIFF"
|
||||
description: "RIFF magic number"
|
||||
color: blue
|
||||
endianness: little
|
||||
`;
|
||||
|
||||
const result = parse(yaml);
|
||||
|
||||
const field = result.fields[0];
|
||||
expect(field.value).toBe('RIFF');
|
||||
expect(field.description).toBe('RIFF magic number');
|
||||
expect(field.color).toBe('blue');
|
||||
expect(field.endianness).toBe('little');
|
||||
});
|
||||
|
||||
it('should parse multiple fields', () => {
|
||||
const yaml = `
|
||||
name: Test
|
||||
size: 16
|
||||
fields:
|
||||
- offset: 0-3
|
||||
name: Field1
|
||||
type: uint32_t
|
||||
- offset: 4-7
|
||||
name: Field2
|
||||
type: uint32_t
|
||||
- offset: 8-15
|
||||
name: Field3
|
||||
type: uint64_t
|
||||
`;
|
||||
|
||||
const result = parse(yaml);
|
||||
|
||||
expect(result.fields).toHaveLength(3);
|
||||
expect(result.fields[0].name).toBe('Field1');
|
||||
expect(result.fields[1].name).toBe('Field2');
|
||||
expect(result.fields[2].name).toBe('Field3');
|
||||
});
|
||||
|
||||
it('should parse bitfields', () => {
|
||||
const yaml = `
|
||||
name: TCP Header
|
||||
size: 20
|
||||
fields:
|
||||
- offset: 12
|
||||
name: Flags
|
||||
type: uint8_t
|
||||
bitfields:
|
||||
- name: CWR
|
||||
bits: "7"
|
||||
- name: ECE
|
||||
bits: "6"
|
||||
- name: URG
|
||||
bits: "5"
|
||||
`;
|
||||
|
||||
const result = parse(yaml);
|
||||
|
||||
const field = result.fields[0];
|
||||
expect(field.bitfields).toBeDefined();
|
||||
expect(field.bitfields).toHaveLength(3);
|
||||
expect(field.bitfields![0].name).toBe('CWR');
|
||||
expect(field.bitfields![0].bits).toBe('7');
|
||||
});
|
||||
|
||||
it('should throw ParseError for invalid YAML', () => {
|
||||
const yaml = `
|
||||
name: Test
|
||||
size: 16
|
||||
fields:
|
||||
- offset: 0-3
|
||||
name: Field1 # Wrong indentation
|
||||
type: uint32_t
|
||||
`;
|
||||
|
||||
expect(() => parse(yaml)).toThrow(ParseError);
|
||||
});
|
||||
|
||||
it('should throw ParseError for missing required field: name', () => {
|
||||
const yaml = `
|
||||
size: 16
|
||||
fields:
|
||||
- offset: 0-3
|
||||
name: Field1
|
||||
type: uint32_t
|
||||
`;
|
||||
|
||||
expect(() => parse(yaml)).toThrow(ParseError);
|
||||
expect(() => parse(yaml)).toThrow(/required field.*name/i);
|
||||
});
|
||||
|
||||
it('should throw ParseError for missing required field: size', () => {
|
||||
const yaml = `
|
||||
name: Test
|
||||
fields:
|
||||
- offset: 0-3
|
||||
name: Field1
|
||||
type: uint32_t
|
||||
`;
|
||||
|
||||
expect(() => parse(yaml)).toThrow(ParseError);
|
||||
expect(() => parse(yaml)).toThrow(/required field.*size/i);
|
||||
});
|
||||
|
||||
it('should throw ParseError for missing required field: fields', () => {
|
||||
const yaml = `
|
||||
name: Test
|
||||
size: 16
|
||||
`;
|
||||
|
||||
expect(() => parse(yaml)).toThrow(ParseError);
|
||||
expect(() => parse(yaml)).toThrow(/required field.*fields/i);
|
||||
});
|
||||
|
||||
it('should throw ParseError for empty fields array', () => {
|
||||
const yaml = `
|
||||
name: Test
|
||||
size: 16
|
||||
fields: []
|
||||
`;
|
||||
|
||||
expect(() => parse(yaml)).toThrow(ParseError);
|
||||
expect(() => parse(yaml)).toThrow(/at least one field/i);
|
||||
});
|
||||
|
||||
it('should throw ParseError for field missing offset', () => {
|
||||
const yaml = `
|
||||
name: Test
|
||||
size: 16
|
||||
fields:
|
||||
- name: Field1
|
||||
type: uint32_t
|
||||
`;
|
||||
|
||||
expect(() => parse(yaml)).toThrow(ParseError);
|
||||
expect(() => parse(yaml)).toThrow(/field.*offset/i);
|
||||
});
|
||||
|
||||
it('should throw ParseError for field missing name', () => {
|
||||
const yaml = `
|
||||
name: Test
|
||||
size: 16
|
||||
fields:
|
||||
- offset: 0-3
|
||||
type: uint32_t
|
||||
`;
|
||||
|
||||
expect(() => parse(yaml)).toThrow(ParseError);
|
||||
expect(() => parse(yaml)).toThrow(/field.*name/i);
|
||||
});
|
||||
|
||||
it('should throw ParseError for field missing type', () => {
|
||||
const yaml = `
|
||||
name: Test
|
||||
size: 16
|
||||
fields:
|
||||
- offset: 0-3
|
||||
name: Field1
|
||||
`;
|
||||
|
||||
expect(() => parse(yaml)).toThrow(ParseError);
|
||||
expect(() => parse(yaml)).toThrow(/field.*type/i);
|
||||
});
|
||||
});
|
||||
});
|
||||
Loading…
Reference in a new issue