feat(bgm): add board game manifest loader
Parse yaml/json/toml and markdown code blocks into packages, expanding $variants via typed-csv and collecting parts, surfaces, and setups by include patterns. Ships zod validation, a vitest config, and 16 tests.
This commit is contained in:
@@ -0,0 +1,75 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { parseCsvData, expandVariants } from './variants.js';
|
||||
import type { DefFile } from './types.js';
|
||||
|
||||
function defFile(name: string, text: string): DefFile {
|
||||
return { name, text, source: name, kind: 'csv' };
|
||||
}
|
||||
|
||||
describe('parseCsvData', () => {
|
||||
it('parses the spec example with an empty array', () => {
|
||||
const csv = [
|
||||
'name,parents',
|
||||
'string,string[]',
|
||||
'clark,[jonathan;martha]',
|
||||
'bruce,[]',
|
||||
].join('\n');
|
||||
const { rows, header } = parseCsvData(csv, 'test');
|
||||
expect(header).toEqual(['name', 'parents']);
|
||||
expect(rows).toEqual([
|
||||
{ name: 'clark', parents: ['jonathan', 'martha'] },
|
||||
{ name: 'bruce', parents: [] },
|
||||
]);
|
||||
});
|
||||
|
||||
it('parses a crop tuple', () => {
|
||||
const csv = [
|
||||
'id,faceCrop',
|
||||
'string,[number;number;number;number]',
|
||||
'fish,[0;0;5;2]',
|
||||
'grain,[1;0;5;2]',
|
||||
].join('\n');
|
||||
const { rows } = parseCsvData(csv, 'test');
|
||||
expect(rows).toEqual([
|
||||
{ id: 'fish', faceCrop: [0, 0, 5, 2] },
|
||||
{ id: 'grain', faceCrop: [1, 0, 5, 2] },
|
||||
]);
|
||||
});
|
||||
|
||||
it('throws a BgmError on a type mismatch', () => {
|
||||
const csv = ['n', 'number', 'not-a-number'].join('\n');
|
||||
expect(() => parseCsvData(csv, 'test')).toThrow(/Invalid CSV/);
|
||||
});
|
||||
|
||||
it('allows a header and schema with no data rows', () => {
|
||||
const { rows } = parseCsvData('a\nstring', 'test');
|
||||
expect(rows).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('expandVariants', () => {
|
||||
it('parses inline CSV when the value contains a newline', () => {
|
||||
const rows = expandVariants('a,b\nstring,number\nx,1', 'pkg/def.yaml', new Map(), 'src');
|
||||
expect(rows).toEqual([{ a: 'x', b: 1 }]);
|
||||
});
|
||||
|
||||
it('resolves a path against the def file directory', () => {
|
||||
const defs = new Map<string, DefFile[]>([
|
||||
['pkg/parts/seats.csv', [defFile('pkg/parts/seats.csv', 'seat\nnumber\n0\n1')]],
|
||||
]);
|
||||
const rows = expandVariants('./seats.csv', 'pkg/parts/board.yaml', defs, 'src');
|
||||
expect(rows).toEqual([{ seat: 0 }, { seat: 1 }]);
|
||||
});
|
||||
|
||||
it('throws when the referenced csv is missing', () => {
|
||||
expect(() => expandVariants('./nope.csv', 'pkg/def.yaml', new Map(), 'src')).toThrow(
|
||||
/CSV not found/,
|
||||
);
|
||||
});
|
||||
|
||||
it('throws when $variants is not a string', () => {
|
||||
expect(() => expandVariants(42, 'pkg/def.yaml', new Map(), 'src')).toThrow(
|
||||
/must be a path or inline CSV/,
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user