#[derive(Debug, Clone)]
pub struct ExpandQuery {
expand_expression: String,
levels: Option<u32>,
}
impl Default for ExpandQuery {
fn default() -> Self {
Self {
expand_expression: ".".to_string(),
levels: Some(1),
}
}
}
impl ExpandQuery {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn no_links() -> Self {
Self {
expand_expression: ".".to_string(),
levels: None,
}
}
#[must_use]
pub fn all() -> Self {
Self {
expand_expression: "*".to_string(),
levels: Some(1),
}
}
#[must_use]
pub fn current() -> Self {
Self {
expand_expression: ".".to_string(),
levels: Some(1),
}
}
#[must_use]
pub fn links() -> Self {
Self {
expand_expression: "~".to_string(),
levels: Some(1),
}
}
pub fn property<S: Into<String>>(property: S) -> Self {
Self {
expand_expression: property.into(),
levels: Some(1),
}
}
#[must_use]
pub fn properties(properties: &[&str]) -> Self {
Self {
expand_expression: properties.join(","),
levels: Some(1),
}
}
#[must_use]
pub const fn levels(mut self, levels: u32) -> Self {
self.levels = Some(levels);
self
}
#[must_use]
#[allow(clippy::option_if_let_else)]
pub fn to_query_string(&self) -> String {
match self.levels {
Some(levels) => format!("$expand={}($levels={})", self.expand_expression, levels),
None => format!("$expand={}", self.expand_expression),
}
}
}
#[derive(Debug, Clone)]
pub enum FilterLiteral {
String(String),
Number(f64),
Integer(i64),
Boolean(bool),
}
impl FilterLiteral {
fn to_odata_string(&self) -> String {
match self {
Self::String(s) => format!("'{}'", s.replace('\'', "''")),
Self::Number(n) => n.to_string(),
Self::Integer(i) => i.to_string(),
Self::Boolean(b) => b.to_string(),
}
}
}
pub trait ToFilterLiteral {
fn to_filter_literal(self) -> FilterLiteral;
}
impl ToFilterLiteral for &str {
fn to_filter_literal(self) -> FilterLiteral {
FilterLiteral::String(self.to_string())
}
}
impl ToFilterLiteral for String {
fn to_filter_literal(self) -> FilterLiteral {
FilterLiteral::String(self)
}
}
impl ToFilterLiteral for i32 {
fn to_filter_literal(self) -> FilterLiteral {
FilterLiteral::Integer(i64::from(self))
}
}
impl ToFilterLiteral for i64 {
fn to_filter_literal(self) -> FilterLiteral {
FilterLiteral::Integer(self)
}
}
impl ToFilterLiteral for f64 {
fn to_filter_literal(self) -> FilterLiteral {
FilterLiteral::Number(self)
}
}
impl ToFilterLiteral for bool {
fn to_filter_literal(self) -> FilterLiteral {
FilterLiteral::Boolean(self)
}
}
#[derive(Debug, Clone)]
enum FilterExpr {
Comparison {
property: String,
operator: &'static str,
value: FilterLiteral,
},
And(Box<FilterExpr>, Box<FilterExpr>),
Or(Box<FilterExpr>, Box<FilterExpr>),
Not(Box<FilterExpr>),
Group(Box<FilterExpr>),
}
impl FilterExpr {
fn to_odata_string(&self) -> String {
match self {
Self::Comparison {
property,
operator,
value,
} => {
format!("{} {} {}", property, operator, value.to_odata_string())
}
Self::And(left, right) => {
format!("{} and {}", left.to_odata_string(), right.to_odata_string())
}
Self::Or(left, right) => {
format!("{} or {}", left.to_odata_string(), right.to_odata_string())
}
Self::Not(expr) => {
format!("not {}", expr.to_odata_string())
}
Self::Group(expr) => {
format!("({})", expr.to_odata_string())
}
}
}
}
#[derive(Debug, Clone)]
pub struct FilterQuery {
expr: Option<FilterExpr>,
pending_logical_op: Option<LogicalOp>,
}
#[derive(Debug, Clone, Copy)]
enum LogicalOp {
And,
Or,
}
impl FilterQuery {
pub fn eq<P: crate::FilterProperty, V: ToFilterLiteral>(property: &P, value: V) -> Self {
Self {
expr: Some(FilterExpr::Comparison {
property: property.property_path().to_string(),
operator: "eq",
value: value.to_filter_literal(),
}),
pending_logical_op: None,
}
}
pub fn ne<P: crate::FilterProperty, V: ToFilterLiteral>(property: &P, value: V) -> Self {
Self {
expr: Some(FilterExpr::Comparison {
property: property.property_path().to_string(),
operator: "ne",
value: value.to_filter_literal(),
}),
pending_logical_op: None,
}
}
pub fn gt<P: crate::FilterProperty, V: ToFilterLiteral>(property: &P, value: V) -> Self {
Self {
expr: Some(FilterExpr::Comparison {
property: property.property_path().to_string(),
operator: "gt",
value: value.to_filter_literal(),
}),
pending_logical_op: None,
}
}
pub fn ge<P: crate::FilterProperty, V: ToFilterLiteral>(property: &P, value: V) -> Self {
Self {
expr: Some(FilterExpr::Comparison {
property: property.property_path().to_string(),
operator: "ge",
value: value.to_filter_literal(),
}),
pending_logical_op: None,
}
}
pub fn lt<P: crate::FilterProperty, V: ToFilterLiteral>(property: &P, value: V) -> Self {
Self {
expr: Some(FilterExpr::Comparison {
property: property.property_path().to_string(),
operator: "lt",
value: value.to_filter_literal(),
}),
pending_logical_op: None,
}
}
pub fn le<P: crate::FilterProperty, V: ToFilterLiteral>(property: &P, value: V) -> Self {
Self {
expr: Some(FilterExpr::Comparison {
property: property.property_path().to_string(),
operator: "le",
value: value.to_filter_literal(),
}),
pending_logical_op: None,
}
}
#[must_use]
pub const fn and(mut self) -> Self {
self.pending_logical_op = Some(LogicalOp::And);
self
}
#[must_use]
pub const fn or(mut self) -> Self {
self.pending_logical_op = Some(LogicalOp::Or);
self
}
#[must_use]
#[allow(clippy::should_implement_trait)]
pub fn not(mut self) -> Self {
if let Some(expr) = self.expr.take() {
self.expr = Some(FilterExpr::Not(Box::new(expr)));
}
self
}
#[must_use]
pub fn group(mut self) -> Self {
if let Some(expr) = self.expr.take() {
self.expr = Some(FilterExpr::Group(Box::new(expr)));
}
self
}
#[must_use]
pub fn eq_then<P: crate::FilterProperty, V: ToFilterLiteral>(
self,
property: &P,
value: V,
) -> Self {
let new_expr = FilterExpr::Comparison {
property: property.property_path().to_string(),
operator: "eq",
value: value.to_filter_literal(),
};
self.combine_with_pending_op(new_expr)
}
#[must_use]
pub fn ne_then<P: crate::FilterProperty, V: ToFilterLiteral>(
self,
property: &P,
value: V,
) -> Self {
let new_expr = FilterExpr::Comparison {
property: property.property_path().to_string(),
operator: "ne",
value: value.to_filter_literal(),
};
self.combine_with_pending_op(new_expr)
}
#[must_use]
pub fn gt_then<P: crate::FilterProperty, V: ToFilterLiteral>(
self,
property: &P,
value: V,
) -> Self {
let new_expr = FilterExpr::Comparison {
property: property.property_path().to_string(),
operator: "gt",
value: value.to_filter_literal(),
};
self.combine_with_pending_op(new_expr)
}
#[must_use]
pub fn ge_then<P: crate::FilterProperty, V: ToFilterLiteral>(
self,
property: &P,
value: V,
) -> Self {
let new_expr = FilterExpr::Comparison {
property: property.property_path().to_string(),
operator: "ge",
value: value.to_filter_literal(),
};
self.combine_with_pending_op(new_expr)
}
#[must_use]
pub fn lt_then<P: crate::FilterProperty, V: ToFilterLiteral>(
self,
property: &P,
value: V,
) -> Self {
let new_expr = FilterExpr::Comparison {
property: property.property_path().to_string(),
operator: "lt",
value: value.to_filter_literal(),
};
self.combine_with_pending_op(new_expr)
}
#[must_use]
pub fn le_then<P: crate::FilterProperty, V: ToFilterLiteral>(
self,
property: &P,
value: V,
) -> Self {
let new_expr = FilterExpr::Comparison {
property: property.property_path().to_string(),
operator: "le",
value: value.to_filter_literal(),
};
self.combine_with_pending_op(new_expr)
}
fn combine_with_pending_op(mut self, new_expr: FilterExpr) -> Self {
if let Some(existing) = self.expr.take() {
self.expr = Some(match self.pending_logical_op.take() {
Some(LogicalOp::And) => FilterExpr::And(Box::new(existing), Box::new(new_expr)),
Some(LogicalOp::Or) => FilterExpr::Or(Box::new(existing), Box::new(new_expr)),
None => new_expr,
});
} else {
self.expr = Some(new_expr);
}
self
}
#[must_use]
pub fn to_query_string(&self) -> String {
self.expr.as_ref().map_or_else(String::new, |expr| {
format!("$filter={}", expr.to_odata_string())
})
}
}
impl crate::FilterProperty for &str {
fn property_path(&self) -> &str {
self
}
}
impl crate::FilterProperty for String {
fn property_path(&self) -> &str {
self.as_str()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_expand() {
let query = ExpandQuery::default();
assert_eq!(query.to_query_string(), "$expand=.($levels=1)");
}
#[test]
fn test_expand_all() {
let query = ExpandQuery::all();
assert_eq!(query.to_query_string(), "$expand=*($levels=1)");
}
#[test]
fn test_expand_current() {
let query = ExpandQuery::current();
assert_eq!(query.to_query_string(), "$expand=.($levels=1)");
}
#[test]
fn test_expand_links() {
let query = ExpandQuery::links();
assert_eq!(query.to_query_string(), "$expand=~($levels=1)");
}
#[test]
fn test_expand_property() {
let query = ExpandQuery::property("Thermal");
assert_eq!(query.to_query_string(), "$expand=Thermal($levels=1)");
}
#[test]
fn test_expand_properties() {
let query = ExpandQuery::properties(&["Thermal", "Power"]);
assert_eq!(query.to_query_string(), "$expand=Thermal,Power($levels=1)");
}
#[test]
fn test_expand_with_levels() {
let query = ExpandQuery::all().levels(3);
assert_eq!(query.to_query_string(), "$expand=*($levels=3)");
}
#[test]
fn test_simple_eq() {
let filter = FilterQuery::eq(&"Count", 2);
assert_eq!(filter.to_query_string(), "$filter=Count eq 2");
}
#[test]
fn test_string_literal() {
let filter = FilterQuery::eq(&"SystemType", "Physical");
assert_eq!(filter.to_query_string(), "$filter=SystemType eq 'Physical'");
}
#[test]
fn test_and_operator() {
let filter = FilterQuery::eq(&"Count", 2)
.and()
.eq_then(&"Type", "Physical");
assert_eq!(
filter.to_query_string(),
"$filter=Count eq 2 and Type eq 'Physical'"
);
}
#[test]
fn test_or_operator() {
let filter = FilterQuery::eq(&"Count", 2).or().eq_then(&"Count", 4);
assert_eq!(filter.to_query_string(), "$filter=Count eq 2 or Count eq 4");
}
#[test]
fn test_not_operator() {
let filter = FilterQuery::eq(&"Count", 2).not();
assert_eq!(filter.to_query_string(), "$filter=not Count eq 2");
}
#[test]
fn test_grouping() {
let filter = FilterQuery::eq(&"State", "Enabled")
.and()
.eq_then(&"Health", "OK")
.group()
.or()
.eq_then(&"SystemType", "Physical");
assert_eq!(
filter.to_query_string(),
"$filter=(State eq 'Enabled' and Health eq 'OK') or SystemType eq 'Physical'"
);
}
#[test]
fn test_all_comparison_operators() {
assert_eq!(FilterQuery::ne(&"A", 1).to_query_string(), "$filter=A ne 1");
assert_eq!(FilterQuery::gt(&"B", 2).to_query_string(), "$filter=B gt 2");
assert_eq!(FilterQuery::ge(&"C", 3).to_query_string(), "$filter=C ge 3");
assert_eq!(FilterQuery::lt(&"D", 4).to_query_string(), "$filter=D lt 4");
assert_eq!(FilterQuery::le(&"E", 5).to_query_string(), "$filter=E le 5");
}
#[test]
fn test_boolean_literal() {
let filter = FilterQuery::eq(&"Enabled", true);
assert_eq!(filter.to_query_string(), "$filter=Enabled eq true");
}
#[test]
fn test_float_literal() {
let filter = FilterQuery::gt(&"Temperature", 98.6);
assert_eq!(filter.to_query_string(), "$filter=Temperature gt 98.6");
}
#[test]
fn test_string_escaping() {
let filter = FilterQuery::eq(&"Name", "O'Brien");
assert_eq!(filter.to_query_string(), "$filter=Name eq 'O''Brien'");
}
#[test]
fn test_complex_filter() {
let filter = FilterQuery::eq(&"ProcessorSummary/Count", 2)
.and()
.gt_then(&"MemorySummary/TotalSystemMemoryGiB", 64);
assert_eq!(
filter.to_query_string(),
"$filter=ProcessorSummary/Count eq 2 and MemorySummary/TotalSystemMemoryGiB gt 64"
);
}
}