Skip to content

Commit

Permalink
feat: install cli via Pyton package
Browse files Browse the repository at this point in the history
  • Loading branch information
gadomski committed Oct 10, 2024
1 parent 12ec99d commit fb4ec61
Show file tree
Hide file tree
Showing 14 changed files with 221 additions and 142 deletions.
2 changes: 2 additions & 0 deletions .github/workflows/python.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ jobs:
run: uv run mypy . && uv run ruff check && uv run ruff format --check
- name: Test
run: uv run pytest
- name: CLI smoke test
run: uv run cql2 < ../fixtures/text/example01.txt
linux:
runs-on: ${{ matrix.platform.runner }}
strategy:
Expand Down
21 changes: 15 additions & 6 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,6 @@ rstest = "0.23"
[workspace]
default-members = [".", "cli"]
members = ["cli", "python"]

[workspace.dependencies]
clap = "4.5"
5 changes: 3 additions & 2 deletions cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ name = "cql2-cli"
version = "0.1.0"
authors = ["David Bitner <[email protected]>"]
edition = "2021"
description = "Command line interface (CLI) for Common Query Language (CQL2)"
description = "Command line interface for Common Query Language (CQL2)"
readme = "README.md"
homepage = "https://github.com/developmentseed/cql2-rs"
repository = "https://github.com/developmentseed/cql2-rs"
Expand All @@ -12,8 +12,9 @@ keywords = ["cql2"]


[dependencies]
anyhow = "1.0"
clap = { workspace = true, features = ["derive"] }
cql2 = { path = "..", version = "0.1.0" }
clap = { version = "4.5", features = ["derive"] }
serde_json = "1.0"

