1use anyhow::{Result, anyhow, bail};
2use clap::{Args as ClapArgs, Parser, Subcommand, ValueEnum};
3use std::path::{Path, PathBuf};
4
5#[derive(Debug, Parser)]
8#[command(version, about, long_about = None)]
9pub struct Cli {
10 #[command(subcommand)]
11 pub command: Option<Command>,
12
13 #[command(flatten)]
14 pub diff: Args,
15}
16
17#[derive(Debug, Subcommand)]
18pub enum Command {
19 Serve(ServeArgs),
22}
23
24#[derive(Debug, ClapArgs)]
26pub struct Args {
27 pub file_a: Option<PathBuf>,
29 pub file_b: Option<PathBuf>,
31
32 #[arg(long = "format-a", value_enum)]
34 pub format_a: Option<InputFormat>,
35
36 #[arg(long = "format-b", value_enum)]
38 pub format_b: Option<InputFormat>,
39
40 #[arg(short = 'o', long = "output")]
42 pub output: Option<PathBuf>,
43
44 #[arg(long = "output-format", value_enum, default_value_t = OutputFormat::Trig)]
46 pub output_format: OutputFormat,
47
48 #[arg(long = "graph-a")]
50 pub graph_a: Option<String>,
51
52 #[arg(long = "graph-b")]
54 pub graph_b: Option<String>,
55
56 #[arg(long)]
58 pub quiet: bool,
59
60 #[arg(long)]
62 pub ci: bool,
63
64 #[arg(long)]
66 pub view: bool,
67
68 #[arg(long = "no-open")]
70 pub no_open: bool,
71
72 #[arg(long, default_value = "127.0.0.1:0")]
74 pub bind: String,
75
76 #[arg(long = "ignore-blank-nodes")]
80 pub ignore_blank_nodes: bool,
81}
82
83#[derive(Debug, ClapArgs)]
85pub struct ServeArgs {
86 #[arg(long = "file-a", requires = "file_b")]
88 pub file_a: Option<PathBuf>,
89 #[arg(long = "file-b", requires = "file_a")]
91 pub file_b: Option<PathBuf>,
92 #[arg(long = "format-a", value_enum)]
94 pub format_a: Option<InputFormat>,
95 #[arg(long = "format-b", value_enum)]
97 pub format_b: Option<InputFormat>,
98 #[arg(long, conflicts_with_all = ["file_a", "file_b"])]
100 pub diff: Option<PathBuf>,
101 #[arg(long = "graph-a")]
103 pub graph_a: Option<String>,
104 #[arg(long = "graph-b")]
106 pub graph_b: Option<String>,
107 #[arg(long, default_value = "127.0.0.1:0")]
109 pub bind: String,
110 #[arg(long = "no-open")]
112 pub no_open: bool,
113 #[arg(long = "ignore-blank-nodes")]
115 pub ignore_blank_nodes: bool,
116}
117
118#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
119pub enum InputFormat {
120 Nt,
122 Ttl,
124 #[value(alias = "xml")]
126 Rdf,
127 Trig,
129 Nq,
131}
132
133#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
134pub enum OutputFormat {
135 Trig,
137 Nq,
139}
140
141pub fn detect_format(path: &Path) -> Result<InputFormat> {
143 let name = path
144 .file_name()
145 .and_then(|s| s.to_str())
146 .ok_or_else(|| anyhow!("path has no filename: {}", path.display()))?
147 .to_ascii_lowercase();
148
149 let stem = name.strip_suffix(".gz").unwrap_or(&name);
150 let ext = stem.rsplit_once('.').map(|(_, e)| e).unwrap_or("");
151
152 match ext {
153 "nt" | "ntriples" => Ok(InputFormat::Nt),
154 "ttl" | "turtle" => Ok(InputFormat::Ttl),
155 "rdf" | "owl" | "xml" => Ok(InputFormat::Rdf),
156 "trig" => Ok(InputFormat::Trig),
157 "nq" | "nquads" => Ok(InputFormat::Nq),
158 "" => bail!(
159 "could not detect RDF format for {} (no extension); use --format-a/--format-b",
160 path.display()
161 ),
162 other => bail!(
163 "unknown RDF extension '.{}' for {}; use --format-a/--format-b",
164 other,
165 path.display()
166 ),
167 }
168}
169
170#[cfg(test)]
171mod tests {
172 use super::*;
173
174 #[test]
175 fn detects_basic_extensions() {
176 assert_eq!(detect_format(Path::new("a.ttl")).unwrap(), InputFormat::Ttl);
177 assert_eq!(detect_format(Path::new("a.nt")).unwrap(), InputFormat::Nt);
178 assert_eq!(detect_format(Path::new("a.rdf")).unwrap(), InputFormat::Rdf);
179 assert_eq!(
180 detect_format(Path::new("a.trig")).unwrap(),
181 InputFormat::Trig
182 );
183 assert_eq!(detect_format(Path::new("a.nq")).unwrap(), InputFormat::Nq);
184 }
185
186 #[test]
187 fn detects_gzipped_extensions() {
188 assert_eq!(
189 detect_format(Path::new("a.ttl.gz")).unwrap(),
190 InputFormat::Ttl
191 );
192 assert_eq!(
193 detect_format(Path::new("a.nt.gz")).unwrap(),
194 InputFormat::Nt
195 );
196 }
197
198 #[test]
199 fn rejects_unknown_extension() {
200 assert!(detect_format(Path::new("a.foo")).is_err());
201 assert!(detect_format(Path::new("noext")).is_err());
202 }
203}