import { z } from 'zod';
import { parseString as parseXml, Builder as XmlBuilder } from 'xml2js';
import * as yaml from 'js-yaml';
import { parse as parseCsv } from 'csv-parse/sync';
import { stringify as stringifyCsv } from 'csv-stringify/sync';
import * as toml from '@iarna/toml';
import { BaseTool } from './base.js';
import { logger } from '../utils/logger.js';
/**
* Supported data formats
*/
const formatEnum = z.enum(['json', 'xml', 'yaml', 'csv', 'toml']);
type DataFormat = z.infer<typeof formatEnum>;
/**
* Conversion options schema
*/
const conversionOptionsSchema = z
.object({
pretty: z.boolean().optional().default(true).describe('Pretty print output (with indentation)'),
indent: z.number().int().min(0).max(8).optional().default(2).describe('Indentation spaces for pretty printing'),
csvDelimiter: z.string().optional().default(',').describe('CSV delimiter character'),
csvHeaders: z.boolean().optional().default(true).describe('CSV has headers in first row'),
})
.optional();
/**
* Type conversion tool schema
*/
const typeConversionSchema = z.object({
input: z.string().min(1).describe('Input data as string'),
fromFormat: formatEnum.describe('Source data format'),
toFormat: formatEnum.describe('Target data format'),
options: conversionOptionsSchema,
});
type TypeConversionParams = z.infer<typeof typeConversionSchema>;
/**
* TypeConversionTool - Convert between different data formats
*/
export class TypeConversionTool extends BaseTool<typeof typeConversionSchema> {
readonly name = 'typeconversion';
readonly description =
'Convert data between different formats: JSON, XML, YAML, CSV, and TOML. Supports pretty printing, minification, and format-specific options like CSV delimiters and headers.';
readonly schema = typeConversionSchema;
protected async execute(params: TypeConversionParams): Promise<string> {
logger.info(`Converting from ${params.fromFormat} to ${params.toFormat}`);
try {
// Parse input to intermediate format (JavaScript object)
const data = await this.parseInput(params.input, params.fromFormat, params.options);
// Convert to target format
const output = await this.formatOutput(data, params.toFormat, params.options);
logger.info(`Conversion completed successfully`);
return output;
} catch (error) {
throw new Error(
`Conversion failed: ${error instanceof Error ? error.message : 'Unknown error'}`
);
}
}
/**
* Parse input string to JavaScript object
*/
private async parseInput(
input: string,
format: DataFormat,
options?: z.infer<typeof conversionOptionsSchema>
): Promise<any> {
switch (format) {
case 'json':
return this.parseJson(input);
case 'xml':
return await this.parseXmlToObject(input);
case 'yaml':
return this.parseYaml(input);
case 'csv':
return this.parseCsvToObject(input, options);
case 'toml':
return this.parseToml(input);
default:
throw new Error(`Unsupported input format: ${format}`);
}
}
/**
* Format JavaScript object to output string
*/
private async formatOutput(
data: any,
format: DataFormat,
options?: z.infer<typeof conversionOptionsSchema>
): Promise<string> {
switch (format) {
case 'json':
return this.formatJson(data, options);
case 'xml':
return await this.formatXml(data, options);
case 'yaml':
return this.formatYaml(data, options);
case 'csv':
return this.formatCsv(data, options);
case 'toml':
return this.formatToml(data);
default:
throw new Error(`Unsupported output format: ${format}`);
}
}
/**
* Parse JSON string
*/
private parseJson(input: string): any {
try {
return JSON.parse(input);
} catch (error) {
throw new Error(`Invalid JSON: ${error instanceof Error ? error.message : 'Parse error'}`);
}
}
/**
* Format data as JSON
*/
private formatJson(data: any, options?: z.infer<typeof conversionOptionsSchema>): string {
const indent = options?.pretty ? (options?.indent ?? 2) : 0;
return JSON.stringify(data, null, indent);
}
/**
* Parse XML string to object
*/
private async parseXmlToObject(input: string): Promise<any> {
return new Promise((resolve, reject) => {
parseXml(
input,
{
explicitArray: false,
mergeAttrs: true,
explicitRoot: false,
},
(error, result) => {
if (error) {
reject(new Error(`Invalid XML: ${error.message}`));
} else {
resolve(result);
}
}
);
});
}
/**
* Format data as XML
*/
private async formatXml(data: any, options?: z.infer<typeof conversionOptionsSchema>): Promise<string> {
const builder = new XmlBuilder({
renderOpts: {
pretty: options?.pretty ?? true,
indent: ' '.repeat(options?.indent ?? 2),
},
});
// Wrap data in root element if it's not already wrapped
const wrappedData = typeof data === 'object' && !Array.isArray(data) ? data : { root: data };
return builder.buildObject(wrappedData);
}
/**
* Parse YAML string
*/
private parseYaml(input: string): any {
try {
return yaml.load(input);
} catch (error) {
throw new Error(`Invalid YAML: ${error instanceof Error ? error.message : 'Parse error'}`);
}
}
/**
* Format data as YAML
*/
private formatYaml(data: any, options?: z.infer<typeof conversionOptionsSchema>): string {
return yaml.dump(data, {
indent: options?.indent ?? 2,
lineWidth: -1, // No line wrapping
noRefs: true, // Don't use references
});
}
/**
* Parse CSV string to array of objects
*/
private parseCsvToObject(input: string, options?: z.infer<typeof conversionOptionsSchema>): any[] {
try {
const records = parseCsv(input, {
delimiter: options?.csvDelimiter ?? ',',
columns: options?.csvHeaders ?? true,
skip_empty_lines: true,
trim: true,
});
return records;
} catch (error) {
throw new Error(`Invalid CSV: ${error instanceof Error ? error.message : 'Parse error'}`);
}
}
/**
* Format data as CSV
*/
private formatCsv(data: any, options?: z.infer<typeof conversionOptionsSchema>): string {
try {
// Ensure data is an array
const arrayData = Array.isArray(data) ? data : [data];
if (arrayData.length === 0) {
return '';
}
return stringifyCsv(arrayData, {
delimiter: options?.csvDelimiter ?? ',',
header: options?.csvHeaders ?? true,
});
} catch (error) {
throw new Error(`CSV formatting failed: ${error instanceof Error ? error.message : 'Format error'}`);
}
}
/**
* Parse TOML string
*/
private parseToml(input: string): any {
try {
return toml.parse(input);
} catch (error) {
throw new Error(`Invalid TOML: ${error instanceof Error ? error.message : 'Parse error'}`);
}
}
/**
* Format data as TOML
*/
private formatToml(data: any): string {
try {
// TOML requires an object at the root level
if (typeof data !== 'object' || Array.isArray(data)) {
throw new Error('TOML format requires an object at the root level');
}
return toml.stringify(data);
} catch (error) {
throw new Error(`TOML formatting failed: ${error instanceof Error ? error.message : 'Format error'}`);
}
}
}