[[bin]]
Expand Down
9 changes: 7 additions & 2 deletions cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,18 @@ A Command Line Interface (CLI) for [Common Query Language (CQL2)](https://www.og

## Installation

Install [Rust](https://rustup.rs/).
Then:
With cargo:

```shell
cargo install cql2-cli
```

Or from [PyPI](https://pypi.org/project/cql2/):

```shell
pip install cql2
```

## CLI

At its simplest, the CLI is a pass-through validator:
Expand Down
145 changes: 145 additions & 0 deletions cli/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
use anyhow::{anyhow, Result};
use clap::{ArgAction, Parser, ValueEnum};
use cql2::{Expr, Validator};
use std::io::Read;

/// The CQL2 command-line interface.
#[derive(Debug, Parser)]
#[command(version, about, long_about = None)]
pub struct Cli {
/// The input CQL2
///
/// If not provided, or `-`, the CQL2 will be read from standard input. The
/// type (json or text) will be auto-detected. To specify a format, use
/// --input-format.
input: Option<String>,

/// The input format.
///
/// If not provided, the format will be auto-detected from the input.
#[arg(short, long)]
input_format: Option<InputFormat>,

/// The output format.
///
/// If not provided, the format will be the same as the input.
#[arg(short, long)]
output_format: Option<OutputFormat>,

/// Validate the CQL2
#[arg(long, default_value_t = true, action = ArgAction::Set)]
validate: bool,

/// Verbosity.
///
/// Provide this argument several times to turn up the chatter.
#[arg(short, long, action = ArgAction::Count)]
verbose: u8,
}

/// The input CQL2 format.
#[derive(Debug, ValueEnum, Clone)]
pub enum InputFormat {
/// cql2-json
Json,

/// cql2-text
Text,
}

/// The output CQL2 format.
#[derive(Debug, ValueEnum, Clone)]
enum OutputFormat {
/// cql2-json, pretty-printed
JsonPretty,

/// cql2-json, compact
Json,

/// cql2-text
Text,

/// SQL
Sql,
}

impl Cli {
/// Runs the cli.
///
/// # Examples
///
/// ```
/// use cql2_cli::Cli;
/// use clap::Parser;
///
/// let cli = Cli::try_parse_from(&["--help"]).unwrap();
/// cli.run();
/// ```
pub fn run(self) {
if let Err(err) = self.run_inner() {
eprintln!("{}", err);
std::process::exit(1)
}
}

pub fn run_inner(self) -> Result<()> {
let input = self
.input
.and_then(|input| if input == "-" { None } else { Some(input) })
.map(Ok)
.unwrap_or_else(read_stdin)?;
let input_format = self.input_format.unwrap_or_else(|| {
if input.starts_with('{') {
InputFormat::Json
} else {
InputFormat::Text
}
});
let expr: Expr = match input_format {
InputFormat::Json => cql2::parse_json(&input)?,
InputFormat::Text => match cql2::parse_text(&input) {
Ok(expr) => expr,
Err(err) => {
return Err(anyhow!("[ERROR] Parsing error: {input}\n{err}"));
}
},
};
if self.validate {
let validator = Validator::new().unwrap();
let value = serde_json::to_value(&expr).unwrap();
if let Err(error) = validator.validate(&value) {
return Err(anyhow!(
"[ERROR] Invalid CQL2: {input}\n{}",
match self.verbose {
0 => "For more detailed validation information, use -v".to_string(),
1 => format!("For more detailed validation information, use -vv\n{error}"),
2 =>
format!("For more detailed validation information, use -vvv\n{error:#}"),
_ => {
let detailed_output = error.detailed_output();
format!("{detailed_output:#}")
}
}
));
}
}
let output_format = self.output_format.unwrap_or(match input_format {
InputFormat::Json => OutputFormat::Json,
InputFormat::Text => OutputFormat::Text,
});
match output_format {
OutputFormat::JsonPretty => serde_json::to_writer_pretty(std::io::stdout(), &expr)?,
OutputFormat::Json => serde_json::to_writer(std::io::stdout(), &expr)?,
OutputFormat::Text => print!("{}", expr.to_text()?),
OutputFormat::Sql => serde_json::to_writer_pretty(std::io::stdout(), &expr.to_sql()?)?,
}
println!();
Ok(())
}
}

fn read_stdin() -> Result<String> {
let mut buf = String::new();
std::io::stdin().read_to_string(&mut buf)?;
Ok(buf)
}
121 changes: 3 additions & 118 deletions cli/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,121 +1,6 @@
use clap::{ArgAction, Parser, ValueEnum};
use cql2::{Expr, Validator};
use std::io::Read;

#[derive(Debug, Parser)]
struct Cli {
/// The input CQL2
///
/// If not provided, or `-`, the CQL2 will be read from standard input. The
/// type (json or text) will be auto-detected. To specify a format, use
/// --input-format.
input: Option<String>,

/// The input format.
///
/// If not provided, the format will be auto-detected from the input.
#[arg(short, long)]
input_format: Option<InputFormat>,

/// The output format.
///
/// If not provided, the format will be the same as the input.
#[arg(short, long)]
output_format: Option<OutputFormat>,

/// Validate the CQL2
#[arg(long, default_value_t = true, action = ArgAction::Set)]
validate: bool,

/// Verbosity.
///
/// Provide this argument several times to turn up the chatter.
#[arg(short, long, action = ArgAction::Count)]
verbose: u8,
}

#[derive(Debug, ValueEnum, Clone)]
enum InputFormat {
/// cql2-json
Json,

/// cql2-text
Text,
}

#[derive(Debug, ValueEnum, Clone)]
enum OutputFormat {
/// cql2-json, pretty-printed
JsonPretty,

/// cql2-json, compact
Json,

/// cql2-text
Text,

/// SQL
Sql,
}
use clap::Parser;
use cql2_cli::Cli;

fn main() {
let cli = Cli::parse();
let input = cli
.input
.and_then(|input| if input == "-" { None } else { Some(input) })
.unwrap_or_else(read_stdin);
let input_format = cli.input_format.unwrap_or_else(|| {
if input.starts_with('{') {
InputFormat::Json
} else {
InputFormat::Text
}
});
let expr: Expr = match input_format {
InputFormat::Json => cql2::parse_json(&input).unwrap(),
InputFormat::Text => match cql2::parse_text(&input) {
Ok(expr) => expr,
Err(err) => {
eprintln!("[ERROR] Parsing error: {input}");
eprintln!("{err}");
std::process::exit(1)
}
},
};
if cli.validate {
let validator = Validator::new().unwrap();
let value = serde_json::to_value(&expr).unwrap();
if let Err(error) = validator.validate(&value) {
eprintln!("[ERROR] Invalid CQL2: {input}");
match cli.verbose {
0 => eprintln!("For more detailed validation information, use -v"),
1 => eprintln!("For more detailed validation information, use -vv\n{error}"),
2 => eprintln!("For more detailed validation information, use -vvv\n{error:#}"),
_ => {
let detailed_output = error.detailed_output();
eprintln!("{detailed_output:#}");
}
}
std::process::exit(1)
}
}
let output_format = cli.output_format.unwrap_or(match input_format {
InputFormat::Json => OutputFormat::Json,
InputFormat::Text => OutputFormat::Text,
});
match output_format {
OutputFormat::JsonPretty => serde_json::to_writer_pretty(std::io::stdout(), &expr).unwrap(),
OutputFormat::Json => serde_json::to_writer(std::io::stdout(), &expr).unwrap(),
OutputFormat::Text => print!("{}", expr.to_text().unwrap()),
OutputFormat::Sql => {
serde_json::to_writer_pretty(std::io::stdout(), &expr.to_sql().unwrap()).unwrap()
}
}
println!()
}

fn read_stdin() -> String {
let mut buf = String::new();
std::io::stdin().read_to_string(&mut buf).unwrap();
buf
Cli::parse().run()
}
Loading

0 comments on commit fb4ec61

Please sign in to comment.