hedl_cli/commands/
convert.rs

1// Dweve HEDL - Hierarchical Entity Data Language
2//
3// Copyright (c) 2025 Dweve IP B.V. and individual contributors.
4//
5// SPDX-License-Identifier: Apache-2.0
6//
7// Licensed under the Apache License, Version 2.0 (the "License");
8// you may not use this file except in compliance with the License.
9// You may obtain a copy of the License in the LICENSE file at the
10// root of this repository or at: http://www.apache.org/licenses/LICENSE-2.0
11//
12// Unless required by applicable law or agreed to in writing, software
13// distributed under the License is distributed on an "AS IS" BASIS,
14// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15// See the License for the specific language governing permissions and
16// limitations under the License.
17
18//! Conversion commands - HEDL format interoperability
19//!
20//! This module provides bidirectional conversion between HEDL and popular data formats:
21//! - JSON (compact and pretty-printed)
22//! - YAML
23//! - XML (compact and pretty-printed)
24//! - CSV
25//! - Parquet
26//!
27//! All conversions preserve data fidelity where possible, with format-specific
28//! optimizations and configurations.
29
30use super::{read_file, write_output};
31use hedl_c14n::canonicalize;
32use hedl_core::parse;
33use hedl_csv::{from_csv as csv_to_hedl, to_csv as hedl_to_csv};
34use hedl_json::{from_json as json_to_hedl, to_json_value, FromJsonConfig, ToJsonConfig};
35use hedl_parquet::{from_parquet as parquet_to_hedl, to_parquet as hedl_to_parquet};
36use hedl_xml::{from_xml as xml_to_hedl, to_xml as hedl_to_xml, FromXmlConfig, ToXmlConfig};
37use hedl_yaml::{from_yaml as yaml_to_hedl, to_yaml as hedl_to_yaml, FromYamlConfig, ToYamlConfig};
38use hedl_toon::{hedl_to_toon, toon_to_hedl};
39use std::path::Path;
40
41// JSON conversion
42
43/// Convert a HEDL file to JSON format.
44///
45/// Parses a HEDL file and converts it to JSON, with options for metadata inclusion
46/// and pretty-printing.
47///
48/// # Arguments
49///
50/// * `file` - Path to the HEDL file to convert
51/// * `output` - Optional output file path. If `None`, writes to stdout
52/// * `metadata` - If `true`, includes HEDL-specific metadata in the JSON output
53/// * `pretty` - If `true`, pretty-prints the JSON with indentation
54///
55/// # Returns
56///
57/// Returns `Ok(())` on success.
58///
59/// # Errors
60///
61/// Returns `Err` if:
62/// - The file cannot be read
63/// - The file contains syntax errors
64/// - JSON conversion fails
65/// - Output writing fails
66///
67/// # Examples
68///
69/// ```no_run
70/// use hedl_cli::commands::to_json;
71///
72/// # fn main() -> Result<(), String> {
73/// // Convert to compact JSON on stdout
74/// to_json("data.hedl", None, false, false)?;
75///
76/// // Convert to pretty JSON with metadata
77/// to_json("data.hedl", Some("output.json"), true, true)?;
78/// # Ok(())
79/// # }
80/// ```
81pub fn to_json(
82    file: &str,
83    output: Option<&str>,
84    metadata: bool,
85    pretty: bool,
86) -> Result<(), String> {
87    let content = read_file(file)?;
88
89    let doc = parse(content.as_bytes()).map_err(|e| format!("Parse error: {}", e))?;
90
91    let config = ToJsonConfig {
92        include_metadata: metadata,
93        ..Default::default()
94    };
95
96    // P0 OPTIMIZATION: Direct conversion to Value (no double conversion)
97    let value = to_json_value(&doc, &config).map_err(|e| format!("JSON conversion error: {}", e))?;
98    let output_str = if pretty {
99        serde_json::to_string_pretty(&value).map_err(|e| format!("JSON format error: {}", e))?
100    } else {
101        serde_json::to_string(&value).map_err(|e| format!("JSON format error: {}", e))?
102    };
103
104    write_output(&output_str, output)
105}
106
107/// Convert a JSON file to HEDL format.
108///
109/// Parses a JSON file and converts it to canonical HEDL format.
110///
111/// # Arguments
112///
113/// * `file` - Path to the JSON file to convert
114/// * `output` - Optional output file path. If `None`, writes to stdout
115///
116/// # Returns
117///
118/// Returns `Ok(())` on success.
119///
120/// # Errors
121///
122/// Returns `Err` if:
123/// - The file cannot be read
124/// - The JSON is malformed
125/// - JSON-to-HEDL conversion fails
126/// - HEDL canonicalization fails
127/// - Output writing fails
128///
129/// # Examples
130///
131/// ```no_run
132/// use hedl_cli::commands::from_json;
133///
134/// # fn main() -> Result<(), String> {
135/// // Convert JSON to HEDL on stdout
136/// from_json("data.json", None)?;
137///
138/// // Convert JSON to HEDL file
139/// from_json("data.json", Some("output.hedl"))?;
140/// # Ok(())
141/// # }
142/// ```
143pub fn from_json(file: &str, output: Option<&str>) -> Result<(), String> {
144    let content = read_file(file)?;
145
146    let config = FromJsonConfig::default();
147    let doc =
148        json_to_hedl(&content, &config).map_err(|e| format!("JSON conversion error: {}", e))?;
149
150    let hedl = canonicalize(&doc).map_err(|e| format!("HEDL generation error: {}", e))?;
151
152    write_output(&hedl, output)
153}
154
155// YAML conversion
156
157/// Convert a HEDL file to YAML format.
158///
159/// Parses a HEDL file and converts it to YAML format.
160///
161/// # Arguments
162///
163/// * `file` - Path to the HEDL file to convert
164/// * `output` - Optional output file path. If `None`, writes to stdout
165///
166/// # Returns
167///
168/// Returns `Ok(())` on success.
169///
170/// # Errors
171///
172/// Returns `Err` if:
173/// - The file cannot be read
174/// - The file contains syntax errors
175/// - YAML conversion fails
176/// - Output writing fails
177///
178/// # Examples
179///
180/// ```no_run
181/// use hedl_cli::commands::to_yaml;
182///
183/// # fn main() -> Result<(), String> {
184/// // Convert to YAML on stdout
185/// to_yaml("data.hedl", None)?;
186///
187/// // Convert to YAML file
188/// to_yaml("data.hedl", Some("output.yaml"))?;
189/// # Ok(())
190/// # }
191/// ```
192pub fn to_yaml(file: &str, output: Option<&str>) -> Result<(), String> {
193    let content = read_file(file)?;
194
195    let doc = parse(content.as_bytes()).map_err(|e| format!("Parse error: {}", e))?;
196
197    let config = ToYamlConfig::default();
198    let yaml = hedl_to_yaml(&doc, &config).map_err(|e| format!("YAML conversion error: {}", e))?;
199
200    write_output(&yaml, output)
201}
202
203/// Convert a YAML file to HEDL format.
204///
205/// Parses a YAML file and converts it to canonical HEDL format.
206///
207/// # Arguments
208///
209/// * `file` - Path to the YAML file to convert
210/// * `output` - Optional output file path. If `None`, writes to stdout
211///
212/// # Returns
213///
214/// Returns `Ok(())` on success.
215///
216/// # Errors
217///
218/// Returns `Err` if:
219/// - The file cannot be read
220/// - The YAML is malformed
221/// - YAML-to-HEDL conversion fails
222/// - HEDL canonicalization fails
223/// - Output writing fails
224///
225/// # Examples
226///
227/// ```no_run
228/// use hedl_cli::commands::from_yaml;
229///
230/// # fn main() -> Result<(), String> {
231/// // Convert YAML to HEDL on stdout
232/// from_yaml("data.yaml", None)?;
233///
234/// // Convert YAML to HEDL file
235/// from_yaml("data.yml", Some("output.hedl"))?;
236/// # Ok(())
237/// # }
238/// ```
239pub fn from_yaml(file: &str, output: Option<&str>) -> Result<(), String> {
240    let content = read_file(file)?;
241
242    let config = FromYamlConfig::default();
243    let doc =
244        yaml_to_hedl(&content, &config).map_err(|e| format!("YAML conversion error: {}", e))?;
245
246    let hedl = canonicalize(&doc).map_err(|e| format!("HEDL generation error: {}", e))?;
247
248    write_output(&hedl, output)
249}
250
251// XML conversion
252
253/// Convert a HEDL file to XML format.
254///
255/// Parses a HEDL file and converts it to XML, with optional pretty-printing.
256///
257/// # Arguments
258///
259/// * `file` - Path to the HEDL file to convert
260/// * `output` - Optional output file path. If `None`, writes to stdout
261/// * `pretty` - If `true`, pretty-prints the XML with indentation
262///
263/// # Returns
264///
265/// Returns `Ok(())` on success.
266///
267/// # Errors
268///
269/// Returns `Err` if:
270/// - The file cannot be read
271/// - The file contains syntax errors
272/// - XML conversion fails
273/// - Output writing fails
274///
275/// # Examples
276///
277/// ```no_run
278/// use hedl_cli::commands::to_xml;
279///
280/// # fn main() -> Result<(), String> {
281/// // Convert to compact XML on stdout
282/// to_xml("data.hedl", None, false)?;
283///
284/// // Convert to pretty XML file
285/// to_xml("data.hedl", Some("output.xml"), true)?;
286/// # Ok(())
287/// # }
288/// ```
289pub fn to_xml(file: &str, output: Option<&str>, pretty: bool) -> Result<(), String> {
290    let content = read_file(file)?;
291
292    let doc = parse(content.as_bytes()).map_err(|e| format!("Parse error: {}", e))?;
293
294    let config = ToXmlConfig {
295        pretty,
296        ..Default::default()
297    };
298    let xml = hedl_to_xml(&doc, &config).map_err(|e| format!("XML conversion error: {}", e))?;
299
300    write_output(&xml, output)
301}
302
303/// Convert an XML file to HEDL format.
304///
305/// Parses an XML file and converts it to canonical HEDL format.
306///
307/// # Arguments
308///
309/// * `file` - Path to the XML file to convert
310/// * `output` - Optional output file path. If `None`, writes to stdout
311///
312/// # Returns
313///
314/// Returns `Ok(())` on success.
315///
316/// # Errors
317///
318/// Returns `Err` if:
319/// - The file cannot be read
320/// - The XML is malformed
321/// - XML-to-HEDL conversion fails
322/// - HEDL canonicalization fails
323/// - Output writing fails
324///
325/// # Examples
326///
327/// ```no_run
328/// use hedl_cli::commands::from_xml;
329///
330/// # fn main() -> Result<(), String> {
331/// // Convert XML to HEDL on stdout
332/// from_xml("data.xml", None)?;
333///
334/// // Convert XML to HEDL file
335/// from_xml("data.xml", Some("output.hedl"))?;
336/// # Ok(())
337/// # }
338/// ```
339pub fn from_xml(file: &str, output: Option<&str>) -> Result<(), String> {
340    let content = read_file(file)?;
341
342    let config = FromXmlConfig::default();
343    let doc = xml_to_hedl(&content, &config).map_err(|e| format!("XML conversion error: {}", e))?;
344
345    let hedl = canonicalize(&doc).map_err(|e| format!("HEDL generation error: {}", e))?;
346
347    write_output(&hedl, output)
348}
349
350// CSV conversion
351
352/// Convert a HEDL file to CSV format.
353///
354/// Parses a HEDL file and converts it to CSV format. Expects the HEDL file to contain
355/// a matrix list that can be represented as a table.
356///
357/// # Arguments
358///
359/// * `file` - Path to the HEDL file to convert
360/// * `output` - Optional output file path. If `None`, writes to stdout
361/// * `_include_headers` - Reserved for future use (headers always included)
362///
363/// # Returns
364///
365/// Returns `Ok(())` on success.
366///
367/// # Errors
368///
369/// Returns `Err` if:
370/// - The file cannot be read
371/// - The file contains syntax errors
372/// - The HEDL structure is not compatible with CSV (e.g., nested structures)
373/// - CSV conversion fails
374/// - Output writing fails
375///
376/// # Examples
377///
378/// ```no_run
379/// use hedl_cli::commands::to_csv;
380///
381/// # fn main() -> Result<(), String> {
382/// // Convert to CSV on stdout
383/// to_csv("data.hedl", None, true)?;
384///
385/// // Convert to CSV file
386/// to_csv("data.hedl", Some("output.csv"), true)?;
387/// # Ok(())
388/// # }
389/// ```
390pub fn to_csv(file: &str, output: Option<&str>, _include_headers: bool) -> Result<(), String> {
391    let content = read_file(file)?;
392
393    let doc = parse(content.as_bytes()).map_err(|e| format!("Parse error: {}", e))?;
394
395    let csv = hedl_to_csv(&doc).map_err(|e| format!("CSV conversion error: {}", e))?;
396
397    write_output(&csv, output)
398}
399
400/// Convert a CSV file to HEDL format.
401///
402/// Parses a CSV file and converts it to canonical HEDL format. The first row is assumed
403/// to be the header row containing column names.
404///
405/// # Arguments
406///
407/// * `file` - Path to the CSV file to convert
408/// * `output` - Optional output file path. If `None`, writes to stdout
409/// * `type_name` - The type name to use for the HEDL matrix list (must be alphanumeric)
410///
411/// # Returns
412///
413/// Returns `Ok(())` on success.
414///
415/// # Errors
416///
417/// Returns `Err` if:
418/// - The file cannot be read
419/// - The CSV is malformed or empty
420/// - The type name is invalid (must be alphanumeric with underscores)
421/// - CSV-to-HEDL conversion fails
422/// - HEDL canonicalization fails
423/// - Output writing fails
424///
425/// # Examples
426///
427/// ```no_run
428/// use hedl_cli::commands::from_csv;
429///
430/// # fn main() -> Result<(), String> {
431/// // Convert CSV to HEDL on stdout with type name "Person"
432/// from_csv("people.csv", None, "Person")?;
433///
434/// // Convert CSV to HEDL file
435/// from_csv("data.csv", Some("output.hedl"), "Record")?;
436///
437/// // Invalid type name will fail
438/// let result = from_csv("data.csv", None, "Invalid-Name!");
439/// assert!(result.is_err());
440/// # Ok(())
441/// # }
442/// ```
443///
444/// # Security
445///
446/// The type name is validated to prevent injection attacks. Only alphanumeric
447/// characters and underscores are allowed.
448pub fn from_csv(file: &str, output: Option<&str>, type_name: &str) -> Result<(), String> {
449    let content = read_file(file)?;
450
451    // Validate type_name to prevent injection
452    if !type_name.chars().all(|c| c.is_alphanumeric() || c == '_') {
453        return Err("Type name must be alphanumeric (with underscores allowed)".to_string());
454    }
455
456    // Infer column names from header row
457    let first_line = content
458        .lines()
459        .next()
460        .ok_or_else(|| "CSV file is empty or has no header row".to_string())?;
461    let columns: Vec<&str> = first_line.split(',').skip(1).collect(); // Skip ID column
462
463    let doc = csv_to_hedl(&content, type_name, &columns)
464        .map_err(|e| format!("CSV conversion error: {}", e))?;
465
466    let hedl = canonicalize(&doc).map_err(|e| format!("HEDL generation error: {}", e))?;
467
468    write_output(&hedl, output)
469}
470
471// Parquet conversion
472
473/// Convert a HEDL file to Parquet format.
474///
475/// Parses a HEDL file and converts it to Apache Parquet columnar format. This is ideal
476/// for analytical workloads and integration with data processing frameworks.
477///
478/// # Arguments
479///
480/// * `file` - Path to the HEDL file to convert
481/// * `output` - Output Parquet file path (required, cannot write to stdout)
482///
483/// # Returns
484///
485/// Returns `Ok(())` on success.
486///
487/// # Errors
488///
489/// Returns `Err` if:
490/// - The file cannot be read
491/// - The file contains syntax errors
492/// - The HEDL structure is not compatible with Parquet
493/// - Parquet conversion fails
494/// - Output file cannot be written
495///
496/// # Examples
497///
498/// ```no_run
499/// use hedl_cli::commands::to_parquet;
500///
501/// # fn main() -> Result<(), String> {
502/// // Convert to Parquet file
503/// to_parquet("data.hedl", "output.parquet")?;
504/// # Ok(())
505/// # }
506/// ```
507///
508/// # Note
509///
510/// Parquet requires a file path for output; it cannot write to stdout due to
511/// the binary columnar format.
512pub fn to_parquet(file: &str, output: &str) -> Result<(), String> {
513    let content = read_file(file)?;
514
515    let doc = parse(content.as_bytes()).map_err(|e| format!("Parse error: {}", e))?;
516
517    hedl_to_parquet(&doc, Path::new(output))
518        .map_err(|e| format!("Parquet conversion error: {}", e))?;
519
520    Ok(())
521}
522
523/// Convert a Parquet file to HEDL format.
524///
525/// Reads an Apache Parquet file and converts it to canonical HEDL format.
526///
527/// # Arguments
528///
529/// * `file` - Path to the Parquet file to convert
530/// * `output` - Optional output file path. If `None`, writes to stdout
531///
532/// # Returns
533///
534/// Returns `Ok(())` on success.
535///
536/// # Errors
537///
538/// Returns `Err` if:
539/// - The file cannot be read
540/// - The Parquet file is malformed or unsupported
541/// - Parquet-to-HEDL conversion fails
542/// - HEDL canonicalization fails
543/// - Output writing fails
544///
545/// # Examples
546///
547/// ```no_run
548/// use hedl_cli::commands::from_parquet;
549///
550/// # fn main() -> Result<(), String> {
551/// // Convert Parquet to HEDL on stdout
552/// from_parquet("data.parquet", None)?;
553///
554/// // Convert Parquet to HEDL file
555/// from_parquet("data.parquet", Some("output.hedl"))?;
556/// # Ok(())
557/// # }
558/// ```
559pub fn from_parquet(file: &str, output: Option<&str>) -> Result<(), String> {
560    let doc =
561        parquet_to_hedl(Path::new(file)).map_err(|e| format!("Parquet conversion error: {}", e))?;
562
563    let hedl = canonicalize(&doc).map_err(|e| format!("HEDL generation error: {}", e))?;
564
565    write_output(&hedl, output)
566}
567
568// TOON conversion
569
570/// Convert a HEDL file to TOON format.
571///
572/// Parses a HEDL file and converts it to TOON format.
573///
574/// # Arguments
575///
576/// * `file` - Path to the HEDL file to convert
577/// * `output` - Optional output file path. If `None`, writes to stdout
578///
579/// # Returns
580///
581/// Returns `Ok(())` on success.
582///
583/// # Errors
584///
585/// Returns `Err` if:
586/// - The file cannot be read
587/// - The file contains syntax errors
588/// - TOON conversion fails
589/// - Output writing fails
590///
591/// # Examples
592///
593/// ```no_run
594/// use hedl_cli::commands::to_toon;
595///
596/// # fn main() -> Result<(), String> {
597/// // Convert to TOON on stdout
598/// to_toon("data.hedl", None)?;
599///
600/// // Convert to TOON file
601/// to_toon("data.hedl", Some("output.toon"))?;
602/// # Ok(())
603/// # }
604/// ```
605pub fn to_toon(file: &str, output: Option<&str>) -> Result<(), String> {
606    let content = read_file(file)?;
607
608    let doc = parse(content.as_bytes()).map_err(|e| format!("Parse error: {}", e))?;
609
610    let toon = hedl_to_toon(&doc).map_err(|e| format!("TOON conversion error: {}", e))?;
611
612    write_output(&toon, output)
613}
614
615/// Convert a TOON file to HEDL format.
616///
617/// Parses a TOON file and converts it to HEDL format.
618///
619/// # Arguments
620///
621/// * `file` - Path to the TOON file to convert
622/// * `output` - Optional output file path. If `None`, writes to stdout
623///
624/// # Returns
625///
626/// Returns `Ok(())` on success.
627///
628/// # Errors
629///
630/// Returns `Err` if:
631/// - The file cannot be read
632/// - The file contains syntax errors
633/// - HEDL generation fails
634/// - Output writing fails
635///
636/// # Examples
637///
638/// ```no_run
639/// use hedl_cli::commands::from_toon;
640///
641/// # fn main() -> Result<(), String> {
642/// // Convert TOON to HEDL on stdout
643/// from_toon("data.toon", None)?;
644///
645/// // Convert TOON to HEDL file
646/// from_toon("data.toon", Some("output.hedl"))?;
647/// # Ok(())
648/// # }
649/// ```
650pub fn from_toon(file: &str, output: Option<&str>) -> Result<(), String> {
651    let content = read_file(file)?;
652
653    let doc = toon_to_hedl(&content).map_err(|e| format!("TOON parse error: {}", e))?;
654
655    let hedl = canonicalize(&doc).map_err(|e| format!("HEDL generation error: {}", e))?;
656
657    write_output(&hedl, output)
658}