Add utility files: spec-parser, file-writer, helpers, wizard
This commit is contained in:
122
src/utils/spec-parser.ts
Normal file
122
src/utils/spec-parser.ts
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
import * as fs from 'fs';
|
||||||
|
import * as yaml from 'js-yaml';
|
||||||
|
import { CLISpec } from '../types/spec.js';
|
||||||
|
import { validateSpec } from '../validators/schema.js';
|
||||||
|
|
||||||
|
export interface ParseOptions {
|
||||||
|
filePath?: string;
|
||||||
|
content?: string;
|
||||||
|
format?: 'auto' | 'json' | 'yaml';
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ParseResult {
|
||||||
|
success: boolean;
|
||||||
|
spec?: CLISpec;
|
||||||
|
errors?: string[];
|
||||||
|
source?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function detectFormat(content: string): 'json' | 'yaml' {
|
||||||
|
const trimmed = content.trim();
|
||||||
|
if (trimmed.startsWith('{') || trimmed.startsWith('[')) {
|
||||||
|
return 'json';
|
||||||
|
}
|
||||||
|
return 'yaml';
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseJSON(content: string): unknown {
|
||||||
|
return JSON.parse(content);
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseYAML(content: string): unknown {
|
||||||
|
return yaml.load(content);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseSpec(options: ParseOptions): ParseResult {
|
||||||
|
let content: string;
|
||||||
|
let source: string;
|
||||||
|
|
||||||
|
if (options.content) {
|
||||||
|
content = options.content;
|
||||||
|
source = 'input';
|
||||||
|
} else if (options.filePath) {
|
||||||
|
if (!fs.existsSync(options.filePath)) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
errors: [`File not found: ${options.filePath}`],
|
||||||
|
source: options.filePath,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
content = fs.readFileSync(options.filePath, 'utf-8');
|
||||||
|
source = options.filePath;
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
errors: ['Either filePath or content must be provided'],
|
||||||
|
source: undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let parsed: unknown;
|
||||||
|
try {
|
||||||
|
const format = options.format === 'auto' || !options.format ? detectFormat(content) : options.format;
|
||||||
|
|
||||||
|
if (format === 'json') {
|
||||||
|
parsed = parseJSON(content);
|
||||||
|
} else {
|
||||||
|
parsed = parseYAML(content);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
const message = error instanceof Error ? error.message : 'Unknown parsing error';
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
errors: [`Failed to parse ${source}: ${message}`],
|
||||||
|
source,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!parsed || typeof parsed !== 'object') {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
errors: [`Invalid spec format in ${source}: expected object`],
|
||||||
|
source,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const validation = validateSpec(parsed);
|
||||||
|
if (!validation.success) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
errors: validation.errors,
|
||||||
|
source,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
spec: validation.data,
|
||||||
|
source,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function loadSpec(filePath: string): ParseResult {
|
||||||
|
return parseSpec({ filePath });
|
||||||
|
}
|
||||||
|
|
||||||
|
export function loadSpecFromStdin(): Promise<ParseResult> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
let content = '';
|
||||||
|
process.stdin.setEncoding('utf-8');
|
||||||
|
|
||||||
|
process.stdin.on('readable', () => {
|
||||||
|
let chunk;
|
||||||
|
while ((chunk = process.stdin.read()) !== null) {
|
||||||
|
content += chunk;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
process.stdin.on('end', () => {
|
||||||
|
resolve(parseSpec({ content }));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user