Expand $variants to accept an array of csv paths, concatenating their rows. Detect a path by a .csv suffix on the first line instead of a newline, so inline csv and paths are self-documenting and the rule applies uniformly to single values and array elements.
90 lines
3.0 KiB
TypeScript
90 lines
3.0 KiB
TypeScript
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 first line does not end in .csv', () => {
|
|
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('concatenates rows from an array of csv paths', () => {
|
|
const defs = new Map<string, DefFile[]>([
|
|
['pkg/parts/a.csv', [defFile('pkg/parts/a.csv', 'id\nstring\nred')]],
|
|
['pkg/parts/b.csv', [defFile('pkg/parts/b.csv', 'id\nstring\nblack')]],
|
|
]);
|
|
const rows = expandVariants(['./a.csv', './b.csv'], 'pkg/parts/board.yaml', defs, 'src');
|
|
expect(rows).toEqual([{ id: 'red' }, { id: 'black' }]);
|
|
});
|
|
|
|
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 or string array', () => {
|
|
expect(() => expandVariants(42, 'pkg/def.yaml', new Map(), 'src')).toThrow(
|
|
/must be a path or inline CSV string, or an array of them/,
|
|
);
|
|
});
|
|
|
|
it('throws on an empty $variants array', () => {
|
|
expect(() => expandVariants([], 'pkg/def.yaml', new Map(), 'src')).toThrow(
|
|
/must not be empty/,
|
|
);
|
|
});
|
|
}); |