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}