cql2_cli/
lib.rs

1use anyhow::{anyhow, Result};
2use clap::{ArgAction, Parser, ValueEnum};
3use cql2::{Expr, ToSqlAst, Validator};
4use std::io::Read;
5
6/// The CQL2 command-line interface.
7#[derive(Debug, Parser)]
8#[command(version, about, long_about = None)]
9pub struct Cli {
10    /// Path to NDJSON file to filter (if set, filters using the CQL2 expression)
11    #[arg(short, long)]
12    filter: Option<String>,
13
14    /// The input CQL2
15    ///
16    /// If not provided, or `-`, the CQL2 will be read from standard input. The
17    /// type (json or text) will be auto-detected. To specify a format, use
18    /// --input-format.
19    input: Option<String>,
20
21    /// The input format.
22    ///
23    /// If not provided, the format will be auto-detected from the input.
24    #[arg(short, long)]
25    input_format: Option<InputFormat>,
26
27    /// The output format.
28    ///
29    /// If not provided, the format will be the same as the input.
30    #[arg(short, long)]
31    output_format: Option<OutputFormat>,
32
33    /// Validate the CQL2
34    #[arg(long, default_value_t = true, action = ArgAction::Set)]
35    validate: bool,
36
37    /// Reduce the CQL2
38    #[arg(long, default_value_t = false, action = ArgAction::Set)]
39    reduce: bool,
40
41    /// Verbosity.
42    ///
43    /// Provide this argument several times to turn up the chatter.
44    #[arg(short, long, action = ArgAction::Count)]
45    verbose: u8,
46}
47
48/// The input CQL2 format.
49#[derive(Debug, ValueEnum, Clone)]
50pub enum InputFormat {
51    /// cql2-json
52    Json,
53
54    /// cql2-text
55    Text,
56}
57
58/// The output CQL2 format.
59#[derive(Debug, ValueEnum, Clone)]
60enum OutputFormat {
61    /// cql2-json, pretty-printed
62    JsonPretty,
63
64    /// cql2-json, compact
65    Json,
66
67    /// cql2-text
68    Text,
69
70    /// SQL
71    Sql,
72}
73
74impl Cli {
75    /// Runs the cli.
76    ///
77    /// # Examples
78    ///
79    /// ```
80    /// use cql2_cli::Cli;
81    /// use clap::Parser;
82    ///
83    /// let cli = Cli::try_parse_from(&["cql2", "landsat:scene_id = 'LC82030282019133LGN00'"]).unwrap();
84    /// cli.run();
85    /// ```
86    pub fn run(self) {
87        if let Err(err) = self.run_inner() {
88            eprintln!("{}", err);
89            std::process::exit(1)
90        }
91    }
92
93    pub fn run_inner(self) -> Result<()> {
94        if let Some(filter_path) = self.filter.as_ref() {
95            use std::fs::File;
96            use std::io::{BufRead, BufReader};
97            // Use self.input as the CQL2 expression
98            let expr_str = self.input.as_ref().ok_or_else(|| {
99                anyhow!("CQL2 expression required as positional argument when using --filter")
100            })?;
101            let expr: Expr = if expr_str.trim_start().starts_with('{') {
102                cql2::parse_json(expr_str)?
103            } else {
104                cql2::parse_text(expr_str)?
105            };
106            let file = File::open(filter_path)?;
107            let reader = BufReader::new(file);
108            reader
109                .lines()
110                .map(|line| {
111                    let line = line?;
112                    Ok(serde_json::from_str(&line)?)
113                })
114                .collect::<Result<Vec<_>, anyhow::Error>>()?
115                .into_iter()
116                .filter_map(|value| {
117                    expr.filter(&[value])
118                        .ok()
119                        .and_then(|mut v| v.pop().cloned())
120                })
121                .for_each(|v| println!("{}", serde_json::to_string(&v).unwrap()));
122            return Ok(());
123        }
124        let input = self
125            .input
126            .and_then(|input| if input == "-" { None } else { Some(input) })
127            .map(Ok)
128            .unwrap_or_else(read_stdin)?;
129        let input_format = self.input_format.unwrap_or_else(|| {
130            if input.starts_with('{') {
131                InputFormat::Json
132            } else {
133                InputFormat::Text
134            }
135        });
136        let mut expr: Expr = match input_format {
137            InputFormat::Json => cql2::parse_json(&input)?,
138            InputFormat::Text => match cql2::parse_text(&input) {
139                Ok(expr) => expr,
140                Err(err) => {
141                    return Err(anyhow!("[ERROR] Parsing error: {input}\n{err}"));
142                }
143            },
144        };
145        if self.reduce {
146            expr = expr.reduce(None)?;
147        }
148        if self.validate {
149            let validator = Validator::new().unwrap();
150            let value = serde_json::to_value(&expr).unwrap();
151            if let Err(error) = validator.validate(&value) {
152                return Err(anyhow!(
153                    "[ERROR] Invalid CQL2: {input}\n{}",
154                    match self.verbose {
155                        0 => "For more detailed validation information, use -v".to_string(),
156                        1 => format!("For more detailed validation information, use -vv\n{error}"),
157                        _ => format!("{error:#}"),
158                    }
159                ));
160            }
161        }
162        let output_format = self.output_format.unwrap_or(match input_format {
163            InputFormat::Json => OutputFormat::Json,
164            InputFormat::Text => OutputFormat::Text,
165        });
166        match output_format {
167            OutputFormat::JsonPretty => serde_json::to_writer_pretty(std::io::stdout(), &expr)?,
168            OutputFormat::Json => serde_json::to_writer(std::io::stdout(), &expr)?,
169            OutputFormat::Text => print!("{}", expr.to_text()?),
170            OutputFormat::Sql => {
171                let sql_ast = expr.to_sql_ast()?;
172                println!("{}", sql_ast);
173            }
174        }
175        println!();
176        Ok(())
177    }
178}
179
180fn read_stdin() -> Result<String> {
181    let mut buf = String::new();
182    std::io::stdin().read_to_string(&mut buf)?;
183    Ok(buf)
184}