1use crate::{Error, Result};
7
8#[derive(Debug, Clone, PartialEq, Eq)]
10pub enum ReplCommand {
11 Load {
16 path: String,
18 },
19
20 Datasets,
22
23 Use {
25 name: String,
27 },
28
29 Info,
34
35 Head {
37 n: usize,
39 },
40
41 Schema,
43
44 QualityCheck,
49
50 QualityScore {
52 suggest: bool,
54 json: bool,
56 badge: bool,
58 },
59
60 DriftDetect {
65 reference: String,
67 },
68
69 Convert {
71 format: String,
73 },
74
75 Export {
80 what: String,
82 json: bool,
84 },
85
86 Validate {
88 schema: String,
90 },
91
92 History {
97 export: bool,
99 },
100
101 Help {
103 topic: Option<String>,
105 },
106
107 Quit,
109}
110
111pub struct CommandParser;
113
114impl CommandParser {
115 pub fn parse(input: &str) -> Result<ReplCommand> {
121 let input = input.trim();
122
123 if input.is_empty() {
124 return Err(Error::parse("Empty command"));
125 }
126
127 let parts: Vec<&str> = input.split_whitespace().collect();
128 let cmd = parts[0].to_lowercase();
129 let args = &parts[1..];
130
131 match cmd.as_str() {
132 "load" => Self::parse_load(args),
134 "datasets" => Ok(ReplCommand::Datasets),
135 "use" => Self::parse_use(args),
136
137 "info" => Ok(ReplCommand::Info),
139 "head" => Self::parse_head(args),
140 "schema" => Ok(ReplCommand::Schema),
141
142 "quality" => Self::parse_quality(args),
144
145 "drift" => Self::parse_drift(args),
147 "convert" => Self::parse_convert(args),
148
149 "export" => Self::parse_export(args),
151 "validate" => Self::parse_validate(args),
152
153 "history" => Ok(Self::parse_history(args)),
155 "help" | "?" => Ok(Self::parse_help(args)),
156 "quit" | "exit" | "q" => Ok(ReplCommand::Quit),
157
158 _ => Err(Error::parse(format!("Unknown command: '{}'", cmd))),
159 }
160 }
161
162 fn parse_load(args: &[&str]) -> Result<ReplCommand> {
163 if args.is_empty() {
164 return Err(Error::parse("load requires a file path"));
165 }
166 Ok(ReplCommand::Load {
167 path: args[0].to_string(),
168 })
169 }
170
171 fn parse_use(args: &[&str]) -> Result<ReplCommand> {
172 if args.is_empty() {
173 return Err(Error::parse("use requires a dataset name"));
174 }
175 Ok(ReplCommand::Use {
176 name: args[0].to_string(),
177 })
178 }
179
180 fn parse_head(args: &[&str]) -> Result<ReplCommand> {
181 let n = if args.is_empty() {
182 10 } else {
184 args[0]
185 .parse()
186 .map_err(|_| Error::parse(format!("Invalid number: '{}'", args[0])))?
187 };
188 Ok(ReplCommand::Head { n })
189 }
190
191 fn parse_quality(args: &[&str]) -> Result<ReplCommand> {
192 if args.is_empty() {
193 return Err(Error::parse("quality requires subcommand: check, score"));
194 }
195
196 match args[0].to_lowercase().as_str() {
197 "check" => Ok(ReplCommand::QualityCheck),
198 "score" => {
199 let suggest = args.contains(&"--suggest");
200 let json = args.contains(&"--json");
201 let badge = args.contains(&"--badge");
202 Ok(ReplCommand::QualityScore {
203 suggest,
204 json,
205 badge,
206 })
207 }
208 _ => Err(Error::parse(format!(
209 "Unknown quality subcommand: '{}'. Use: check, score",
210 args[0]
211 ))),
212 }
213 }
214
215 fn parse_drift(args: &[&str]) -> Result<ReplCommand> {
216 if args.is_empty() {
217 return Err(Error::parse("drift requires subcommand: detect"));
218 }
219
220 match args[0].to_lowercase().as_str() {
221 "detect" => {
222 if args.len() < 2 {
223 return Err(Error::parse("drift detect requires a reference file"));
224 }
225 Ok(ReplCommand::DriftDetect {
226 reference: args[1].to_string(),
227 })
228 }
229 _ => Err(Error::parse(format!(
230 "Unknown drift subcommand: '{}'. Use: detect",
231 args[0]
232 ))),
233 }
234 }
235
236 fn parse_convert(args: &[&str]) -> Result<ReplCommand> {
237 if args.is_empty() {
238 return Err(Error::parse(
239 "convert requires a format: csv, parquet, json",
240 ));
241 }
242 let format = args[0].to_lowercase();
243 match format.as_str() {
244 "csv" | "parquet" | "json" => Ok(ReplCommand::Convert { format }),
245 _ => Err(Error::parse(format!(
246 "Unknown format: '{}'. Use: csv, parquet, json",
247 args[0]
248 ))),
249 }
250 }
251
252 fn parse_export(args: &[&str]) -> Result<ReplCommand> {
253 if args.is_empty() {
254 return Err(Error::parse("export requires what to export: quality"));
255 }
256
257 let what = args[0].to_lowercase();
258 let json = args.iter().any(|f| *f == "--json" || *f == "-j");
259
260 Ok(ReplCommand::Export { what, json })
261 }
262
263 fn parse_validate(args: &[&str]) -> Result<ReplCommand> {
264 let schema_idx = args.iter().position(|f| *f == "--schema" || *f == "-s");
266
267 let schema = if let Some(idx) = schema_idx {
268 if idx + 1 < args.len() {
269 args[idx + 1].to_string()
270 } else {
271 return Err(Error::parse("--schema requires a file path"));
272 }
273 } else {
274 return Err(Error::parse("validate requires --schema <file>"));
275 };
276
277 Ok(ReplCommand::Validate { schema })
278 }
279
280 fn parse_history(args: &[&str]) -> ReplCommand {
281 let export = args.iter().any(|f| *f == "--export" || *f == "-e");
282 ReplCommand::History { export }
283 }
284
285 fn parse_help(args: &[&str]) -> ReplCommand {
286 let topic = args.first().map(|s| (*s).to_string());
287 ReplCommand::Help { topic }
288 }
289
290 #[must_use]
292 pub fn command_names() -> Vec<&'static str> {
293 vec![
294 "load", "datasets", "use", "info", "head", "schema", "quality", "drift", "convert",
295 "export", "validate", "history", "help", "quit", "exit",
296 ]
297 }
298
299 #[must_use]
301 pub fn subcommands(command: &str) -> Vec<&'static str> {
302 match command {
303 "quality" => vec!["check", "score"],
304 "drift" => vec!["detect"],
305 _ => vec![],
306 }
307 }
308
309 #[must_use]
311 pub fn flags(command: &str, subcommand: Option<&str>) -> Vec<&'static str> {
312 match (command, subcommand) {
313 ("quality", Some("score")) => vec!["--suggest", "--json", "--badge"],
314 ("export", _) => vec!["--json"],
315 ("validate", _) => vec!["--schema"],
316 ("history", _) => vec!["--export"],
317 _ => vec![],
318 }
319 }
320}