ggen_cli_lib/cmds/
graph.rs

1use clap::Args;
2use ggen_core::graph::Graph;
3use ggen_utils::error::Result;
4use std::io::Write;
5use std::path::PathBuf;
6
7#[derive(Args, Debug)]
8pub struct GraphArgs {
9    #[arg(value_name = "SCOPE")]
10    pub scope: String,
11    #[arg(value_name = "ACTION")]
12    pub action: String,
13
14    /// Output format (turtle, ntriples, rdfxml, jsonld)
15    #[arg(short, long, default_value = "turtle")]
16    pub format: String,
17
18    /// Output file path (default: stdout)
19    #[arg(short, long)]
20    pub output: Option<PathBuf>,
21
22    /// Include prefixes in output
23    #[arg(long)]
24    pub include_prefixes: bool,
25}
26
27pub fn run(args: &GraphArgs) -> Result<()> {
28    println!(
29        "Exporting graph for scope: {}, action: {}",
30        args.scope, args.action
31    );
32
33    // Load the appropriate graph based on scope and action
34    let graph = load_graph_for_scope_action(&args.scope, &args.action)?;
35
36    if graph.is_empty() {
37        println!(
38            "No data found for scope '{}' and action '{}'",
39            args.scope, args.action
40        );
41        return Ok(());
42    }
43
44    // Export the graph in the requested format
45    export_graph(
46        &graph,
47        &args.format,
48        args.output.as_ref(),
49        args.include_prefixes,
50    )?;
51
52    println!("Graph exported successfully");
53    Ok(())
54}
55
56fn load_graph_for_scope_action(scope: &str, action: &str) -> Result<Graph> {
57    let graph = Graph::new()?;
58
59    // Load graphs based on scope and action
60    match (scope, action) {
61        ("cli", "export") => {
62            // Load CLI-related graphs
63            load_cli_graphs(&graph)?;
64        }
65        ("api", "export") => {
66            // Load API-related graphs
67            load_api_graphs(&graph)?;
68        }
69        ("core", "export") => {
70            // Load core graphs
71            load_core_graphs(&graph)?;
72        }
73        _ => {
74            // Try to load from template locations
75            let graph_paths = vec![
76                format!("templates/{}/{}/graphs/{}.ttl", scope, action, scope),
77                format!("templates/{}/graphs/{}.ttl", scope, scope),
78            ];
79
80            let mut found = false;
81            for graph_path in graph_paths {
82                if std::path::Path::new(&graph_path).exists() {
83                    graph.load_path(&graph_path)?;
84                    found = true;
85                    break;
86                }
87            }
88
89            if !found {
90                println!(
91                    "No graph found for scope '{}' and action '{}'",
92                    scope, action
93                );
94            }
95        }
96    }
97
98    Ok(graph)
99}
100
101fn load_cli_graphs(graph: &Graph) -> Result<()> {
102    let cli_graph_paths = vec![
103        "templates/cli/subcommand/graphs/cli.ttl",
104        "templates/cli/graphs/cli.ttl",
105    ];
106
107    for cli_graph_path in cli_graph_paths {
108        if std::path::Path::new(cli_graph_path).exists() {
109            graph.load_path(cli_graph_path)?;
110            println!("Loaded CLI graph from {}", cli_graph_path);
111            return Ok(());
112        }
113    }
114    Ok(())
115}
116
117fn load_api_graphs(graph: &Graph) -> Result<()> {
118    // Look for API-related graphs
119    let api_graph_paths = vec![
120        "templates/api/endpoint/graphs/api.ttl",
121        "templates/api/graphs/api.ttl",
122    ];
123
124    for api_graph_path in api_graph_paths {
125        if std::path::Path::new(api_graph_path).exists() {
126            graph.load_path(api_graph_path)?;
127            println!("Loaded API graph from {}", api_graph_path);
128            return Ok(());
129        }
130    }
131    Ok(())
132}
133
134fn load_core_graphs(graph: &Graph) -> Result<()> {
135    let core_graph_paths = vec![
136        "templates/core/graphs/core.ttl",
137        "templates/api/endpoint/graphs/api.ttl", // API endpoint contains core concepts
138    ];
139
140    for core_graph_path in core_graph_paths {
141        if std::path::Path::new(core_graph_path).exists() {
142            graph.load_path(core_graph_path)?;
143            println!("Loaded core graph from {}", core_graph_path);
144            return Ok(());
145        }
146    }
147    Ok(())
148}
149
150fn export_graph(
151    graph: &Graph, format: &str, output_path: Option<&PathBuf>, include_prefixes: bool,
152) -> Result<()> {
153    let format_lower = format.to_lowercase();
154
155    match format_lower.as_str() {
156        "turtle" | "ttl" => {
157            export_turtle(graph, output_path, include_prefixes)?;
158        }
159        "ntriples" | "nt" => {
160            export_ntriples(graph, output_path)?;
161        }
162        "rdfxml" | "xml" => {
163            export_rdfxml(graph, output_path)?;
164        }
165        "jsonld" | "json" => {
166            export_jsonld(graph, output_path)?;
167        }
168        _ => {
169            return Err(ggen_utils::error::Error::new(&format!(
170                "Unsupported format: {}. Supported formats: turtle, ntriples, rdfxml, jsonld",
171                format
172            )));
173        }
174    }
175
176    Ok(())
177}
178
179fn export_turtle(
180    graph: &Graph, output_path: Option<&PathBuf>, include_prefixes: bool,
181) -> Result<()> {
182    // Use streaming approach for large graphs
183    let query = "CONSTRUCT { ?s ?p ?o } WHERE { ?s ?p ?o }";
184    let results = graph.query(query)?;
185
186    match results {
187        oxigraph::sparql::QueryResults::Graph(graph_iter) => {
188            // Use streaming writer for better memory efficiency
189            let writer: Box<dyn std::io::Write> = if let Some(path) = output_path {
190                Box::new(std::fs::File::create(path)?)
191            } else {
192                Box::new(std::io::stdout())
193            };
194
195            let mut writer = std::io::BufWriter::new(writer);
196
197            // Write prefixes if requested
198            if include_prefixes {
199                writeln!(
200                    writer,
201                    "@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> ."
202                )?;
203                writeln!(
204                    writer,
205                    "@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> ."
206                )?;
207                writeln!(writer, "@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .")?;
208                writeln!(writer, "@prefix sh: <http://www.w3.org/ns/shacl#> .")?;
209                writeln!(writer, "@prefix ex: <http://example.org/> .")?;
210                writeln!(writer)?;
211            }
212
213            // Stream triples to avoid loading everything into memory
214            let mut triple_count = 0;
215            for triple in graph_iter {
216                let triple = triple.map_err(|e| anyhow::anyhow!("Graph iteration error: {}", e))?;
217                let s = triple.subject.to_string();
218                let p = triple.predicate.to_string();
219                let o = triple.object.to_string();
220
221                writeln!(writer, "{} {} {} .", s, p, o)?;
222                triple_count += 1;
223
224                // Flush periodically for large datasets
225                if triple_count % 1000 == 0 {
226                    writer.flush()?;
227                }
228            }
229
230            writer.flush()?;
231
232            if let Some(path) = output_path {
233                println!(
234                    "Turtle format exported to {} ({} triples)",
235                    path.display(),
236                    triple_count
237                );
238            }
239        }
240        _ => {
241            return Err(ggen_utils::error::Error::new(
242                "Expected graph results for CONSTRUCT query",
243            ));
244        }
245    }
246
247    Ok(())
248}
249
250fn export_ntriples(graph: &Graph, output_path: Option<&PathBuf>) -> Result<()> {
251    let query = "CONSTRUCT { ?s ?p ?o } WHERE { ?s ?p ?o }";
252    let results = graph.query(query)?;
253
254    match results {
255        oxigraph::sparql::QueryResults::Graph(graph_iter) => {
256            // Use streaming writer for better memory efficiency
257            let writer: Box<dyn std::io::Write> = if let Some(path) = output_path {
258                Box::new(std::fs::File::create(path)?)
259            } else {
260                Box::new(std::io::stdout())
261            };
262
263            let mut writer = std::io::BufWriter::new(writer);
264            let mut triple_count = 0;
265
266            for triple in graph_iter {
267                let triple = triple.map_err(|e| anyhow::anyhow!("Graph iteration error: {}", e))?;
268                let s = triple.subject.to_string();
269                let p = triple.predicate.to_string();
270                let o = triple.object.to_string();
271
272                writeln!(writer, "{} {} {} .", s, p, o)?;
273                triple_count += 1;
274
275                // Flush periodically for large datasets
276                if triple_count % 1000 == 0 {
277                    writer.flush()?;
278                }
279            }
280
281            writer.flush()?;
282
283            if let Some(path) = output_path {
284                println!(
285                    "N-Triples format exported to {} ({} triples)",
286                    path.display(),
287                    triple_count
288                );
289            }
290        }
291        _ => {
292            return Err(ggen_utils::error::Error::new(
293                "Expected graph results for CONSTRUCT query",
294            ));
295        }
296    }
297
298    Ok(())
299}
300
301fn export_rdfxml(graph: &Graph, output_path: Option<&PathBuf>) -> Result<()> {
302    // For RDF/XML, we'll use a simple approach
303    let query = "CONSTRUCT { ?s ?p ?o } WHERE { ?s ?p ?o }";
304    let results = graph.query(query)?;
305
306    match results {
307        oxigraph::sparql::QueryResults::Graph(graph_iter) => {
308            let mut xml_content = String::new();
309            xml_content.push_str("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
310            xml_content
311                .push_str("<rdf:RDF xmlns:rdf=\"http://www.w3.org/1999/02/22-rdf-syntax-ns#\"\n");
312            xml_content
313                .push_str("         xmlns:rdfs=\"http://www.w3.org/2000/01/rdf-schema#\">\n");
314
315            for triple in graph_iter {
316                let triple = triple.map_err(|e| anyhow::anyhow!("Graph iteration error: {}", e))?;
317                let s = triple.subject.to_string();
318                let p = triple.predicate.to_string();
319                let o = triple.object.to_string();
320
321                xml_content.push_str(&format!("  <rdf:Description rdf:about=\"{}\">\n", s));
322                xml_content.push_str(&format!("    <{}>{}</{}>\n", p, o, p));
323                xml_content.push_str("  </rdf:Description>\n");
324            }
325
326            xml_content.push_str("</rdf:RDF>\n");
327
328            if let Some(path) = output_path {
329                std::fs::write(path, xml_content)?;
330                println!("RDF/XML format exported to {}", path.display());
331            } else {
332                print!("{}", xml_content);
333            }
334        }
335        _ => {
336            return Err(ggen_utils::error::Error::new(
337                "Expected graph results for CONSTRUCT query",
338            ));
339        }
340    }
341
342    Ok(())
343}
344
345fn export_jsonld(graph: &Graph, output_path: Option<&PathBuf>) -> Result<()> {
346    // For JSON-LD, we'll create a simple structure
347    let query = "SELECT ?s ?p ?o WHERE { ?s ?p ?o }";
348    let results = graph.query(query)?;
349
350    match results {
351        oxigraph::sparql::QueryResults::Solutions(solutions) => {
352            let mut jsonld_content = serde_json::json!({
353                "@context": {
354                    "rdf": "http://www.w3.org/1999/02/22-rdf-syntax-ns#",
355                    "rdfs": "http://www.w3.org/2000/01/rdf-schema#",
356                    "xsd": "http://www.w3.org/2001/XMLSchema#",
357                    "sh": "http://www.w3.org/ns/shacl#",
358                    "ex": "http://example.org/"
359                },
360                "@graph": []
361            });
362
363            let mut triples = Vec::new();
364            for solution in solutions {
365                let solution =
366                    solution.map_err(|e| anyhow::anyhow!("Query solution error: {}", e))?;
367                let s = solution.get("s").unwrap().to_string();
368                let p = solution.get("p").unwrap().to_string();
369                let o = solution.get("o").unwrap().to_string();
370
371                triples.push(serde_json::json!({
372                    "@id": s,
373                    p: o
374                }));
375            }
376
377            jsonld_content["@graph"] = serde_json::Value::Array(triples);
378
379            let json_string = serde_json::to_string_pretty(&jsonld_content)?;
380
381            if let Some(path) = output_path {
382                std::fs::write(path, json_string)?;
383                println!("JSON-LD format exported to {}", path.display());
384            } else {
385                print!("{}", json_string);
386            }
387        }
388        _ => {
389            return Err(ggen_utils::error::Error::new(
390                "Expected solutions for SELECT query",
391            ));
392        }
393    }
394
395    Ok(())
396}