add (some) lang tests, implement "in" logic
parent
cb22756a47
commit
6aa804584d
|
@ -11,7 +11,7 @@ use std::borrow::Borrow;
|
||||||
use std::convert::TryFrom;
|
use std::convert::TryFrom;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug, PartialEq)]
|
||||||
pub enum QueryComponent<T>
|
pub enum QueryComponent<T>
|
||||||
where
|
where
|
||||||
T: FromStr,
|
T: FromStr,
|
||||||
|
@ -22,33 +22,33 @@ where
|
||||||
Any,
|
Any,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug, PartialEq)]
|
||||||
pub struct EntryQuery {
|
pub struct EntryQuery {
|
||||||
pub entity: QueryComponent<Address>,
|
pub entity: QueryComponent<Address>,
|
||||||
pub attribute: QueryComponent<String>,
|
pub attribute: QueryComponent<String>,
|
||||||
pub value: QueryComponent<EntryValue>,
|
pub value: QueryComponent<EntryValue>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug, PartialEq)]
|
||||||
pub enum QueryPart {
|
pub enum QueryPart {
|
||||||
Matches(EntryQuery),
|
Matches(EntryQuery),
|
||||||
Type(String),
|
Type(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug, PartialEq)]
|
||||||
pub enum QueryQualifier {
|
pub enum QueryQualifier {
|
||||||
And,
|
And,
|
||||||
Or,
|
Or,
|
||||||
Not,
|
Not,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug, PartialEq)]
|
||||||
pub struct MultiQuery {
|
pub struct MultiQuery {
|
||||||
pub qualifier: QueryQualifier,
|
pub qualifier: QueryQualifier,
|
||||||
pub queries: NonEmpty<Box<Query>>,
|
pub queries: NonEmpty<Box<Query>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug, PartialEq)]
|
||||||
pub enum Query {
|
pub enum Query {
|
||||||
SingleQuery(QueryPart),
|
SingleQuery(QueryPart),
|
||||||
MultiQuery(MultiQuery),
|
MultiQuery(MultiQuery),
|
||||||
|
@ -291,12 +291,31 @@ impl Query {
|
||||||
))?;
|
))?;
|
||||||
|
|
||||||
match first {
|
match first {
|
||||||
EntryValue::Number(_) => todo!(),
|
EntryValue::Number(_) => subqueries.push(Box::new(
|
||||||
|
data::value_num.eq_any(
|
||||||
|
q_values
|
||||||
|
.iter()
|
||||||
|
.map(|v| {
|
||||||
|
if let EntryValue::Number(n) = v {
|
||||||
|
Ok(*n)
|
||||||
|
} else {
|
||||||
|
Err(anyhow!("IN queries fidj oisdjfo isdjfoi jsdoifj sodijf oisdjf oij must not combine numeric and string values! ({v} is not a number)"))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect::<Result<Vec<f64>>>()?,
|
||||||
|
),
|
||||||
|
)),
|
||||||
_ => subqueries.push(Box::new(
|
_ => subqueries.push(Box::new(
|
||||||
data::value_str.eq_any(
|
data::value_str.eq_any(
|
||||||
q_values
|
q_values
|
||||||
.iter()
|
.iter()
|
||||||
.map(|v| v.to_string())
|
.map(|v| {
|
||||||
|
if let EntryValue::Number(_) = v {
|
||||||
|
Err(anyhow!("IN queries must not combine numeric and string values! (Found {v})"))
|
||||||
|
} else {
|
||||||
|
v.to_string()
|
||||||
|
}
|
||||||
|
})
|
||||||
.collect::<Result<Vec<String>>>()?,
|
.collect::<Result<Vec<String>>>()?,
|
||||||
),
|
),
|
||||||
)),
|
)),
|
||||||
|
@ -363,3 +382,188 @@ impl Query {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use super::*;
|
||||||
|
use anyhow::Result;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_matches() -> Result<()> {
|
||||||
|
let query = "(matches ? ? ?)".parse::<Query>()?;
|
||||||
|
assert_eq!(
|
||||||
|
query,
|
||||||
|
Query::SingleQuery(QueryPart::Matches(EntryQuery {
|
||||||
|
entity: QueryComponent::Any,
|
||||||
|
attribute: QueryComponent::Any,
|
||||||
|
value: QueryComponent::Any
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
query.to_sqlite_predicates()?;
|
||||||
|
|
||||||
|
let address = Address::Url(String::from("https://upendproject.net"));
|
||||||
|
let query = format!("(matches \"{address}\" ? ?)").parse::<Query>()?;
|
||||||
|
assert_eq!(
|
||||||
|
query,
|
||||||
|
Query::SingleQuery(QueryPart::Matches(EntryQuery {
|
||||||
|
entity: QueryComponent::Exact(address),
|
||||||
|
attribute: QueryComponent::Any,
|
||||||
|
value: QueryComponent::Any
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
query.to_sqlite_predicates()?;
|
||||||
|
|
||||||
|
let query = "(matches ? \"FOO\" ?)".parse::<Query>()?;
|
||||||
|
assert_eq!(
|
||||||
|
query,
|
||||||
|
Query::SingleQuery(QueryPart::Matches(EntryQuery {
|
||||||
|
entity: QueryComponent::Any,
|
||||||
|
attribute: QueryComponent::Exact(String::from("FOO")),
|
||||||
|
value: QueryComponent::Any
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
query.to_sqlite_predicates()?;
|
||||||
|
|
||||||
|
let value = EntryValue::Number(1337.93);
|
||||||
|
let query = format!("(matches ? ? \"{}\")", value.to_string().unwrap()).parse::<Query>()?;
|
||||||
|
assert_eq!(
|
||||||
|
query,
|
||||||
|
Query::SingleQuery(QueryPart::Matches(EntryQuery {
|
||||||
|
entity: QueryComponent::Any,
|
||||||
|
attribute: QueryComponent::Any,
|
||||||
|
value: QueryComponent::Exact(value)
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
query.to_sqlite_predicates()?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_in_parse() -> Result<()> {
|
||||||
|
let query = "(matches ? (in \"FOO\" \"BAR\") ?)".parse::<Query>()?;
|
||||||
|
assert_eq!(
|
||||||
|
query,
|
||||||
|
Query::SingleQuery(QueryPart::Matches(EntryQuery {
|
||||||
|
entity: QueryComponent::Any,
|
||||||
|
attribute: QueryComponent::In(vec!("FOO".to_string(), "BAR".to_string())),
|
||||||
|
value: QueryComponent::Any
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
query.to_sqlite_predicates()?;
|
||||||
|
|
||||||
|
let values = vec![
|
||||||
|
EntryValue::String("FOO".to_string()),
|
||||||
|
EntryValue::String("BAR".to_string()),
|
||||||
|
];
|
||||||
|
let query = format!(
|
||||||
|
"(matches ? ? (in {}))",
|
||||||
|
values
|
||||||
|
.iter()
|
||||||
|
.map(|v| format!("\"{}\"", v.to_string().unwrap()))
|
||||||
|
.collect::<Vec<String>>()
|
||||||
|
.join(" ")
|
||||||
|
)
|
||||||
|
.parse::<Query>()?;
|
||||||
|
assert_eq!(
|
||||||
|
query,
|
||||||
|
Query::SingleQuery(QueryPart::Matches(EntryQuery {
|
||||||
|
entity: QueryComponent::Any,
|
||||||
|
attribute: QueryComponent::Any,
|
||||||
|
value: QueryComponent::In(values)
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
query.to_sqlite_predicates()?;
|
||||||
|
|
||||||
|
let values = vec![EntryValue::Number(1337.93), EntryValue::Number(1968.12)];
|
||||||
|
let query = format!(
|
||||||
|
"(matches ? ? (in {}))",
|
||||||
|
values
|
||||||
|
.iter()
|
||||||
|
.map(|v| format!("\"{}\"", v.to_string().unwrap()))
|
||||||
|
.collect::<Vec<String>>()
|
||||||
|
.join(" ")
|
||||||
|
)
|
||||||
|
.parse::<Query>()?;
|
||||||
|
assert_eq!(
|
||||||
|
query,
|
||||||
|
Query::SingleQuery(QueryPart::Matches(EntryQuery {
|
||||||
|
entity: QueryComponent::Any,
|
||||||
|
attribute: QueryComponent::Any,
|
||||||
|
value: QueryComponent::In(values)
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
query.to_sqlite_predicates()?;
|
||||||
|
|
||||||
|
// Invalid queries
|
||||||
|
let values = vec![
|
||||||
|
EntryValue::String("FOO".to_string()),
|
||||||
|
EntryValue::Number(1337.93),
|
||||||
|
];
|
||||||
|
let query = format!(
|
||||||
|
"(matches ? ? (in {}))",
|
||||||
|
values
|
||||||
|
.iter()
|
||||||
|
.map(|v| format!("\"{}\"", v.to_string().unwrap()))
|
||||||
|
.collect::<Vec<String>>()
|
||||||
|
.join(" ")
|
||||||
|
)
|
||||||
|
.parse::<Query>()?;
|
||||||
|
assert_eq!(
|
||||||
|
query,
|
||||||
|
Query::SingleQuery(QueryPart::Matches(EntryQuery {
|
||||||
|
entity: QueryComponent::Any,
|
||||||
|
attribute: QueryComponent::Any,
|
||||||
|
value: QueryComponent::In(values)
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
query
|
||||||
|
.to_sqlite_predicates()
|
||||||
|
.err()
|
||||||
|
.ok_or(anyhow!("Failed to reject mixed query."))?;
|
||||||
|
|
||||||
|
let values = vec![
|
||||||
|
EntryValue::Number(1337.93),
|
||||||
|
EntryValue::String("FOO".to_string()),
|
||||||
|
];
|
||||||
|
let query = format!(
|
||||||
|
"(matches ? ? (in {}))",
|
||||||
|
values
|
||||||
|
.iter()
|
||||||
|
.map(|v| format!("\"{}\"", v.to_string().unwrap()))
|
||||||
|
.collect::<Vec<String>>()
|
||||||
|
.join(" ")
|
||||||
|
)
|
||||||
|
.parse::<Query>()?;
|
||||||
|
assert_eq!(
|
||||||
|
query,
|
||||||
|
Query::SingleQuery(QueryPart::Matches(EntryQuery {
|
||||||
|
entity: QueryComponent::Any,
|
||||||
|
attribute: QueryComponent::Any,
|
||||||
|
value: QueryComponent::In(values)
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
query
|
||||||
|
.to_sqlite_predicates()
|
||||||
|
.err()
|
||||||
|
.ok_or(anyhow!("Failed to reject mixed query."))?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_contains() -> Result<()> {
|
||||||
|
let query = "(matches ? ? (contains \"foo\"))".parse::<Query>()?;
|
||||||
|
assert_eq!(
|
||||||
|
query,
|
||||||
|
Query::SingleQuery(QueryPart::Matches(EntryQuery {
|
||||||
|
entity: QueryComponent::Any,
|
||||||
|
attribute: QueryComponent::Any,
|
||||||
|
value: QueryComponent::Contains("foo".to_string())
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
query.to_sqlite_predicates()?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue