Skip to main content

nv_redfish_core/
query.rs

1// SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2// SPDX-License-Identifier: Apache-2.0
3//
4// Licensed under the Apache License, Version 2.0 (the "License");
5// you may not use this file except in compliance with the License.
6// You may obtain a copy of the License at
7//
8// http://www.apache.org/licenses/LICENSE-2.0
9//
10// Unless required by applicable law or agreed to in writing, software
11// distributed under the License is distributed on an "AS IS" BASIS,
12// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13// See the License for the specific language governing permissions and
14// limitations under the License.
15
16//! OData query parameter builders for Redfish API requests.
17//!
18//! This module provides type-safe builders for constructing OData query parameters
19//! according to the Redfish specification (DSP0266). These query parameters allow
20//! clients to customize API responses through resource expansion and filtering.
21//!
22//! # Query Parameters
23//!
24//! ## Expand Query (`$expand`)
25//!
26//! The [`ExpandQuery`] builder constructs `$expand` parameters to request inline expansion
27//! of navigation properties, reducing the number of HTTP requests needed.
28//!
29//! ```rust
30//! use nv_redfish_core::query::ExpandQuery;
31//!
32//! // Expand current resource with 2 levels
33//! let query = ExpandQuery::current().levels(2);
34//! assert_eq!(query.to_query_string(), "$expand=.($levels=2)");
35//! ```
36//!
37//! ## Filter Query (`$filter`)
38//!
39//! The [`FilterQuery`] builder constructs `$filter` parameters to request server-side
40//! filtering of collection members or resource properties using OData filter expressions.
41//!
42//! ```rust
43//! use nv_redfish_core::query::FilterQuery;
44//!
45//! // Filter for resources where Status/Health equals "OK"
46//! let query = FilterQuery::eq(&"Status/Health", "OK");
47//! assert_eq!(query.to_query_string(), "$filter=Status/Health eq 'OK'");
48//!
49//! // Complex filter with logical operators
50//! let query = FilterQuery::gt(&"Temperature", 50)
51//!     .and()
52//!     .lt_then(&"Temperature", 80);
53//! assert_eq!(query.to_query_string(), "$filter=Temperature gt 50 and Temperature lt 80");
54//! ```
55//!
56//! # Type Safety
57//!
58//! Both builders use traits to ensure type safety:
59//!
60//! - [`crate::FilterProperty`]: Types that can be used as filter property paths
61//! - [`ToFilterLiteral`]: Types that can be converted to filter literal values
62//!
63//! Property paths can be:
64//! - String literals (`"PropertyName"`)
65//! - Generated property accessors from CSDL compilation
66//! - Nested paths (`"Parent/Child"`)
67//!
68//! # References
69//!
70//! - [Redfish Specification DSP0266](https://redfish.dmtf.org/schemas/DSP0266_1.15.0.html)
71//! - [OData Version 4.0 Protocol](http://docs.oasis-open.org/odata/odata/v4.0/os/part2-url-conventions/odata-v4.0-os-part2-url-conventions.html)
72
73/// Builder for Redfish `$expand` query parameters according to DSP0266 specification.
74///
75/// The `$expand` query parameter allows clients to request that the server expand
76/// navigation properties inline instead of returning just references. This is particularly
77/// useful for reducing the number of HTTP requests needed to retrieve related data.
78///
79/// According to the [Redfish specification Table 9](https://redfish.dmtf.org/schemas/DSP0266_1.15.0.html#the-expand-query-parameter),
80/// the supported expand options are:
81///
82/// | Option | Description | Example URL |
83/// |--------|-------------|-------------|
84/// | `*` | Expand all hyperlinks, including payload annotations | `?$expand=*` |
85/// | `.` | Expand hyperlinks not in links property instances | `?$expand=.` |
86/// | `~` | Expand hyperlinks in links property instances | `?$expand=~` |
87/// | `$levels` | Number of levels to cascade expansion | `?$expand=.($levels=2)` |
88///
89/// # Examples
90///
91/// ```rust
92/// use nv_redfish_core::query::ExpandQuery;
93///
94/// // Default: expand current resource one level
95/// let default = ExpandQuery::default();
96/// assert_eq!(default.to_query_string(), "$expand=.($levels=1)");
97///
98/// // Expand all hyperlinks
99/// let all = ExpandQuery::all();
100/// assert_eq!(all.to_query_string(), "$expand=*($levels=1)");
101///
102/// // Expand with multiple levels
103/// let deep = ExpandQuery::current().levels(3);
104/// assert_eq!(deep.to_query_string(), "$expand=.($levels=3)");
105///
106/// // Expand specific navigation property
107/// let thermal = ExpandQuery::property("Thermal");
108/// assert_eq!(thermal.to_query_string(), "$expand=Thermal($levels=1)");
109/// ```
110#[derive(Debug, Clone)]
111pub struct ExpandQuery {
112    /// The expand expression (*, ., ~, or specific navigation properties)
113    expand_expression: String,
114    /// Number of levels to cascade the expand operation (default is 1)
115    levels: Option<u32>,
116}
117
118impl Default for ExpandQuery {
119    /// Default expand query: $expand=.($levels=1)
120    /// Expands all hyperlinks not in any links property instances of the resource
121    fn default() -> Self {
122        Self {
123            expand_expression: ".".to_string(),
124            levels: Some(1),
125        }
126    }
127}
128
129impl ExpandQuery {
130    /// Create a new expand query with default values.
131    ///
132    /// This is equivalent to [`ExpandQuery::default()`] and creates a query
133    /// that expands the current resource one level deep: `$expand=.($levels=1)`.
134    ///
135    /// # Examples
136    ///
137    /// ```rust
138    /// use nv_redfish_core::query::ExpandQuery;
139    ///
140    /// let query = ExpandQuery::new();
141    /// assert_eq!(query.to_query_string(), "$expand=.($levels=1)");
142    /// ```
143    #[must_use]
144    pub fn new() -> Self {
145        Self::default()
146    }
147
148    /// Expand all hyperlinks excluding links.
149    ///
150    /// # Examples
151    ///
152    /// ```rust
153    /// use nv_redfish_core::query::ExpandQuery;
154    ///
155    /// let query = ExpandQuery::no_links();
156    /// assert_eq!(query.to_query_string(), "$expand=.");
157    /// ```
158    #[must_use]
159    pub fn no_links() -> Self {
160        Self {
161            expand_expression: ".".to_string(),
162            levels: None,
163        }
164    }
165
166    /// Expand all hyperlinks, including those in payload annotations.
167    ///
168    /// This expands all hyperlinks found in the resource, including those in payload
169    /// annotations such as `@Redfish.Settings`, `@Redfish.ActionInfo`, and
170    /// `@Redfish.CollectionCapabilities`.
171    ///
172    /// Equivalent to: `$expand=*`
173    ///
174    /// # Examples
175    ///
176    /// ```rust
177    /// use nv_redfish_core::query::ExpandQuery;
178    ///
179    /// let query = ExpandQuery::all();
180    /// assert_eq!(query.to_query_string(), "$expand=*($levels=1)");
181    ///
182    /// // With multiple levels
183    /// let deep = ExpandQuery::all().levels(3);
184    /// assert_eq!(deep.to_query_string(), "$expand=*($levels=3)");
185    /// ```
186    #[must_use]
187    pub fn all() -> Self {
188        Self {
189            expand_expression: "*".to_string(),
190            levels: Some(1),
191        }
192    }
193
194    /// Expand all hyperlinks not in any links property instances of the resource.
195    ///
196    /// This expands hyperlinks found directly in the resource properties, but not
197    /// those in dedicated `Links` sections. Includes payload annotations.
198    ///
199    /// Equivalent to: `$expand=.`
200    ///
201    /// # Examples
202    ///
203    /// ```rust
204    /// use nv_redfish_core::query::ExpandQuery;
205    ///
206    /// let query = ExpandQuery::current();
207    /// assert_eq!(query.to_query_string(), "$expand=.($levels=1)");
208    /// ```
209    #[must_use]
210    pub fn current() -> Self {
211        Self {
212            expand_expression: ".".to_string(),
213            levels: Some(1),
214        }
215    }
216
217    /// Expand all hyperlinks found in all links property instances of the resource.
218    ///
219    /// This expands only hyperlinks found in `Links` sections of the resource,
220    /// which typically contain references to related resources.
221    ///
222    /// Equivalent to: `$expand=~`
223    ///
224    /// # Examples
225    ///
226    /// ```rust
227    /// use nv_redfish_core::query::ExpandQuery;
228    ///
229    /// let query = ExpandQuery::links();
230    /// assert_eq!(query.to_query_string(), "$expand=~($levels=1)");
231    /// ```
232    #[must_use]
233    pub fn links() -> Self {
234        Self {
235            expand_expression: "~".to_string(),
236            levels: Some(1),
237        }
238    }
239
240    /// Expand a specific navigation property.
241    ///
242    /// This expands only the specified navigation property, which is useful when you
243    /// know exactly which related data you need.
244    ///
245    /// # Arguments
246    ///
247    /// * `property` - The name of the navigation property to expand
248    ///
249    /// # Examples
250    ///
251    /// ```rust
252    /// use nv_redfish_core::query::ExpandQuery;
253    ///
254    /// let thermal = ExpandQuery::property("Thermal");
255    /// assert_eq!(thermal.to_query_string(), "$expand=Thermal($levels=1)");
256    ///
257    /// let members = ExpandQuery::property("Members");
258    /// assert_eq!(members.to_query_string(), "$expand=Members($levels=1)");
259    /// ```
260    pub fn property<S: Into<String>>(property: S) -> Self {
261        Self {
262            expand_expression: property.into(),
263            levels: Some(1),
264        }
265    }
266
267    /// Expand multiple specific navigation properties.
268    ///
269    /// This allows expanding several navigation properties in a single request,
270    /// which is more efficient than making separate requests for each property.
271    ///
272    /// # Arguments
273    ///
274    /// * `properties` - A slice of navigation property names to expand
275    ///
276    /// # Examples
277    ///
278    /// ```rust
279    /// use nv_redfish_core::query::ExpandQuery;
280    ///
281    /// let env = ExpandQuery::properties(&["Thermal", "Power"]);
282    /// assert_eq!(env.to_query_string(), "$expand=Thermal,Power($levels=1)");
283    ///
284    /// let system = ExpandQuery::properties(&["Processors", "Memory", "Storage"]);
285    /// assert_eq!(system.to_query_string(), "$expand=Processors,Memory,Storage($levels=1)");
286    /// ```
287    #[must_use]
288    pub fn properties(properties: &[&str]) -> Self {
289        Self {
290            expand_expression: properties.join(","),
291            levels: Some(1),
292        }
293    }
294
295    /// Set the number of levels to cascade the expand operation.
296    ///
297    /// The `$levels` parameter controls how deep the expansion goes:
298    /// - Level 1: Expand hyperlinks in the current resource
299    /// - Level 2: Also expand hyperlinks in the resources expanded at level 1
300    /// - And so on...
301    ///
302    /// # Arguments
303    ///
304    /// * `levels` - Number of levels to expand (typically 1-6 in practice)
305    ///
306    /// # Examples
307    ///
308    /// ```rust
309    /// use nv_redfish_core::query::ExpandQuery;
310    ///
311    /// let shallow = ExpandQuery::current().levels(1);
312    /// assert_eq!(shallow.to_query_string(), "$expand=.($levels=1)");
313    ///
314    /// let deep = ExpandQuery::all().levels(3);
315    /// assert_eq!(deep.to_query_string(), "$expand=*($levels=3)");
316    /// ```
317    #[must_use]
318    pub const fn levels(mut self, levels: u32) -> Self {
319        self.levels = Some(levels);
320        self
321    }
322
323    /// Convert to the `OData` query string according to Redfish specification.
324    ///
325    /// This generates the actual query parameter string that will be appended to
326    /// HTTP requests to Redfish services.
327    ///
328    /// # Returns
329    ///
330    /// A query string in the format `$expand=expression($levels=n)` or just
331    /// `$expand=expression` if no levels are specified.
332    ///
333    /// # Examples
334    ///
335    /// ```rust
336    /// use nv_redfish_core::query::ExpandQuery;
337    ///
338    /// let query = ExpandQuery::property("Thermal").levels(2);
339    /// assert_eq!(query.to_query_string(), "$expand=Thermal($levels=2)");
340    ///
341    /// let query = ExpandQuery::all();
342    /// assert_eq!(query.to_query_string(), "$expand=*($levels=1)");
343    /// ```
344    #[must_use]
345    #[allow(clippy::option_if_let_else)]
346    pub fn to_query_string(&self) -> String {
347        match self.levels {
348            Some(levels) => format!("$expand={}($levels={})", self.expand_expression, levels),
349            None => format!("$expand={}", self.expand_expression),
350        }
351    }
352}
353
354/// Literal value types supported in filter expressions
355#[derive(Debug, Clone)]
356pub enum FilterLiteral {
357    /// String literal value
358    String(String),
359    /// Floating point number literal value
360    Number(f64),
361    /// Integer literal value
362    Integer(i64),
363    /// Boolean literal value
364    Boolean(bool),
365}
366
367impl FilterLiteral {
368    fn to_odata_string(&self) -> String {
369        match self {
370            Self::String(s) => format!("'{}'", s.replace('\'', "''")),
371            Self::Number(n) => n.to_string(),
372            Self::Integer(i) => i.to_string(),
373            Self::Boolean(b) => b.to_string(),
374        }
375    }
376}
377
378/// Trait for types that can be converted to filter literals
379pub trait ToFilterLiteral {
380    /// Convert this value to a filter literal
381    fn to_filter_literal(self) -> FilterLiteral;
382}
383
384impl ToFilterLiteral for &str {
385    fn to_filter_literal(self) -> FilterLiteral {
386        FilterLiteral::String(self.to_string())
387    }
388}
389
390impl ToFilterLiteral for String {
391    fn to_filter_literal(self) -> FilterLiteral {
392        FilterLiteral::String(self)
393    }
394}
395
396impl ToFilterLiteral for i32 {
397    fn to_filter_literal(self) -> FilterLiteral {
398        FilterLiteral::Integer(i64::from(self))
399    }
400}
401
402impl ToFilterLiteral for i64 {
403    fn to_filter_literal(self) -> FilterLiteral {
404        FilterLiteral::Integer(self)
405    }
406}
407
408impl ToFilterLiteral for f64 {
409    fn to_filter_literal(self) -> FilterLiteral {
410        FilterLiteral::Number(self)
411    }
412}
413
414impl ToFilterLiteral for bool {
415    fn to_filter_literal(self) -> FilterLiteral {
416        FilterLiteral::Boolean(self)
417    }
418}
419
420/// Filter expression component
421#[derive(Debug, Clone)]
422enum FilterExpr {
423    Comparison {
424        property: String,
425        operator: &'static str,
426        value: FilterLiteral,
427    },
428    And(Box<FilterExpr>, Box<FilterExpr>),
429    Or(Box<FilterExpr>, Box<FilterExpr>),
430    Not(Box<FilterExpr>),
431    Group(Box<FilterExpr>),
432}
433
434impl FilterExpr {
435    fn to_odata_string(&self) -> String {
436        match self {
437            Self::Comparison {
438                property,
439                operator,
440                value,
441            } => {
442                format!("{} {} {}", property, operator, value.to_odata_string())
443            }
444            Self::And(left, right) => {
445                format!("{} and {}", left.to_odata_string(), right.to_odata_string())
446            }
447            Self::Or(left, right) => {
448                format!("{} or {}", left.to_odata_string(), right.to_odata_string())
449            }
450            Self::Not(expr) => {
451                format!("not {}", expr.to_odata_string())
452            }
453            Self::Group(expr) => {
454                format!("({})", expr.to_odata_string())
455            }
456        }
457    }
458}
459
460/// Builder for Redfish `$filter` query parameters according to DSP0266 specification.
461///
462/// The `$filter` query parameter allows clients to request a subset of collection members
463/// based on comparison and logical expressions.
464///
465/// # Supported Operators
466///
467/// - Comparison: `eq`, `ne`, `gt`, `ge`, `lt`, `le`
468/// - Logical: `and`, `or`, `not`
469/// - Grouping: `()`
470///
471/// # Examples
472///
473/// ```rust
474/// use nv_redfish_core::query::FilterQuery;
475///
476/// // Simple equality
477/// let filter = FilterQuery::eq(&"ProcessorSummary/Count", 2);
478/// assert_eq!(filter.to_query_string(), "$filter=ProcessorSummary/Count eq 2");
479///
480/// // Complex expression with logical operators
481/// let filter = FilterQuery::eq(&"ProcessorSummary/Count", 2)
482///     .and()
483///     .gt_then(&"MemorySummary/TotalSystemMemoryGiB", 64);
484/// assert_eq!(
485///     filter.to_query_string(),
486///     "$filter=ProcessorSummary/Count eq 2 and MemorySummary/TotalSystemMemoryGiB gt 64"
487/// );
488///
489/// // With grouping
490/// let filter = FilterQuery::eq(&"Status/State", "Enabled")
491///     .and()
492///     .eq_then(&"Status/Health", "OK")
493///     .group()
494///     .or()
495///     .eq_then(&"SystemType", "Physical");
496/// ```
497#[derive(Debug, Clone)]
498pub struct FilterQuery {
499    expr: Option<FilterExpr>,
500    pending_logical_op: Option<LogicalOp>,
501}
502
503#[derive(Debug, Clone, Copy)]
504enum LogicalOp {
505    And,
506    Or,
507}
508
509impl FilterQuery {
510    /// Create a new filter with an equality comparison
511    pub fn eq<P: crate::FilterProperty, V: ToFilterLiteral>(property: &P, value: V) -> Self {
512        Self {
513            expr: Some(FilterExpr::Comparison {
514                property: property.property_path().to_string(),
515                operator: "eq",
516                value: value.to_filter_literal(),
517            }),
518            pending_logical_op: None,
519        }
520    }
521
522    /// Create a new filter with a not-equal comparison
523    pub fn ne<P: crate::FilterProperty, V: ToFilterLiteral>(property: &P, value: V) -> Self {
524        Self {
525            expr: Some(FilterExpr::Comparison {
526                property: property.property_path().to_string(),
527                operator: "ne",
528                value: value.to_filter_literal(),
529            }),
530            pending_logical_op: None,
531        }
532    }
533
534    /// Create a new filter with a greater-than comparison
535    pub fn gt<P: crate::FilterProperty, V: ToFilterLiteral>(property: &P, value: V) -> Self {
536        Self {
537            expr: Some(FilterExpr::Comparison {
538                property: property.property_path().to_string(),
539                operator: "gt",
540                value: value.to_filter_literal(),
541            }),
542            pending_logical_op: None,
543        }
544    }
545
546    /// Create a new filter with a greater-than-or-equal comparison
547    pub fn ge<P: crate::FilterProperty, V: ToFilterLiteral>(property: &P, value: V) -> Self {
548        Self {
549            expr: Some(FilterExpr::Comparison {
550                property: property.property_path().to_string(),
551                operator: "ge",
552                value: value.to_filter_literal(),
553            }),
554            pending_logical_op: None,
555        }
556    }
557
558    /// Create a new filter with a less-than comparison
559    pub fn lt<P: crate::FilterProperty, V: ToFilterLiteral>(property: &P, value: V) -> Self {
560        Self {
561            expr: Some(FilterExpr::Comparison {
562                property: property.property_path().to_string(),
563                operator: "lt",
564                value: value.to_filter_literal(),
565            }),
566            pending_logical_op: None,
567        }
568    }
569
570    /// Create a new filter with a less-than-or-equal comparison
571    pub fn le<P: crate::FilterProperty, V: ToFilterLiteral>(property: &P, value: V) -> Self {
572        Self {
573            expr: Some(FilterExpr::Comparison {
574                property: property.property_path().to_string(),
575                operator: "le",
576                value: value.to_filter_literal(),
577            }),
578            pending_logical_op: None,
579        }
580    }
581
582    /// Add a logical AND operator (must be followed by another comparison)
583    #[must_use]
584    pub const fn and(mut self) -> Self {
585        self.pending_logical_op = Some(LogicalOp::And);
586        self
587    }
588
589    /// Add a logical OR operator (must be followed by another comparison)
590    #[must_use]
591    pub const fn or(mut self) -> Self {
592        self.pending_logical_op = Some(LogicalOp::Or);
593        self
594    }
595
596    /// Wrap the current expression in a NOT operator
597    #[must_use]
598    #[allow(clippy::should_implement_trait)]
599    pub fn not(mut self) -> Self {
600        if let Some(expr) = self.expr.take() {
601            self.expr = Some(FilterExpr::Not(Box::new(expr)));
602        }
603        self
604    }
605
606    /// Wrap the current expression in grouping parentheses
607    #[must_use]
608    pub fn group(mut self) -> Self {
609        if let Some(expr) = self.expr.take() {
610            self.expr = Some(FilterExpr::Group(Box::new(expr)));
611        }
612        self
613    }
614
615    /// Chain an equality comparison (after .`and()` or .`or()`)
616    #[must_use]
617    pub fn eq_then<P: crate::FilterProperty, V: ToFilterLiteral>(
618        self,
619        property: &P,
620        value: V,
621    ) -> Self {
622        let new_expr = FilterExpr::Comparison {
623            property: property.property_path().to_string(),
624            operator: "eq",
625            value: value.to_filter_literal(),
626        };
627        self.combine_with_pending_op(new_expr)
628    }
629
630    /// Chain a not-equal comparison (after .`and()` or .`or()`)
631    #[must_use]
632    pub fn ne_then<P: crate::FilterProperty, V: ToFilterLiteral>(
633        self,
634        property: &P,
635        value: V,
636    ) -> Self {
637        let new_expr = FilterExpr::Comparison {
638            property: property.property_path().to_string(),
639            operator: "ne",
640            value: value.to_filter_literal(),
641        };
642        self.combine_with_pending_op(new_expr)
643    }
644
645    /// Chain a greater-than comparison (after .`and()` or .`or()`)
646    #[must_use]
647    pub fn gt_then<P: crate::FilterProperty, V: ToFilterLiteral>(
648        self,
649        property: &P,
650        value: V,
651    ) -> Self {
652        let new_expr = FilterExpr::Comparison {
653            property: property.property_path().to_string(),
654            operator: "gt",
655            value: value.to_filter_literal(),
656        };
657        self.combine_with_pending_op(new_expr)
658    }
659
660    /// Chain a greater-than-or-equal comparison (after .`and()` or .`or()`)
661    #[must_use]
662    pub fn ge_then<P: crate::FilterProperty, V: ToFilterLiteral>(
663        self,
664        property: &P,
665        value: V,
666    ) -> Self {
667        let new_expr = FilterExpr::Comparison {
668            property: property.property_path().to_string(),
669            operator: "ge",
670            value: value.to_filter_literal(),
671        };
672        self.combine_with_pending_op(new_expr)
673    }
674
675    /// Chain a less-than comparison (after .`and()` or .`or()`)
676    #[must_use]
677    pub fn lt_then<P: crate::FilterProperty, V: ToFilterLiteral>(
678        self,
679        property: &P,
680        value: V,
681    ) -> Self {
682        let new_expr = FilterExpr::Comparison {
683            property: property.property_path().to_string(),
684            operator: "lt",
685            value: value.to_filter_literal(),
686        };
687        self.combine_with_pending_op(new_expr)
688    }
689
690    /// Chain a less-than-or-equal comparison (after .`and()` or .`or()`)
691    #[must_use]
692    pub fn le_then<P: crate::FilterProperty, V: ToFilterLiteral>(
693        self,
694        property: &P,
695        value: V,
696    ) -> Self {
697        let new_expr = FilterExpr::Comparison {
698            property: property.property_path().to_string(),
699            operator: "le",
700            value: value.to_filter_literal(),
701        };
702        self.combine_with_pending_op(new_expr)
703    }
704
705    fn combine_with_pending_op(mut self, new_expr: FilterExpr) -> Self {
706        if let Some(existing) = self.expr.take() {
707            self.expr = Some(match self.pending_logical_op.take() {
708                Some(LogicalOp::And) => FilterExpr::And(Box::new(existing), Box::new(new_expr)),
709                Some(LogicalOp::Or) => FilterExpr::Or(Box::new(existing), Box::new(new_expr)),
710                None => new_expr,
711            });
712        } else {
713            self.expr = Some(new_expr);
714        }
715        self
716    }
717
718    /// Convert to the `OData` query string
719    #[must_use]
720    pub fn to_query_string(&self) -> String {
721        self.expr.as_ref().map_or_else(String::new, |expr| {
722            format!("$filter={}", expr.to_odata_string())
723        })
724    }
725}
726
727/// Implement `FilterProperty` for `&str`
728impl crate::FilterProperty for &str {
729    fn property_path(&self) -> &str {
730        self
731    }
732}
733
734/// Implement `FilterProperty` for `String`
735impl crate::FilterProperty for String {
736    fn property_path(&self) -> &str {
737        self.as_str()
738    }
739}
740
741#[cfg(test)]
742mod tests {
743    use super::*;
744
745    #[test]
746    fn test_default_expand() {
747        let query = ExpandQuery::default();
748        assert_eq!(query.to_query_string(), "$expand=.($levels=1)");
749    }
750
751    #[test]
752    fn test_expand_all() {
753        let query = ExpandQuery::all();
754        assert_eq!(query.to_query_string(), "$expand=*($levels=1)");
755    }
756
757    #[test]
758    fn test_expand_current() {
759        let query = ExpandQuery::current();
760        assert_eq!(query.to_query_string(), "$expand=.($levels=1)");
761    }
762
763    #[test]
764    fn test_expand_links() {
765        let query = ExpandQuery::links();
766        assert_eq!(query.to_query_string(), "$expand=~($levels=1)");
767    }
768
769    #[test]
770    fn test_expand_property() {
771        let query = ExpandQuery::property("Thermal");
772        assert_eq!(query.to_query_string(), "$expand=Thermal($levels=1)");
773    }
774
775    #[test]
776    fn test_expand_properties() {
777        let query = ExpandQuery::properties(&["Thermal", "Power"]);
778        assert_eq!(query.to_query_string(), "$expand=Thermal,Power($levels=1)");
779    }
780
781    #[test]
782    fn test_expand_with_levels() {
783        let query = ExpandQuery::all().levels(3);
784        assert_eq!(query.to_query_string(), "$expand=*($levels=3)");
785    }
786
787    #[test]
788    fn test_simple_eq() {
789        let filter = FilterQuery::eq(&"Count", 2);
790        assert_eq!(filter.to_query_string(), "$filter=Count eq 2");
791    }
792
793    #[test]
794    fn test_string_literal() {
795        let filter = FilterQuery::eq(&"SystemType", "Physical");
796        assert_eq!(filter.to_query_string(), "$filter=SystemType eq 'Physical'");
797    }
798
799    #[test]
800    fn test_and_operator() {
801        let filter = FilterQuery::eq(&"Count", 2)
802            .and()
803            .eq_then(&"Type", "Physical");
804        assert_eq!(
805            filter.to_query_string(),
806            "$filter=Count eq 2 and Type eq 'Physical'"
807        );
808    }
809
810    #[test]
811    fn test_or_operator() {
812        let filter = FilterQuery::eq(&"Count", 2).or().eq_then(&"Count", 4);
813        assert_eq!(filter.to_query_string(), "$filter=Count eq 2 or Count eq 4");
814    }
815
816    #[test]
817    fn test_not_operator() {
818        let filter = FilterQuery::eq(&"Count", 2).not();
819        assert_eq!(filter.to_query_string(), "$filter=not Count eq 2");
820    }
821
822    #[test]
823    fn test_grouping() {
824        let filter = FilterQuery::eq(&"State", "Enabled")
825            .and()
826            .eq_then(&"Health", "OK")
827            .group()
828            .or()
829            .eq_then(&"SystemType", "Physical");
830        assert_eq!(
831            filter.to_query_string(),
832            "$filter=(State eq 'Enabled' and Health eq 'OK') or SystemType eq 'Physical'"
833        );
834    }
835
836    #[test]
837    fn test_all_comparison_operators() {
838        assert_eq!(FilterQuery::ne(&"A", 1).to_query_string(), "$filter=A ne 1");
839        assert_eq!(FilterQuery::gt(&"B", 2).to_query_string(), "$filter=B gt 2");
840        assert_eq!(FilterQuery::ge(&"C", 3).to_query_string(), "$filter=C ge 3");
841        assert_eq!(FilterQuery::lt(&"D", 4).to_query_string(), "$filter=D lt 4");
842        assert_eq!(FilterQuery::le(&"E", 5).to_query_string(), "$filter=E le 5");
843    }
844
845    #[test]
846    fn test_boolean_literal() {
847        let filter = FilterQuery::eq(&"Enabled", true);
848        assert_eq!(filter.to_query_string(), "$filter=Enabled eq true");
849    }
850
851    #[test]
852    fn test_float_literal() {
853        let filter = FilterQuery::gt(&"Temperature", 98.6);
854        assert_eq!(filter.to_query_string(), "$filter=Temperature gt 98.6");
855    }
856
857    #[test]
858    fn test_string_escaping() {
859        let filter = FilterQuery::eq(&"Name", "O'Brien");
860        assert_eq!(filter.to_query_string(), "$filter=Name eq 'O''Brien'");
861    }
862
863    #[test]
864    fn test_complex_filter() {
865        let filter = FilterQuery::eq(&"ProcessorSummary/Count", 2)
866            .and()
867            .gt_then(&"MemorySummary/TotalSystemMemoryGiB", 64);
868        assert_eq!(
869            filter.to_query_string(),
870            "$filter=ProcessorSummary/Count eq 2 and MemorySummary/TotalSystemMemoryGiB gt 64"
871        );
872    }
873}