nv_redfish_core/
nav_property.rs1use 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#[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#[derive(Serialize, Deserialize, Debug, Clone)]
88pub struct ReferenceLeaf {
89 #[serde(rename = "@odata.id")]
91 pub odata_id: ODataId,
92}
93
94#[derive(Debug)]
96pub struct Expanded<T>(Arc<T>);
97
98impl<'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#[derive(Debug)]
114pub enum NavProperty<T: EntityTypeRef> {
115 Expanded(Expanded<T>),
118 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 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 #[must_use]
179 pub const fn new_reference(odata_id: ODataId) -> Self {
180 Self::Reference(Reference { odata_id })
181 }
182
183 #[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 #[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 #[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 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 #[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}