diff --git a/Cargo.lock b/Cargo.lock index 03882bb..5b2d8d1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -115,6 +115,7 @@ dependencies = [ "serde", "serde_derive", "serde_json", + "thiserror", ] [[package]] @@ -626,9 +627,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.71" +version = "2.0.72" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b146dcf730474b4bcd16c311627b31ede9ab149045db4d6088b3becaea046462" +checksum = "dc4b9b9bf2add8093d3f2c0204471e951b2285580335de42f9d2534f3ae7a8af" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index a73dd2c..556d467 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,3 +14,9 @@ rstest = "0.21.0" serde = "1.0.204" serde_derive = "1.0.204" serde_json = "1.0.120" +thiserror = "1.0.63" + +[[bin]] +name = "cql2-rs" +test = false +doc = false diff --git a/src/lib.rs b/src/lib.rs index 236c9e7..d4e3705 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,4 @@ -use boon::{Compiler, SchemaIndex, Schemas}; +use boon::{Compiler, SchemaIndex, Schemas, ValidationError}; use geozero::geojson::GeoJsonWriter; use geozero::wkt::Wkt; use geozero::{CoordDimensions, GeozeroGeometry, ToJson}; @@ -6,61 +6,77 @@ use pest::iterators::{Pair, Pairs}; use pest::pratt_parser::PrattParser; use pest::Parser; use serde_derive::{Deserialize, Serialize}; +use serde_json::Value; use std::fs; +use thiserror::Error; + +/// Crate-specific error enum. +#[derive(Debug, Error)] +pub enum Error { + /// [boon::CompileError] + #[error(transparent)] + BoonCompile(#[from] boon::CompileError), + + /// [serde_json::Error] + #[error(transparent)] + SerdeJson(#[from] serde_json::Error), +} + pub struct Validator { schemas: Schemas, index: SchemaIndex, } impl Validator { - pub fn new() -> Validator { + /// Creates a new validator. + /// + /// # Examples + /// + /// ``` + /// use cql2_rs::Validator; + /// + /// let validator = Validator::new().unwrap(); + /// ``` + pub fn new() -> Result { let mut schemas = Schemas::new(); let mut compiler = Compiler::new(); - let schema_json = serde_json::from_str(include_str!("cql2.json")) - .expect("Could not parse schema to json"); - compiler - .add_resource("/tmp/cql2.json", schema_json) - .expect("Could not add schema to compiler"); - let index = compiler - .compile("/tmp/cql2.json", &mut schemas) - .expect("Could not compile schema"); - Validator { schemas, index } - } - - pub fn validate(self, obj: serde_json::Value) -> bool { - let valid = self.schemas.validate(&obj, self.index); - match valid { - Ok(()) => true, - Err(e) => { - let debug_level: &str = - &std::env::var("CQL2_DEBUG_LEVEL").unwrap_or("1".to_string()); - match debug_level { - "3" => { - println!("-----------\n{e:#?}\n---------------") - } - "2" => { - println!("-----------\n{e:?}\n---------------") - } - "1" => { - println!("-----------\n{e}\n---------------") - } - _ => { - println!("-----------\nCQL2 Is Invalid!\n---------------") - } - } - - false - } - } - } - pub fn validate_str(self, obj: &str) -> bool { - self.validate(serde_json::from_str(obj).expect("Could not convert string to json.")) + let schema_json = serde_json::from_str(include_str!("cql2.json"))?; + // TODO make this windoze friendly + compiler.add_resource("/tmp/cql2.json", schema_json)?; + let index = compiler.compile("/tmp/cql2.json", &mut schemas)?; + Ok(Validator { schemas, index }) } -} -impl Default for Validator { - fn default() -> Self { - Self::new() + /// Validates a [serde_json::Value]. + /// + /// Returns either `Ok(())` or the error output as a [serde_json::Value]. + /// Use [Validator::detailed] to configure whether the basic or detailed + /// output is returned. + /// + /// # Examples + /// + /// ``` + /// use cql2_rs::Validator; + /// use serde_json::json; + /// + /// let validator = Validator::new().unwrap(); + /// + /// let valid = json!({ + /// "op": "=", + /// "args": [ + /// { "property": "landsat:scene_id" }, + /// "LC82030282019133LGN00" + /// ] + /// }); + /// validator.validate(&valid).unwrap(); + /// + /// let invalid = json!({ + /// "op": "not an operator!", + /// }); + /// validator.validate(&invalid).unwrap_err(); + /// ``` + pub fn validate<'a, 'b>(&'a self, value: &'b Value) -> Result<(), ValidationError<'a, 'b>> { + self.schemas.validate(value, self.index) } } @@ -96,14 +112,47 @@ impl Expr { fn as_sql() -> String { return "sql".to_string(); } */ - pub fn as_json(&self) -> String { - serde_json::to_string(&self).unwrap() + + /// Converts this expression to a JSON string. + /// + /// # Examples + /// + /// ``` + /// use cql2_rs::Expr; + /// + /// let expr = Expr::BoolConst(true); + /// let s = expr.to_json().unwrap(); + /// ``` + pub fn to_json(&self) -> Result { + serde_json::to_string(&self) } - pub fn as_json_pretty(&self) -> String { - serde_json::to_string_pretty(&self).unwrap() + + /// Converts this expression to a pretty JSON string. + /// + /// # Examples + /// + /// ``` + /// use cql2_rs::Expr; + /// + /// let expr = Expr::BoolConst(true); + /// let s = expr.to_json_pretty().unwrap(); + /// ``` + pub fn to_json_pretty(&self) -> Result { + serde_json::to_string_pretty(&self) } - pub fn validate(&self) -> bool { - Validator::new().validate_str(&self.as_json()) + + /// Converts this expression to a [serde_json::Value]. + /// + /// # Examples + /// + /// ``` + /// use cql2_rs::Expr; + /// + /// let expr = Expr::BoolConst(true); + /// let value = expr.to_value().unwrap(); + /// ``` + pub fn to_value(&self) -> Result { + serde_json::to_value(self) } } diff --git a/src/main.rs b/src/main.rs index 8cc872d..a113e1d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,13 +1,34 @@ -use cql2_rs::parse; -use std::io::{self, BufRead}; -fn main() -> io::Result<()> { - for line in io::stdin().lock().lines() { - let parsed = parse(&line?); +use cql2_rs::Validator; +use std::io::BufRead; +fn main() { + let debug_level: u8 = std::env::var("CQL2_DEBUG_LEVEL") + .map(|s| { + s.parse() + .unwrap_or_else(|_| panic!("CQL2_DEBUG_LEVEL should be an integer: {}", s)) + }) + .unwrap_or(1); + let validator = Validator::new().unwrap(); + + let mut ok = true; + for line in std::io::stdin().lock().lines() { + let parsed = cql2_rs::parse(&line.unwrap()); println!("Parsed: {:#?}", &parsed); - println!("{}", parsed.as_json()); + println!("{}", parsed.to_json().unwrap()); + let value = serde_json::to_value(parsed).unwrap(); + + if let Err(err) = validator.validate(&value) { + match debug_level { + 0 => println!("-----------\nCQL2 Is Invalid!\n---------------"), + 1 => println!("-----------\n{err}\n---------------"), + 2 => println!("-----------\n{err:?}\n---------------"), + _ => println!("-----------\n{err:?}\n---------------"), + } + ok = false; + } + } - parsed.validate(); + if !ok { + std::process::exit(1); } - Ok(()) } diff --git a/tests/ogc_tests.rs b/tests/ogc_tests.rs index 3bc3337..a1a8768 100644 --- a/tests/ogc_tests.rs +++ b/tests/ogc_tests.rs @@ -4,22 +4,16 @@ use std::fs; use std::path::PathBuf; pub fn validate_file(f: &str) { - //println!("Current Directory: {:#?}", env::current_dir()); println!("File Path: {:#?}", f); let cql2 = fs::read_to_string(f).unwrap(); println!("CQL2: {}", cql2); let expr: cql2_rs::Expr = parse(&cql2); - println!("Expr: {}", expr.as_json_pretty()); - let valid = expr.validate(); - assert!(valid) -} + println!("Expr: {}", expr.to_json_pretty().unwrap()); -#[rstest] -fn json_examples_are_valid(#[files("tests/fixtures/json/*.json")] path: PathBuf) { - let cql2 = fs::read_to_string(path).unwrap(); - let validator = Validator::new(); - let result = validator.validate_str(&cql2); - assert!(result) + Validator::new() + .unwrap() + .validate(&expr.to_value().unwrap()) + .unwrap(); } #[rstest]