inline-schema/dist/csv-loader/loader.mjs

459 lines
14 KiB
JavaScript

// src/csv-loader/loader.ts
import { parse } from "csv-parse/sync";
// src/parser.ts
var ParseError = class extends Error {
constructor(message, position) {
super(position !== void 0 ? `${message} at position ${position}` : message);
this.position = position;
this.name = "ParseError";
}
};
var Parser = class {
constructor(input) {
this.pos = 0;
this.input = input;
}
peek() {
return this.input[this.pos] || "";
}
consume() {
return this.input[this.pos++] || "";
}
skipWhitespace() {
while (this.pos < this.input.length && /\s/.test(this.input[this.pos])) {
this.pos++;
}
}
match(str) {
return this.input.slice(this.pos, this.pos + str.length) === str;
}
consumeStr(str) {
if (this.match(str)) {
this.pos += str.length;
return true;
}
return false;
}
getPosition() {
return this.pos;
}
getInputLength() {
return this.input.length;
}
parseSchema() {
this.skipWhitespace();
if (this.consumeStr("string")) {
if (this.consumeStr("[")) {
this.skipWhitespace();
if (!this.consumeStr("]")) {
throw new ParseError("Expected ]", this.pos);
}
return { type: "array", element: { type: "string" } };
}
return { type: "string" };
}
if (this.consumeStr("number")) {
if (this.consumeStr("[")) {
this.skipWhitespace();
if (!this.consumeStr("]")) {
throw new ParseError("Expected ]", this.pos);
}
return { type: "array", element: { type: "number" } };
}
return { type: "number" };
}
if (this.consumeStr("boolean")) {
if (this.consumeStr("[")) {
this.skipWhitespace();
if (!this.consumeStr("]")) {
throw new ParseError("Expected ]", this.pos);
}
return { type: "array", element: { type: "boolean" } };
}
return { type: "boolean" };
}
if (this.consumeStr("[")) {
const elements = [];
this.skipWhitespace();
if (this.peek() === "]") {
this.consume();
throw new ParseError("Empty array/tuple not allowed", this.pos);
}
elements.push(this.parseSchema());
this.skipWhitespace();
if (this.consumeStr(";")) {
const remainingElements = [];
while (true) {
this.skipWhitespace();
remainingElements.push(this.parseSchema());
this.skipWhitespace();
if (!this.consumeStr(";")) {
break;
}
}
elements.push(...remainingElements);
}
this.skipWhitespace();
if (!this.consumeStr("]")) {
throw new ParseError("Expected ]", this.pos);
}
if (this.consumeStr("[")) {
this.skipWhitespace();
if (!this.consumeStr("]")) {
throw new ParseError("Expected ]", this.pos);
}
if (elements.length === 1) {
return { type: "array", element: elements[0] };
}
return { type: "array", element: { type: "tuple", elements } };
}
if (elements.length === 1) {
return { type: "array", element: elements[0] };
}
return { type: "tuple", elements };
}
let identifier = "";
while (this.pos < this.input.length && /[a-zA-Z0-9\-_]/.test(this.peek())) {
identifier += this.consume();
}
if (identifier.length > 0) {
if (this.consumeStr("[")) {
this.skipWhitespace();
if (!this.consumeStr("]")) {
throw new ParseError("Expected ]", this.pos);
}
return { type: "array", element: { type: "string" } };
}
return { type: "string" };
}
throw new ParseError(`Unexpected character: ${this.peek()}`, this.pos);
}
};
function parseSchema(schemaString) {
const parser = new Parser(schemaString.trim());
const schema = parser.parseSchema();
if (parser.getPosition() < parser.getInputLength()) {
throw new ParseError("Unexpected input after schema", parser.getPosition());
}
return schema;
}
// src/validator.ts
var ValueParser = class {
constructor(input) {
this.pos = 0;
this.input = input;
}
peek() {
return this.input[this.pos] || "";
}
consume() {
return this.input[this.pos++] || "";
}
skipWhitespace() {
while (this.pos < this.input.length && /\s/.test(this.input[this.pos])) {
this.pos++;
}
}
consumeStr(str) {
if (this.input.slice(this.pos, this.pos + str.length) === str) {
this.pos += str.length;
return true;
}
return false;
}
parseValue(schema, allowOmitBrackets = false) {
this.skipWhitespace();
switch (schema.type) {
case "string":
return this.parseStringValue();
case "number":
return this.parseNumberValue();
case "boolean":
return this.parseBooleanValue();
case "tuple":
return this.parseTupleValue(schema, allowOmitBrackets);
case "array":
return this.parseArrayValue(schema, allowOmitBrackets);
default:
throw new ParseError(`Unknown schema type: ${schema.type}`, this.pos);
}
}
parseStringValue() {
let result = "";
while (this.pos < this.input.length) {
const char = this.peek();
if (char === "\\") {
this.consume();
const nextChar = this.consume();
if (nextChar === ";" || nextChar === "[" || nextChar === "]" || nextChar === "\\") {
result += nextChar;
} else {
result += "\\" + nextChar;
}
} else if (char === ";" || char === "]") {
break;
} else {
result += this.consume();
}
}
return result.trim();
}
parseNumberValue() {
let numStr = "";
while (this.pos < this.input.length && /[\d.\-+eE]/.test(this.peek())) {
numStr += this.consume();
}
const num = parseFloat(numStr);
if (isNaN(num)) {
throw new ParseError("Invalid number", this.pos - numStr.length);
}
return num;
}
parseBooleanValue() {
if (this.consumeStr("true")) {
return true;
}
if (this.consumeStr("false")) {
return false;
}
throw new ParseError("Expected true or false", this.pos);
}
parseTupleValue(schema, allowOmitBrackets) {
let hasOpenBracket = false;
if (this.peek() === "[") {
this.consume();
hasOpenBracket = true;
} else if (!allowOmitBrackets) {
throw new ParseError("Expected [", this.pos);
}
this.skipWhitespace();
if (this.peek() === "]" && hasOpenBracket) {
this.consume();
return [];
}
const result = [];
for (let i = 0; i < schema.elements.length; i++) {
this.skipWhitespace();
result.push(this.parseValue(schema.elements[i], false));
this.skipWhitespace();
if (i < schema.elements.length - 1) {
if (!this.consumeStr(";")) {
throw new ParseError("Expected ;", this.pos);
}
}
}
this.skipWhitespace();
if (hasOpenBracket) {
if (!this.consumeStr("]")) {
throw new ParseError("Expected ]", this.pos);
}
}
return result;
}
parseArrayValue(schema, allowOmitBrackets) {
let hasOpenBracket = false;
const elementIsTupleOrArray = schema.element.type === "tuple" || schema.element.type === "array";
if (this.peek() === "[") {
if (!elementIsTupleOrArray) {
this.consume();
hasOpenBracket = true;
} else if (this.input[this.pos + 1] === "[") {
this.consume();
hasOpenBracket = true;
}
}
if (!hasOpenBracket && !allowOmitBrackets && !elementIsTupleOrArray) {
throw new ParseError("Expected [", this.pos);
}
this.skipWhitespace();
if (this.peek() === "]" && hasOpenBracket) {
this.consume();
return [];
}
const result = [];
while (true) {
this.skipWhitespace();
result.push(this.parseValue(schema.element, false));
this.skipWhitespace();
if (!this.consumeStr(";")) {
break;
}
}
this.skipWhitespace();
if (hasOpenBracket) {
if (!this.consumeStr("]")) {
throw new ParseError("Expected ]", this.pos);
}
}
return result;
}
getPosition() {
return this.pos;
}
getInputLength() {
return this.input.length;
}
};
function parseValue(schema, valueString) {
const parser = new ValueParser(valueString.trim());
const allowOmitBrackets = schema.type === "tuple" || schema.type === "array";
const value = parser.parseValue(schema, allowOmitBrackets);
if (parser.getPosition() < parser.getInputLength()) {
throw new ParseError("Unexpected input after value", parser.getPosition());
}
return value;
}
function createValidator(schema) {
return function validate(value) {
switch (schema.type) {
case "string":
return typeof value === "string";
case "number":
return typeof value === "number" && !isNaN(value);
case "boolean":
return typeof value === "boolean";
case "tuple":
if (!Array.isArray(value)) return false;
if (value.length !== schema.elements.length) return false;
return schema.elements.every(
(elementSchema, index) => createValidator(elementSchema)(value[index])
);
case "array":
if (!Array.isArray(value)) return false;
return value.every((item) => createValidator(schema.element)(item));
default:
return false;
}
};
}
// src/csv-loader/loader.ts
import * as path from "path";
import * as fs from "fs";
function schemaToTypeString(schema) {
switch (schema.type) {
case "string":
return "string";
case "number":
return "number";
case "boolean":
return "boolean";
case "array":
if (schema.element.type === "tuple") {
const tupleElements2 = schema.element.elements.map(schemaToTypeString);
return `[${tupleElements2.join(", ")}]`;
}
return `${schemaToTypeString(schema.element)}[]`;
case "tuple":
const tupleElements = schema.elements.map(schemaToTypeString);
return `[${tupleElements.join(", ")}]`;
default:
return "unknown";
}
}
function generateTypeDefinition(resourceName, propertyConfigs, relativePath) {
const interfaceName = path.basename(resourceName, path.extname(resourceName)).replace(/[^a-zA-Z0-9_$]/g, "_").replace(/^(\d)/, "_$1");
const properties = propertyConfigs.map((config) => ` ${config.name}: ${schemaToTypeString(config.schema)};`).join("\n");
return `declare module "${relativePath}" {
export interface ${interfaceName} {
${properties}
}
export type RowType = ${interfaceName};
const data: ${interfaceName}[];
export default data;
}
`;
}
function csvLoader(content) {
const options = this.getOptions();
const delimiter = options?.delimiter ?? ",";
const quote = options?.quote ?? '"';
const escape = options?.escape ?? "\\";
const bom = options?.bom ?? true;
const comment = options?.comment === false ? void 0 : options?.comment ?? "#";
const trim = options?.trim ?? true;
const emitTypes = options?.emitTypes ?? true;
const typesOutputDir = options?.typesOutputDir ?? "";
const writeToDisk = options?.writeToDisk ?? false;
const records = parse(content, {
delimiter,
quote,
escape,
bom,
comment,
trim,
relax_column_count: true
});
if (records.length < 2) {
throw new Error("CSV must have at least 2 rows: headers and schemas");
}
const headers = records[0];
const schemas = records[1];
if (headers.length !== schemas.length) {
throw new Error(
`Header count (${headers.length}) does not match schema count (${schemas.length})`
);
}
const propertyConfigs = headers.map((header, index) => {
const schemaString = schemas[index];
const schema = parseSchema(schemaString);
return {
name: header,
schema,
validator: createValidator(schema),
parser: (valueString) => parseValue(schema, valueString)
};
});
const dataRows = records.slice(2);
const objects = dataRows.map((row, rowIndex) => {
const obj = {};
propertyConfigs.forEach((config, colIndex) => {
const rawValue = row[colIndex] ?? "";
try {
const parsed = config.parser(rawValue);
if (!config.validator(parsed)) {
throw new Error(
`Validation failed for property "${config.name}" at row ${rowIndex + 3}: ${rawValue}`
);
}
obj[config.name] = parsed;
} catch (error) {
if (error instanceof Error) {
throw new Error(
`Failed to parse property "${config.name}" at row ${rowIndex + 3}, column ${colIndex + 1}: ${error.message}`
);
}
throw error;
}
});
return obj;
});
const json = JSON.stringify(objects, null, 2);
if (emitTypes) {
const context = this.context || "";
let relativePath = this.resourcePath.replace(context, "");
if (relativePath.startsWith("\\") || relativePath.startsWith("/")) {
relativePath = relativePath.substring(1);
}
relativePath = relativePath.replace(/\\/g, "/");
const dtsFileName = relativePath.replace(/\.csv$/, ".d.ts");
const outputPath = typesOutputDir ? path.join(typesOutputDir, dtsFileName) : dtsFileName;
const dtsContent = generateTypeDefinition(this.resourcePath, propertyConfigs, `./${relativePath}`);
if (writeToDisk) {
const absolutePath = path.join(this.context || process.cwd(), typesOutputDir || "", dtsFileName);
fs.mkdirSync(path.dirname(absolutePath), { recursive: true });
fs.writeFileSync(absolutePath, dtsContent);
} else {
this.emitFile?.(outputPath, dtsContent);
}
}
return `export default ${json};`;
}
export {
csvLoader as default
};