Skip to main content

nv_redfish_core/
nav_property.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//! Navigation property wrapper for generated types
17//!
18//! Represents Redfish/OData navigation properties which may appear either as
19//! a reference (only `@odata.id`) or as an expanded object. Generated code wraps
20//! navigation properties in [`NavProperty<T>`], allowing code to work uniformly
21//! with both forms and resolve references on demand.
22//!
23//! - Reference form: `{ "@odata.id": "/redfish/v1/Chassis/1/Thermal" }`
24//! - Expanded form: full object payload for `T` (includes `@odata.id` and fields)
25//!
26//! Key points
27//! - [`NavProperty<T>::id`] is always available (delegates to inner entity for expanded form).
28//! - [`NavProperty<T>::get`] returns `Arc<T>`; if already expanded, it clones the `Arc` without I/O.
29//! - [`EntityTypeRef::etag`] is `None` for reference form.
30//!
31//! References:
32//! - DMTF Redfish Specification DSP0266 — `https://www.dmtf.org/standards/redfish`
33//! - OASIS OData 4.01 — navigation properties in CSDL
34//!
35
36use crate::Bmc;
37use crate::Creatable;
38use crate::Deletable;
39use crate::EntityTypeRef;
40use crate::Expandable;
41use crate::FilterQuery;
42use crate::ODataETag;
43use crate::ODataId;
44use crate::Updatable;
45use serde::de;
46use serde::de::Deserializer;
47use serde::Deserialize;
48use serde::Serialize;
49use std::sync::Arc;
50
51/// Reference variant of the navigation property (only `@odata.id`
52/// property is specified).
53#[derive(Serialize, Deserialize, Debug, Clone)]
54#[serde(deny_unknown_fields)]
55pub struct Reference {
56    #[serde(rename = "@odata.id")]
57    odata_id: ODataId,
58}
59
60impl<T: EntityTypeRef> From<&NavProperty<T>> for Reference {
61    fn from(v: &NavProperty<T>) -> Self {
62        Self {
63            odata_id: v.id().clone(),
64        }
65    }
66}
67
68impl From<&Self> for Reference {
69    fn from(v: &Self) -> Self {
70        Self {
71            odata_id: v.odata_id.clone(),
72        }
73    }
74}
75
76impl From<&ReferenceLeaf> for Reference {
77    fn from(v: &ReferenceLeaf) -> Self {
78        Self {
79            odata_id: v.odata_id.clone(),
80        }
81    }
82}
83
84/// `ReferenceLeaf` is special type that is used for navigation
85/// properties that if corresponding `EntityType` was not compiled to
86/// the tree.
87#[derive(Serialize, Deserialize, Debug, Clone)]
88pub struct ReferenceLeaf {
89    /// `OData` identifier for of the property.
90    #[serde(rename = "@odata.id")]
91    pub odata_id: ODataId,
92}
93
94/// Container struct for the expanded property variant.
95#[derive(Debug)]
96pub struct Expanded<T>(Arc<T>);
97
98/// Deserializer that wraps the expanded property value into an `Arc`.
99impl<'de, T> Deserialize<'de> for Expanded<T>
100where
101    T: Deserialize<'de>,
102{
103    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
104    where
105        D: Deserializer<'de>,
106    {
107        T::deserialize(deserializer).map(Arc::new).map(Expanded)
108    }
109}
110
111/// Navigation property variants. All navigation properties in
112/// generated code are wrapped with this type.
113#[derive(Debug)]
114pub enum NavProperty<T: EntityTypeRef> {
115    /// Expanded property variant (content included in the
116    /// response).
117    Expanded(Expanded<T>),
118    /// Reference variant (only `@odata.id` is included in the
119    /// response).
120    Reference(Reference),
121}
122
123impl<'de, T> Deserialize<'de> for NavProperty<T>
124where
125    T: EntityTypeRef + for<'a> Deserialize<'a>,
126{
127    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
128    where
129        D: Deserializer<'de>,
130    {
131        let value = serde_json::Value::deserialize(deserializer)?;
132        let is_reference = value
133            .as_object()
134            .is_some_and(|obj| obj.len() == 1 && obj.contains_key("@odata.id"));
135
136        if is_reference {
137            let reference = serde_json::from_value::<Reference>(value)
138                .map_err(|err| de::Error::custom(err.to_string()))?;
139            Ok(Self::Reference(reference))
140        } else {
141            // Non-reference payloads are always parsed as expanded `T`.
142            let expanded = serde_json::from_value::<T>(value)
143                .map_err(|err| de::Error::custom(err.to_string()))?;
144            Ok(Self::Expanded(Expanded(Arc::new(expanded))))
145        }
146    }
147}
148
149impl<T: EntityTypeRef> EntityTypeRef for NavProperty<T> {
150    fn odata_id(&self) -> &ODataId {
151        match self {
152            Self::Expanded(v) => v.0.odata_id(),
153            Self::Reference(r) => &r.odata_id,
154        }
155    }
156
157    fn etag(&self) -> Option<&ODataETag> {
158        match self {
159            Self::Expanded(v) => v.0.etag(),
160            Self::Reference(_) => None,
161        }
162    }
163}
164
165impl<C, R, T: Creatable<C, R>> Creatable<C, R> for NavProperty<T>
166where
167    C: Sync + Send + Sized + Serialize,
168    R: Sync + Send + Sized + for<'de> Deserialize<'de>,
169{
170}
171impl<U, T: Updatable<U>> Updatable<U> for NavProperty<T> where U: Sync + Send + Sized + Serialize {}
172impl<T: Deletable> Deletable for NavProperty<T> {}
173impl<T: Expandable> Expandable for NavProperty<T> {}
174
175impl<T: EntityTypeRef> NavProperty<T> {
176    /// Create a navigation property with a reference using the `OData`
177    /// identifier.
178    #[must_use]
179    pub const fn new_reference(odata_id: ODataId) -> Self {
180        Self::Reference(Reference { odata_id })
181    }
182
183    /// Convert property to reference regardless expanded it or not.
184    #[must_use]
185    pub fn to_reference(self) -> Self {
186        match self {
187            Self::Reference(_) => self,
188            Self::Expanded(_) => Self::new_reference(self.id().clone()),
189        }
190    }
191
192    /// Downcast to descendant type `D`.
193    #[must_use]
194    pub fn downcast<D: EntityTypeRef>(&self) -> NavProperty<D> {
195        NavProperty::<D>::new_reference(self.id().clone())
196    }
197}
198
199impl<T: EntityTypeRef> NavProperty<T> {
200    /// Extract the identifier from a navigation property.
201    #[must_use]
202    pub fn id(&self) -> &ODataId {
203        match self {
204            Self::Reference(v) => &v.odata_id,
205            Self::Expanded(v) => v.0.odata_id(),
206        }
207    }
208}
209
210impl<T: EntityTypeRef + Sized + for<'a> Deserialize<'a> + 'static + Send + Sync> NavProperty<T> {
211    /// Get the property value.
212    ///
213    /// # Errors
214    ///
215    /// If the navigation property is already expanded then no error is returned.
216    ///
217    /// If the navigation is a reference then a BMC error may be returned if
218    /// retrieval of the entity fails.
219    pub async fn get<B: Bmc>(&self, bmc: &B) -> Result<Arc<T>, B::Error> {
220        match self {
221            Self::Expanded(v) => Ok(v.0.clone()),
222            Self::Reference(_) => bmc.get::<T>(self.id()).await,
223        }
224    }
225
226    /// Filter the property value using the provided query.
227    ///
228    /// # Errors
229    ///
230    /// Returns a BMC error if filtering the entity fails.
231    #[allow(missing_docs)]
232    pub async fn filter<B: Bmc>(&self, bmc: &B, query: FilterQuery) -> Result<Arc<T>, B::Error> {
233        bmc.filter::<T>(self.id(), query).await
234    }
235}
236
237#[cfg(test)]
238mod tests {
239    use super::NavProperty;
240    use crate::EntityTypeRef;
241    use crate::ODataETag;
242    use crate::ODataId;
243    use serde::Deserialize;
244
245    #[derive(Debug, Deserialize)]
246    struct DummyEntity {
247        #[serde(rename = "@odata.id")]
248        odata_id: ODataId,
249        #[serde(rename = "Name")]
250        name: String,
251    }
252
253    impl EntityTypeRef for DummyEntity {
254        fn odata_id(&self) -> &ODataId {
255            &self.odata_id
256        }
257
258        fn etag(&self) -> Option<&ODataETag> {
259            None
260        }
261    }
262
263    #[derive(Debug, Deserialize)]
264    struct DefaultIdEntity {
265        #[serde(rename = "@odata.id", default = "default_id")]
266        odata_id: ODataId,
267        #[serde(rename = "Name")]
268        name: String,
269    }
270
271    impl EntityTypeRef for DefaultIdEntity {
272        fn odata_id(&self) -> &ODataId {
273            &self.odata_id
274        }
275
276        fn etag(&self) -> Option<&ODataETag> {
277            None
278        }
279    }
280
281    fn default_id() -> ODataId {
282        "/default/id".to_string().into()
283    }
284
285    #[allow(dead_code)]
286    #[derive(Debug, Deserialize)]
287    struct StrictNameEntity {
288        #[serde(rename = "@odata.id")]
289        odata_id: ODataId,
290        #[serde(rename = "Name")]
291        name: u64,
292    }
293
294    impl EntityTypeRef for StrictNameEntity {
295        fn odata_id(&self) -> &ODataId {
296            &self.odata_id
297        }
298
299        fn etag(&self) -> Option<&ODataETag> {
300            None
301        }
302    }
303
304    #[test]
305    fn nav_property_reference_for_odata_id_only_object() {
306        let parsed: NavProperty<DummyEntity> =
307            serde_json::from_str(r#"{ "@odata.id": "/redfish/v1/Systems/System_1" }"#).unwrap();
308
309        match parsed {
310            NavProperty::Reference(reference) => {
311                assert_eq!(
312                    reference.odata_id.to_string(),
313                    "/redfish/v1/Systems/System_1"
314                );
315            }
316            NavProperty::Expanded(_) => panic!("expected reference variant"),
317        }
318    }
319
320    #[test]
321    fn nav_property_expanded_for_object_with_extra_fields() {
322        let parsed: NavProperty<DummyEntity> = serde_json::from_str(
323            r#"{
324                "@odata.id": "/redfish/v1/Systems/System_1",
325                "Name": "System_1"
326            }"#,
327        )
328        .unwrap();
329
330        match parsed {
331            NavProperty::Expanded(expanded) => {
332                assert_eq!(
333                    expanded.0.odata_id.to_string(),
334                    "/redfish/v1/Systems/System_1"
335                );
336                assert_eq!(expanded.0.name, "System_1");
337            }
338            NavProperty::Reference(_) => panic!("expected expanded variant"),
339        }
340    }
341
342    #[test]
343    fn nav_property_object_without_odata_id_uses_expanded_path() {
344        let parsed: NavProperty<DefaultIdEntity> =
345            serde_json::from_str(r#"{ "Name": "NoIdObject" }"#).unwrap();
346
347        match parsed {
348            NavProperty::Expanded(expanded) => {
349                assert_eq!(expanded.0.odata_id.to_string(), "/default/id");
350                assert_eq!(expanded.0.name, "NoIdObject");
351            }
352            NavProperty::Reference(_) => panic!("expected expanded variant"),
353        }
354    }
355
356    #[test]
357    fn nav_property_parse_error_for_non_reference_comes_from_t() {
358        let err = serde_json::from_str::<NavProperty<StrictNameEntity>>(
359            r#"{
360                "@odata.id": "/redfish/v1/Systems/System_1",
361                "Name": "not-a-number"
362            }"#,
363        )
364        .unwrap_err()
365        .to_string();
366
367        assert!(
368            err.contains("invalid type: string") && err.contains("u64"),
369            "unexpected error: {}",
370            err
371        );
372    }
373}