Skip to main content

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    /// Don't validate the CQL2
34    #[arg(long)]
35    no_validate: bool,
36
37    /// Reduce the CQL2
38    #[arg(long)]
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::{
96                fs::File,
97                io::{BufRead, BufReader},
98            };
99            // Use self.input as the CQL2 expression
100            let expr_str = self.input.as_ref().ok_or_else(|| {
101                anyhow!("CQL2 expression required as positional argument when using --filter")
102            })?;
103            let expr: Expr = if expr_str.trim_start().starts_with('{') {
104                cql2::parse_json(expr_str)?
105            } else {
106                cql2::parse_text(expr_str)?
107            };
108            let file = File::open(filter_path)?;
109            let reader = BufReader::new(file);
110            reader
111                .lines()
112                .map(|line| {
113                    let line = line?;
114                    Ok(serde_json::from_str(&line)?)
115                })
116                .collect::<Result<Vec<_>, anyhow::Error>>()?
117                .into_iter()
118                .filter_map(|value| {
119                    expr.filter(&[value])
120                        .ok()
121                        .and_then(|mut v| v.pop().cloned())
122                })
123                .for_each(|v| println!("{}", serde_json::to_string(&v).unwrap()));
124            return Ok(());
125        }
126        let input = self
127            .input
128            .and_then(|input| if input == "-" { None } else { Some(input) })
129            .map(Ok)
130            .unwrap_or_else(read_stdin)?;
131        let input_format = self.input_format.unwrap_or_else(|| {
132            if input.starts_with('{') {
133                InputFormat::Json
134            } else {
135                InputFormat::Text
136            }
137        });
138        let mut expr: Expr = match input_format {
139            InputFormat::Json => cql2::parse_json(&input)?,
140            InputFormat::Text => match cql2::parse_text(&input) {
141                Ok(expr) => expr,
142                Err(err) => {
143                    return Err(anyhow!("[ERROR] Parsing error: {input}\n{err}"));
144                }
145            },
146        };
147        if self.reduce {
148            expr = expr.reduce(None)?;
149        }
150        if !self.no_validate {
151            let validator = Validator::new().unwrap();
152            let value = serde_json::to_value(&expr).unwrap();
153            if let Err(error) = validator.validate(&value) {
154                return Err(anyhow!(
155                    "[ERROR] Invalid CQL2: {input}\n{}",
156                    match self.verbose {
157                        0 => "For more detailed validation information, use -v".to_string(),
158                        1 => format!("For more detailed validation information, use -vv\n{error}"),
159                        _ => format!("{error:#}"),
160                    }
161                ));
162            }
163        }
164        let output_format = self.output_format.unwrap_or(match input_format {
165            InputFormat::Json => OutputFormat::Json,
166            InputFormat::Text => OutputFormat::Text,
167        });
168        match output_format {
169            OutputFormat::JsonPretty => serde_json::to_writer_pretty(std::io::stdout(), &expr)?,
170            OutputFormat::Json => serde_json::to_writer(std::io::stdout(), &expr)?,
171            OutputFormat::Text => print!("{}", expr.to_text()?),
172            OutputFormat::Sql => {
173                let sql_ast = expr.to_sql_ast()?;
174                println!("{}", sql_ast);
175            }
176        }
177        println!();
178        Ok(())
179    }
180}
181
182fn read_stdin() -> Result<String> {
183    let mut buf = String::new();
184    std::io::stdin().read_to_string(&mut buf)?;
185    Ok(buf)
186}