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 #[arg(short, long, default_value = "turtle")]
16 pub format: String,
17
18 #[arg(short, long)]
20 pub output: Option<PathBuf>,
21
22 #[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 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_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 match (scope, action) {
61 ("cli", "export") => {
62 load_cli_graphs(&graph)?;
64 }
65 ("api", "export") => {
66 load_api_graphs(&graph)?;
68 }
69 ("core", "export") => {
70 load_core_graphs(&graph)?;
72 }
73 _ => {
74 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 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", ];
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 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 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 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 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 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 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 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 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 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}