mirror of
https://github.com/trailbaseio/trailbase.git
synced 2025-12-30 14:19:43 -06:00
Add a first (yet unused) version of a "querystring" filter parser.
This commit is contained in:
41
Cargo.lock
generated
41
Cargo.lock
generated
@@ -4097,6 +4097,15 @@ dependencies = [
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ordered-float"
|
||||
version = "2.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "68f19d67e5a2795c94e73e0bb1cc1a7edeb2e28efd39e2e1c9b7a40c1108b11c"
|
||||
dependencies = [
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "os_pipe"
|
||||
version = "1.1.5"
|
||||
@@ -5482,6 +5491,16 @@ dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde-value"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f3a1a3341211875ef120e117ea7fd5228530ae7e7036a779fdc9117be6b3282c"
|
||||
dependencies = [
|
||||
"ordered-float",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_bytes"
|
||||
version = "0.11.17"
|
||||
@@ -5536,6 +5555,17 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_qs"
|
||||
version = "0.15.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f3faaf9e727533a19351a43cc5a8de957372163c7d35cc48c90b75cdda13c352"
|
||||
dependencies = [
|
||||
"percent-encoding",
|
||||
"serde",
|
||||
"thiserror 2.0.12",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_rusqlite"
|
||||
version = "0.38.0"
|
||||
@@ -6964,6 +6994,17 @@ dependencies = [
|
||||
"trailbase-sqlite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "trailbase-qs"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"itertools 0.14.0",
|
||||
"serde",
|
||||
"serde-value",
|
||||
"serde_qs",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "trailbase-refinery-core"
|
||||
version = "0.8.16"
|
||||
|
||||
@@ -11,6 +11,7 @@ members = [
|
||||
"trailbase-core",
|
||||
"trailbase-extension",
|
||||
"trailbase-js",
|
||||
"trailbase-qs",
|
||||
"trailbase-schema",
|
||||
"trailbase-sqlite",
|
||||
"vendor/serde_rusqlite",
|
||||
@@ -24,6 +25,7 @@ default-members = [
|
||||
"trailbase-core",
|
||||
"trailbase-extension",
|
||||
"trailbase-js",
|
||||
"trailbase-qs",
|
||||
"trailbase-schema",
|
||||
"trailbase-sqlite",
|
||||
]
|
||||
@@ -75,6 +77,7 @@ trailbase-assets = { path = "trailbase-assets", version = "0.1.0" }
|
||||
trailbase-sqlean = { path = "vendor/sqlean", version = "0.0.2" }
|
||||
trailbase-extension = { path = "trailbase-extension", version = "0.2.0" }
|
||||
trailbase-js = { path = "trailbase-js", version = "0.1.0" }
|
||||
trailbase-qs = { path = "trailbase-qs", version = "0.1.0" }
|
||||
trailbase-refinery-core = { path = "vendor/refinery/refinery_core", version = "0.8.16", default-features = false, features = ["rusqlite-bundled"] }
|
||||
trailbase-refinery-macros = { path = "vendor/refinery/refinery_macros", version = "0.8.15" }
|
||||
trailbase-schema = { path = "trailbase-schema", version = "0.1.0" }
|
||||
|
||||
@@ -6,7 +6,7 @@ use rusqlite::Error;
|
||||
use rusqlite::functions::Context;
|
||||
use std::sync::LazyLock;
|
||||
|
||||
static ARGON2: LazyLock<Argon2<'static>> = LazyLock::new(|| Argon2::default());
|
||||
static ARGON2: LazyLock<Argon2<'static>> = LazyLock::new(Argon2::default);
|
||||
|
||||
pub fn hash_password(password: &str) -> Result<String, argon2::password_hash::Error> {
|
||||
let salt = SaltString::generate(&mut OsRng);
|
||||
|
||||
16
trailbase-qs/Cargo.toml
Normal file
16
trailbase-qs/Cargo.toml
Normal file
@@ -0,0 +1,16 @@
|
||||
[package]
|
||||
name = "trailbase-qs"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
license = "OSL-3.0"
|
||||
description = "Query string parser for TrailBase"
|
||||
homepage = "https://trailbase.io"
|
||||
repository = "https://github.com/trailbaseio/trailbase"
|
||||
readme = "../README.md"
|
||||
|
||||
[dependencies]
|
||||
base64 = "0.22.1"
|
||||
itertools = "0.14.0"
|
||||
serde = "1.0.219"
|
||||
serde-value = "0.7.0"
|
||||
serde_qs = "0.15.0"
|
||||
218
trailbase-qs/src/column_rel_value.rs
Normal file
218
trailbase-qs/src/column_rel_value.rs
Normal file
@@ -0,0 +1,218 @@
|
||||
use serde::de::{Deserializer, Error};
|
||||
|
||||
use crate::value::{Value, unexpected};
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub enum CompareOp {
|
||||
Not,
|
||||
Equal,
|
||||
NotEqual,
|
||||
GreaterThanEqual,
|
||||
GreaterThan,
|
||||
LessThanEqual,
|
||||
LessThan,
|
||||
Like,
|
||||
Regexp,
|
||||
}
|
||||
|
||||
impl CompareOp {
|
||||
pub fn from(qualifier: &str) -> Option<Self> {
|
||||
return match qualifier {
|
||||
"gte" => Some(Self::GreaterThanEqual),
|
||||
"gt" => Some(Self::GreaterThan),
|
||||
"lte" => Some(Self::LessThanEqual),
|
||||
"lt" => Some(Self::LessThan),
|
||||
"eq" => Some(Self::Equal),
|
||||
"not" => Some(Self::Not),
|
||||
"ne" => Some(Self::NotEqual),
|
||||
"like" => Some(Self::Like),
|
||||
"re" => Some(Self::Regexp),
|
||||
_ => None,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn to_sql(self) -> &'static str {
|
||||
return match self {
|
||||
Self::GreaterThanEqual => ">=",
|
||||
Self::GreaterThan => ">",
|
||||
Self::LessThanEqual => "<=",
|
||||
Self::LessThan => "<",
|
||||
Self::Not => "<>",
|
||||
Self::NotEqual => "<>",
|
||||
Self::Like => "LIKE",
|
||||
Self::Regexp => "REGEXP",
|
||||
Self::Equal => "=",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> serde::de::Deserialize<'de> for CompareOp {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
use serde_value::Value;
|
||||
let value = Value::deserialize(deserializer)?;
|
||||
|
||||
let Value::String(ref string) = value else {
|
||||
return Err(Error::invalid_type(unexpected(&value), &"String"));
|
||||
};
|
||||
|
||||
return CompareOp::from(string)
|
||||
.ok_or_else(|| Error::invalid_type(unexpected(&value), &"(eq|ne)"));
|
||||
}
|
||||
}
|
||||
|
||||
/// Type to support query of shape: `[column][op]=value`.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct ColumnOpValue {
|
||||
pub column: String,
|
||||
pub op: CompareOp,
|
||||
pub value: Value,
|
||||
}
|
||||
|
||||
impl ColumnOpValue {
|
||||
pub fn to_sql(&self) -> String {
|
||||
return format!(
|
||||
"{c} {o} {v}",
|
||||
c = self.column,
|
||||
o = self.op.to_sql(),
|
||||
v = self.value.to_sql()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn serde_value_to_single_column_rel_value<'de, D>(
|
||||
key: String,
|
||||
value: serde_value::Value,
|
||||
) -> Result<ColumnOpValue, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
use serde_value::Value;
|
||||
|
||||
return match value {
|
||||
Value::String(_) => Ok(ColumnOpValue {
|
||||
column: key,
|
||||
op: CompareOp::Equal,
|
||||
value: crate::value::serde_value_to_value::<D>(value)?,
|
||||
}),
|
||||
Value::Map(mut m) if m.len() == 1 => {
|
||||
let (k, v) = m.pop_first().expect("len() == 1");
|
||||
|
||||
let op = k.deserialize_into::<CompareOp>().map_err(Error::custom)?;
|
||||
|
||||
Ok(ColumnOpValue {
|
||||
column: key,
|
||||
op,
|
||||
value: crate::value::serde_value_to_value::<D>(v)?,
|
||||
})
|
||||
}
|
||||
v => Err(Error::invalid_type(
|
||||
unexpected(&v),
|
||||
&"Map<String, String | Map<Rel, String>>",
|
||||
)),
|
||||
};
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
pub struct ColumnOpValueMap(pub(crate) Vec<ColumnOpValue>);
|
||||
|
||||
impl<'de> serde::de::Deserialize<'de> for ColumnOpValueMap {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
use serde_value::Value;
|
||||
let value = Value::deserialize(deserializer)?;
|
||||
|
||||
let Value::Map(m) = value else {
|
||||
return Err(Error::invalid_type(
|
||||
unexpected(&value),
|
||||
&"Map<String, String | Map<Rel, String>>",
|
||||
));
|
||||
};
|
||||
|
||||
let vec = m
|
||||
.into_iter()
|
||||
.map(|(k, v)| {
|
||||
return match (k, v) {
|
||||
(Value::String(key), v) => serde_value_to_single_column_rel_value::<D>(key, v),
|
||||
(k, _) => Err(Error::invalid_type(unexpected(&k), &"String")),
|
||||
};
|
||||
})
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
|
||||
return Ok(ColumnOpValueMap(vec));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
use serde::Deserialize;
|
||||
use serde_qs::Config;
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
struct Query {
|
||||
filter: Option<ColumnOpValueMap>,
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_column_rel_value() {
|
||||
let qs = Config::new(5, true);
|
||||
|
||||
let m_empty: Query = qs.deserialize_str("").unwrap();
|
||||
assert_eq!(m_empty.filter, None);
|
||||
|
||||
let m0: Query = qs.deserialize_str("filter[column]=1").unwrap();
|
||||
assert_eq!(
|
||||
m0.filter.unwrap().0,
|
||||
vec![ColumnOpValue {
|
||||
column: "column".to_string(),
|
||||
op: CompareOp::Equal,
|
||||
value: Value::Integer(1),
|
||||
}]
|
||||
);
|
||||
|
||||
let m0: Query = qs.deserialize_str("filter[column]=true").unwrap();
|
||||
assert_eq!(
|
||||
m0.filter.unwrap().0,
|
||||
vec![ColumnOpValue {
|
||||
column: "column".to_string(),
|
||||
op: CompareOp::Equal,
|
||||
value: Value::Bool(true),
|
||||
}]
|
||||
);
|
||||
|
||||
let m1: Query = qs.deserialize_str("filter[column][eq]=1").unwrap();
|
||||
assert_eq!(
|
||||
m1.filter.unwrap().0,
|
||||
vec![ColumnOpValue {
|
||||
column: "column".to_string(),
|
||||
op: CompareOp::Equal,
|
||||
value: Value::Integer(1),
|
||||
}]
|
||||
);
|
||||
|
||||
let m2: Query = qs
|
||||
.deserialize_str("filter[col1][ne]=1&filter[col2]=2")
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
m2.filter.unwrap().0,
|
||||
vec![
|
||||
ColumnOpValue {
|
||||
column: "col1".to_string(),
|
||||
op: CompareOp::NotEqual,
|
||||
value: Value::Integer(1),
|
||||
},
|
||||
ColumnOpValue {
|
||||
column: "col2".to_string(),
|
||||
op: CompareOp::Equal,
|
||||
value: Value::Integer(2),
|
||||
},
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
259
trailbase-qs/src/filter.rs
Normal file
259
trailbase-qs/src/filter.rs
Normal file
@@ -0,0 +1,259 @@
|
||||
/// Custom deserializers for urlencoded filters.
|
||||
///
|
||||
/// Supported examples:
|
||||
///
|
||||
/// filters[column]=value <-- might be nice otherwise below.
|
||||
/// filter[column]=value
|
||||
/// filters[column][eq]=value
|
||||
/// filters[and][0][column0][eq]=value0&filters[and][1][column1][eq]=value1
|
||||
/// filters[and][0][or][0][column0]=value0&[and][0][or][1][column1]=value1
|
||||
use itertools::Itertools;
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use crate::column_rel_value::{ColumnOpValue, serde_value_to_single_column_rel_value};
|
||||
use crate::value::unexpected;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum Combiner {
|
||||
And,
|
||||
Or,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum ValueOrComposite {
|
||||
Value(ColumnOpValue),
|
||||
Composite(Combiner, Vec<ValueOrComposite>),
|
||||
}
|
||||
|
||||
impl ValueOrComposite {
|
||||
#[allow(unused)]
|
||||
pub fn to_sql(&self) -> String {
|
||||
let mut fragments: Vec<String> = vec![];
|
||||
match self {
|
||||
Self::Value(v) => fragments.push(v.to_sql()),
|
||||
Self::Composite(combiner, vec) => {
|
||||
let f = vec.iter().map(|v| v.to_sql()).join(match combiner {
|
||||
Combiner::And => " AND ",
|
||||
Combiner::Or => " OR ",
|
||||
});
|
||||
fragments.push(format!("({f})"));
|
||||
}
|
||||
};
|
||||
return fragments.join(" ");
|
||||
}
|
||||
}
|
||||
|
||||
fn serde_value_to_value_or_composite<'de, D>(
|
||||
value: serde_value::Value,
|
||||
depth: usize,
|
||||
) -> Result<ValueOrComposite, D::Error>
|
||||
where
|
||||
D: serde::de::Deserializer<'de>,
|
||||
{
|
||||
use serde::de::Error;
|
||||
use serde_value::Value;
|
||||
|
||||
// Limit recursion depth
|
||||
if depth >= 5 {
|
||||
return Err(Error::custom("Recursion limit exceeded"));
|
||||
}
|
||||
|
||||
// We always expect [key] = value, i.e. a Map[key] = value.
|
||||
let Value::Map(mut m) = value else {
|
||||
return Err(Error::invalid_type(unexpected(&value), &"Map1"));
|
||||
};
|
||||
|
||||
if m.is_empty() {
|
||||
// We could also error here, but this allows empty query-string.
|
||||
return Ok(ValueOrComposite::Composite(Combiner::And, vec![]));
|
||||
} else if m.len() > 1 {
|
||||
// Multiple entries on the same same level => Implicit AND composite
|
||||
let vec = m
|
||||
.into_iter()
|
||||
.map(|(k, v)| {
|
||||
return match k {
|
||||
Value::String(key) => match key.as_str() {
|
||||
"$and" | "$or" => serde_value_to_value_or_composite::<D>(
|
||||
Value::Map(BTreeMap::from([(Value::String(key), v)])),
|
||||
depth + 1,
|
||||
),
|
||||
_ => Ok(ValueOrComposite::Value(
|
||||
serde_value_to_single_column_rel_value::<D>(key, v)?,
|
||||
)),
|
||||
},
|
||||
_ => Err(Error::invalid_type(unexpected(&k), &"Map0")),
|
||||
};
|
||||
})
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
|
||||
return Ok(ValueOrComposite::Composite(Combiner::And, vec));
|
||||
}
|
||||
|
||||
let combine = |combiner: Combiner, values: Value| -> Result<ValueOrComposite, D::Error> {
|
||||
match values {
|
||||
Value::Seq(vec) => {
|
||||
if vec.len() < 2 {
|
||||
return Err(serde::de::Error::invalid_length(
|
||||
vec.len(),
|
||||
&"Sequence with 2 or more elements",
|
||||
));
|
||||
}
|
||||
|
||||
return Ok(ValueOrComposite::Composite(
|
||||
combiner,
|
||||
vec
|
||||
.into_iter()
|
||||
.map(|v| serde_value_to_value_or_composite::<D>(v, depth + 1))
|
||||
.collect::<Result<Vec<_>, _>>()?,
|
||||
));
|
||||
}
|
||||
v => Err(Error::invalid_type(unexpected(&v), &"Sequence")),
|
||||
}
|
||||
};
|
||||
|
||||
// We iterate only to take ownership.
|
||||
match m.pop_first().expect("len == 1") {
|
||||
(Value::String(str), v) => match str.as_str() {
|
||||
"$and" => combine(Combiner::And, v),
|
||||
"$or" => combine(Combiner::Or, v),
|
||||
_ => Ok(ValueOrComposite::Value(
|
||||
serde_value_to_single_column_rel_value::<D>(str, v)?,
|
||||
)),
|
||||
},
|
||||
(k, _) => Err(Error::invalid_type(unexpected(&k), &"String")),
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> serde::de::Deserialize<'de> for ValueOrComposite {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::de::Deserializer<'de>,
|
||||
{
|
||||
use serde_value::Value;
|
||||
let value = Value::deserialize(deserializer)?;
|
||||
|
||||
return serde_value_to_value_or_composite::<D>(value, 0);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
use serde::Deserialize;
|
||||
use serde_qs::Config;
|
||||
|
||||
use crate::column_rel_value::{ColumnOpValue, CompareOp};
|
||||
use crate::value::Value;
|
||||
|
||||
#[derive(Clone, Debug, Default, Deserialize)]
|
||||
struct Query {
|
||||
composite_filter: Option<ValueOrComposite>,
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_filter_parsing() {
|
||||
let qs = Config::new(5, true);
|
||||
|
||||
let m_empty: Query = qs.deserialize_str("").unwrap();
|
||||
assert_eq!(m_empty.composite_filter, None);
|
||||
|
||||
let m0: Result<Query, _> = qs.deserialize_str("composite_filter[$and][0][col0]=val0");
|
||||
assert!(m0.is_err(), "{m0:?}");
|
||||
|
||||
let m1: Query = qs
|
||||
.deserialize_str("composite_filter[$and][0][col0]=val0&composite_filter[$and][1][col1]=val1")
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
m1.composite_filter.unwrap(),
|
||||
ValueOrComposite::Composite(
|
||||
Combiner::And,
|
||||
vec![
|
||||
ValueOrComposite::Value(ColumnOpValue {
|
||||
column: "col0".to_string(),
|
||||
op: CompareOp::Equal,
|
||||
value: Value::String("val0".to_string()),
|
||||
}),
|
||||
ValueOrComposite::Value(ColumnOpValue {
|
||||
column: "col1".to_string(),
|
||||
op: CompareOp::Equal,
|
||||
value: Value::String("val1".to_string()),
|
||||
}),
|
||||
]
|
||||
)
|
||||
);
|
||||
|
||||
assert!(
|
||||
qs.deserialize_str::<Query>(
|
||||
"composite_filter[$and][0][col0]=val0&composite_filter[$or][1][col1]=val1",
|
||||
)
|
||||
.is_err()
|
||||
);
|
||||
|
||||
let m2: Query = qs
|
||||
.deserialize_str("composite_filter[col0]=val0&composite_filter[col1]=val1")
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
m2.composite_filter.unwrap(),
|
||||
ValueOrComposite::Composite(
|
||||
Combiner::And,
|
||||
vec![
|
||||
ValueOrComposite::Value(ColumnOpValue {
|
||||
column: "col0".to_string(),
|
||||
op: CompareOp::Equal,
|
||||
value: Value::String("val0".to_string()),
|
||||
}),
|
||||
ValueOrComposite::Value(ColumnOpValue {
|
||||
column: "col1".to_string(),
|
||||
op: CompareOp::Equal,
|
||||
value: Value::String("val1".to_string()),
|
||||
}),
|
||||
]
|
||||
)
|
||||
);
|
||||
|
||||
// Too few elements
|
||||
let m4: Result<Query, _> = qs.deserialize_str("composite_filter[$and][0][col0]=val0");
|
||||
assert!(m4.is_err(), "{m4:?}");
|
||||
|
||||
// Too few elements
|
||||
let m3: Result<Query, _> =
|
||||
qs.deserialize_str("composite_filter[col0]=val0&composite_filter[$and][0][col0]=val0&composite_filter[col1]=val1");
|
||||
assert!(m3.is_err(), "{m3:?}");
|
||||
|
||||
// Implicit and with nested or and out of order.
|
||||
let m5: Query = qs.deserialize_str("composite_filter[$or][1][col0][ne]=val0&composite_filter[col1]=1&composite_filter[$or][0][col2]=val2").unwrap();
|
||||
assert_eq!(
|
||||
m5.composite_filter.as_ref().unwrap(),
|
||||
&ValueOrComposite::Composite(
|
||||
Combiner::And,
|
||||
vec![
|
||||
ValueOrComposite::Composite(
|
||||
Combiner::Or,
|
||||
vec![
|
||||
ValueOrComposite::Value(ColumnOpValue {
|
||||
column: "col2".to_string(),
|
||||
op: CompareOp::Equal,
|
||||
value: Value::String("val2".to_string()),
|
||||
}),
|
||||
ValueOrComposite::Value(ColumnOpValue {
|
||||
column: "col0".to_string(),
|
||||
op: CompareOp::NotEqual,
|
||||
value: Value::String("val0".to_string()),
|
||||
}),
|
||||
]
|
||||
),
|
||||
ValueOrComposite::Value(ColumnOpValue {
|
||||
column: "col1".to_string(),
|
||||
op: CompareOp::Equal,
|
||||
value: Value::Integer(1),
|
||||
}),
|
||||
]
|
||||
)
|
||||
);
|
||||
assert_eq!(
|
||||
m5.composite_filter.unwrap().to_sql(),
|
||||
"((col2 = 'val2' OR col0 <> 'val0') AND col1 = 1)"
|
||||
);
|
||||
}
|
||||
}
|
||||
7
trailbase-qs/src/lib.rs
Normal file
7
trailbase-qs/src/lib.rs
Normal file
@@ -0,0 +1,7 @@
|
||||
#![forbid(unsafe_code, clippy::unwrap_used)]
|
||||
#![allow(clippy::needless_return)]
|
||||
#![warn(clippy::await_holding_lock, clippy::inefficient_to_string)]
|
||||
|
||||
mod column_rel_value;
|
||||
mod filter;
|
||||
mod value;
|
||||
171
trailbase-qs/src/value.rs
Normal file
171
trailbase-qs/src/value.rs
Normal file
@@ -0,0 +1,171 @@
|
||||
use base64::prelude::*;
|
||||
use serde::de::{Deserialize, Deserializer, Error, Unexpected};
|
||||
use std::str::FromStr;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum Value {
|
||||
// Note that bytes are also strings, either UUID or url-safe-b64 encoded. Need to be decoded
|
||||
// downstream based on content.
|
||||
String(String),
|
||||
Integer(i64),
|
||||
Double(f64),
|
||||
Bool(bool),
|
||||
}
|
||||
|
||||
impl Value {
|
||||
fn unparse(value: String) -> Self {
|
||||
return match value.as_str() {
|
||||
"true" | "TRUE" => Value::Bool(true),
|
||||
"false" | "FALSE" => Value::Bool(false),
|
||||
_ => {
|
||||
if let Ok(i) = i64::from_str(&value) {
|
||||
Value::Integer(i)
|
||||
} else if let Ok(d) = f64::from_str(&value) {
|
||||
Value::Double(d)
|
||||
} else {
|
||||
Value::String(value)
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
pub fn to_sql(&self) -> String {
|
||||
return match self {
|
||||
Self::String(s) => format!("'{s}'"),
|
||||
Self::Integer(i) => i.to_string(),
|
||||
Self::Double(d) => d.to_string(),
|
||||
Self::Bool(b) => match b {
|
||||
true => "TRUE".to_string(),
|
||||
false => "false".to_string(),
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Value {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
return match self {
|
||||
Self::String(s) => s.fmt(f),
|
||||
Self::Integer(i) => i.fmt(f),
|
||||
Self::Double(d) => d.fmt(f),
|
||||
Self::Bool(b) => b.fmt(f),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
pub fn serde_value_to_value<'de, D>(value: serde_value::Value) -> Result<Value, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
return match value {
|
||||
serde_value::Value::String(value) => Ok(Value::unparse(value)),
|
||||
serde_value::Value::Bytes(bytes) => Ok(Value::String(BASE64_URL_SAFE.encode(bytes))),
|
||||
serde_value::Value::I64(i) => Ok(Value::Integer(i)),
|
||||
serde_value::Value::I32(i) => Ok(Value::Integer(i as i64)),
|
||||
serde_value::Value::I16(i) => Ok(Value::Integer(i as i64)),
|
||||
serde_value::Value::I8(i) => Ok(Value::Integer(i as i64)),
|
||||
serde_value::Value::U64(i) => Ok(Value::Integer(i as i64)),
|
||||
serde_value::Value::U32(i) => Ok(Value::Integer(i as i64)),
|
||||
serde_value::Value::U16(i) => Ok(Value::Integer(i as i64)),
|
||||
serde_value::Value::U8(i) => Ok(Value::Integer(i as i64)),
|
||||
serde_value::Value::Bool(b) => Ok(Value::Bool(b)),
|
||||
_ => Err(Error::invalid_type(unexpected(&value), &"Value")),
|
||||
};
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for Value {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
use serde_value::Value;
|
||||
let value = Value::deserialize(deserializer)?;
|
||||
|
||||
return serde_value_to_value::<'de, D>(value);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn unexpected(value: &serde_value::Value) -> Unexpected {
|
||||
use serde_value::Value;
|
||||
match *value {
|
||||
Value::Bool(b) => serde::de::Unexpected::Bool(b),
|
||||
Value::U8(n) => serde::de::Unexpected::Unsigned(n as u64),
|
||||
Value::U16(n) => serde::de::Unexpected::Unsigned(n as u64),
|
||||
Value::U32(n) => serde::de::Unexpected::Unsigned(n as u64),
|
||||
Value::U64(n) => serde::de::Unexpected::Unsigned(n),
|
||||
Value::I8(n) => serde::de::Unexpected::Signed(n as i64),
|
||||
Value::I16(n) => serde::de::Unexpected::Signed(n as i64),
|
||||
Value::I32(n) => serde::de::Unexpected::Signed(n as i64),
|
||||
Value::I64(n) => serde::de::Unexpected::Signed(n),
|
||||
Value::F32(n) => serde::de::Unexpected::Float(n as f64),
|
||||
Value::F64(n) => serde::de::Unexpected::Float(n),
|
||||
Value::Char(c) => serde::de::Unexpected::Char(c),
|
||||
Value::String(ref s) => serde::de::Unexpected::Str(s),
|
||||
Value::Unit => serde::de::Unexpected::Unit,
|
||||
Value::Option(_) => serde::de::Unexpected::Option,
|
||||
Value::Newtype(_) => serde::de::Unexpected::NewtypeStruct,
|
||||
Value::Seq(_) => serde::de::Unexpected::Seq,
|
||||
Value::Map(_) => serde::de::Unexpected::Map,
|
||||
Value::Bytes(ref b) => serde::de::Unexpected::Bytes(b),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
use serde::Deserialize;
|
||||
use serde_qs::Config;
|
||||
|
||||
use crate::column_rel_value::{ColumnOpValue, CompareOp};
|
||||
use crate::filter::ValueOrComposite;
|
||||
|
||||
#[derive(Clone, Debug, Default, Deserialize)]
|
||||
struct Query {
|
||||
filter: Option<ValueOrComposite>,
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_value() {
|
||||
let qs = Config::new(5, true);
|
||||
|
||||
let v0: Query = qs.deserialize_str("filter[col0][eq]=val0").unwrap();
|
||||
assert_eq!(
|
||||
v0.filter.unwrap(),
|
||||
ValueOrComposite::Value(ColumnOpValue {
|
||||
column: "col0".to_string(),
|
||||
op: CompareOp::Equal,
|
||||
value: Value::String("val0".to_string()),
|
||||
})
|
||||
);
|
||||
let v1: Query = qs.deserialize_str("filter[col0][ne]=TRUE").unwrap();
|
||||
assert_eq!(
|
||||
v1.filter.unwrap(),
|
||||
ValueOrComposite::Value(ColumnOpValue {
|
||||
column: "col0".to_string(),
|
||||
op: CompareOp::NotEqual,
|
||||
value: Value::Bool(true),
|
||||
})
|
||||
);
|
||||
|
||||
let v2: Query = qs.deserialize_str("filter[col0][ne]=0").unwrap();
|
||||
assert_eq!(
|
||||
v2.filter.unwrap(),
|
||||
ValueOrComposite::Value(ColumnOpValue {
|
||||
column: "col0".to_string(),
|
||||
op: CompareOp::NotEqual,
|
||||
value: Value::Integer(0),
|
||||
})
|
||||
);
|
||||
|
||||
let v3: Query = qs.deserialize_str("filter[col0][ne]=0.0").unwrap();
|
||||
assert_eq!(
|
||||
v3.filter.unwrap(),
|
||||
ValueOrComposite::Value(ColumnOpValue {
|
||||
column: "col0".to_string(),
|
||||
op: CompareOp::NotEqual,
|
||||
value: Value::Double(0.0),
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user