Skip to main content

nv_redfish_core/
lib.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//! Core Redfish foundation library used by code generated from the CSDL compiler.
17//!
18//! Purpose
19//! - Serve as a dependency for autogenerated Redfish types produced by the CSDL compiler.
20//! - Provide semantic-unaware primitives that generated code relies on.
21//! - Avoid any knowledge of specific Redfish services, schemas, or OEM semantics.
22//!
23//! Scope (building blocks only)
24//! - Identity and metadata: [`ODataId`], [`ODataETag`]
25//! - EDM value wrappers: [`EdmDateTimeOffset`], [`EdmDuration`]
26//! - Navigation properties: [`NavProperty<T>`]
27//! - Generic operation traits: [`Creatable`], [`Updatable`], [`Deletable`]
28//! - Entity contracts: [`EntityTypeRef`], [`Expandable`]
29//! - Action envelope: [`Action<T, R>`]
30//! - Client abstraction: [`Bmc`] (transport-agnostic interface used by generated code)
31//!
32//! Non-goals
33//! - No service- or schema-specific models are defined here.
34//! - No business logic or policy decisions are embedded here.
35//! - No transport specification
36//!
37//! How generated code uses these primitives
38//! - Each generated entity struct implements [`EntityTypeRef`].
39//! - Navigation properties in generated code are wrapped in [`NavProperty<T>`].
40//! - Generated actions are represented as [`Action<T, R>`].
41//! - If the schema allows it, generated types implement [`Creatable`], [`Updatable`], and/or
42//!   [`Deletable`] and route operations through a user-provided [`Bmc`] implementation.
43
44#![deny(
45    clippy::all,
46    clippy::pedantic,
47    clippy::nursery,
48    clippy::suspicious,
49    clippy::complexity,
50    clippy::perf
51)]
52#![deny(
53    clippy::absolute_paths,
54    clippy::todo,
55    clippy::unimplemented,
56    clippy::tests_outside_test_module,
57    clippy::panic,
58    clippy::unwrap_used,
59    clippy::unwrap_in_result,
60    clippy::unused_trait_names,
61    clippy::print_stdout,
62    clippy::print_stderr
63)]
64#![deny(missing_docs)]
65
66/// Action-related types.
67pub mod action;
68/// BMC trait and credentials.
69pub mod bmc;
70/// Custom deserialization helpers.
71pub mod deserialize;
72/// Dynamic properties support.
73pub mod dynamic_properties;
74/// `Edm.DateTimeOffset` type.
75pub mod edm_date_time_offset;
76/// `Edm.Duration` type.
77pub mod edm_duration;
78/// `Edm.PrimitiveType` type.
79pub mod edm_primitive_type;
80/// Navigation property wrapper.
81pub mod nav_property;
82/// Type for `@odata.id` identifier.
83pub mod odata;
84/// Support of redfish queries
85pub mod query;
86/// Upload data types.
87pub mod upload;
88
89use crate::query::ExpandQuery;
90use futures_core::TryStream;
91use serde::{Deserialize, Serialize};
92use std::fmt;
93use std::pin::Pin;
94use std::time::Duration;
95use std::{future::Future, sync::Arc};
96
97#[doc(inline)]
98pub use action::Action;
99#[doc(inline)]
100pub use action::ActionError;
101#[doc(inline)]
102pub use bmc::Bmc;
103#[doc(inline)]
104pub use deserialize::de_optional_nullable;
105#[doc(inline)]
106pub use deserialize::de_required_nullable;
107#[doc(inline)]
108pub use dynamic_properties::DynamicProperties;
109#[doc(inline)]
110pub use edm_date_time_offset::EdmDateTimeOffset;
111#[doc(inline)]
112pub use edm_duration::EdmDuration;
113#[doc(inline)]
114pub use edm_primitive_type::EdmPrimitiveType;
115#[doc(inline)]
116pub use nav_property::NavProperty;
117#[doc(inline)]
118pub use nav_property::Reference;
119#[doc(inline)]
120pub use nav_property::ReferenceLeaf;
121#[doc(inline)]
122pub use odata::ODataETag;
123#[doc(inline)]
124pub use odata::ODataId;
125#[doc(inline)]
126pub use query::FilterQuery;
127#[doc(inline)]
128pub use query::ToFilterLiteral;
129#[doc(inline)]
130pub use serde_json::Value as AdditionalProperties;
131#[doc(inline)]
132pub use upload::DataStream;
133#[cfg(feature = "update-service-deprecated")]
134#[doc(inline)]
135pub use upload::HttpPushUriUpdateRequest;
136#[doc(inline)]
137pub use upload::MultipartUpdateRequest;
138#[doc(inline)]
139pub use upload::OemMultipartPart;
140#[doc(inline)]
141pub use upload::OemMultipartPartNameError;
142#[doc(inline)]
143pub use upload::OemMultipartPartReader;
144#[doc(inline)]
145pub use upload::UploadReader;
146#[cfg(feature = "update-service-deprecated")]
147#[doc(inline)]
148pub use upload::UploadStream;
149#[doc(inline)]
150pub use uuid::Uuid as EdmGuid;
151
152/// Entity type reference trait implemented by the CSDL compiler
153/// for all generated entity types and for all [`NavProperty<T>`] where
154/// `T` is a struct for an entity type.
155pub trait EntityTypeRef: Send + Sync + Sized {
156    /// Value of `@odata.id` field of the Entity.
157    fn odata_id(&self) -> &ODataId;
158
159    /// Value of `@odata.etag` field of the Entity.
160    fn etag(&self) -> Option<&ODataETag>;
161
162    /// Refresh the entity by fetching it again from the BMC.
163    fn refresh<B: Bmc>(&self, bmc: &B) -> impl Future<Output = Result<Arc<Self>, B::Error>> + Send
164    where
165        Self: for<'de> Deserialize<'de> + 'static,
166    {
167        bmc.get::<Self>(self.odata_id())
168    }
169}
170
171/// Defines entity types that support `$expand` via query parameters.
172pub trait Expandable: EntityTypeRef + for<'de> Deserialize<'de> + 'static {
173    /// Expand the entity according to the provided query.
174    fn expand<B: Bmc>(
175        &self,
176        bmc: &B,
177        query: ExpandQuery,
178    ) -> impl Future<Output = Result<Arc<Self>, B::Error>> + Send {
179        bmc.expand::<Self>(self.odata_id(), query)
180    }
181}
182
183/// Boxed fallible stream used by BMC streaming APIs.
184pub type BoxTryStream<T, E> =
185    Pin<Box<dyn TryStream<Ok = T, Error = E, Item = Result<T, E>> + Send>>;
186
187/// Location of an asynchronous task monitor.
188///
189/// Wraps the `Location` returned for an operation that is completing
190/// asynchronously.
191#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
192#[repr(transparent)]
193pub struct AsyncTaskLocation(
194    /// `OData` URI returned in the async response `Location` header.
195    pub ODataId,
196);
197
198impl From<ODataId> for AsyncTaskLocation {
199    fn from(value: ODataId) -> Self {
200        Self(value)
201    }
202}
203
204/// Outcome of a mutating Redfish operation that can complete asynchronously.
205#[derive(Debug)]
206pub struct AsyncTask {
207    /// Location to use for polling completion.
208    pub location: AsyncTaskLocation,
209
210    /// Recommended duration to wait before polling again.
211    pub retry_after: Option<Duration>,
212}
213
214/// Outcome of a mutating Redfish operation.
215#[must_use = "mutating Redfish responses may contain an asynchronous task handle"]
216#[derive(Debug)]
217pub enum ModificationResponse<T> {
218    /// Request completed synchronously.
219    Entity(T),
220
221    /// Request is completing asynchronously with the provided task location.
222    Task(AsyncTask),
223
224    /// Request completed successfully with no response body.
225    Empty,
226}
227
228impl<T> ModificationResponse<T> {
229    /// Maps an entity outcome while preserving task and empty outcomes.
230    pub fn map_entity<U, F>(self, f: F) -> ModificationResponse<U>
231    where
232        F: FnOnce(T) -> U,
233    {
234        match self {
235            Self::Entity(entity) => ModificationResponse::Entity(f(entity)),
236            Self::Task(task) => ModificationResponse::Task(task),
237            Self::Empty => ModificationResponse::Empty,
238        }
239    }
240
241    /// Maps an entity outcome with a fallible function while preserving task
242    /// and empty outcomes.
243    ///
244    /// # Errors
245    ///
246    /// Returns the error produced by `f` when this response contains an entity.
247    pub fn try_map_entity<U, E, F>(self, f: F) -> Result<ModificationResponse<U>, E>
248    where
249        F: FnOnce(T) -> Result<U, E>,
250    {
251        match self {
252            Self::Entity(entity) => f(entity).map(ModificationResponse::Entity),
253            Self::Task(task) => Ok(ModificationResponse::Task(task)),
254            Self::Empty => Ok(ModificationResponse::Empty),
255        }
256    }
257
258    /// Asynchronously maps an entity outcome with a fallible function while
259    /// preserving task and empty outcomes.
260    ///
261    /// # Errors
262    ///
263    /// Returns the error produced by `f` when this response contains an entity.
264    pub async fn try_map_entity_async<U, E, F, Fut>(
265        self,
266        f: F,
267    ) -> Result<ModificationResponse<U>, E>
268    where
269        F: FnOnce(T) -> Fut,
270        Fut: Future<Output = Result<U, E>>,
271    {
272        match self {
273            Self::Entity(entity) => f(entity).await.map(ModificationResponse::Entity),
274            Self::Task(task) => Ok(ModificationResponse::Task(task)),
275            Self::Empty => Ok(ModificationResponse::Empty),
276        }
277    }
278}
279
280/// Redfish session creation returns the session resource in the response body,
281/// the authentication token in the `X-Auth-Token` header, and the session URI in
282/// the `Location` header.
283pub struct SessionCreateResponse<T> {
284    /// Created session entity.
285    pub entity: T,
286    /// Authentication token from `X-Auth-Token`.
287    pub auth_token: String,
288    /// Session resource URI from `Location`.
289    pub location: ODataId,
290}
291
292impl<T: fmt::Debug> fmt::Debug for SessionCreateResponse<T> {
293    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
294        f.debug_struct("SessionCreateResponse")
295            .field("entity", &self.entity)
296            .field("auth_token", &"[REDACTED]")
297            .field("location", &self.location)
298            .finish()
299    }
300}
301
302/// This trait is assigned to the collections that are marked as
303/// creatable in the CSDL specification.
304pub trait Creatable<V: Send + Sync + Serialize, R: Send + Sync + for<'de> Deserialize<'de>>:
305    EntityTypeRef
306{
307    /// Create an entity using `create` as payload.
308    fn create<B: Bmc>(
309        &self,
310        bmc: &B,
311        create: &V,
312    ) -> impl Future<Output = Result<ModificationResponse<R>, B::Error>> + Send {
313        bmc.create::<V, R>(self.odata_id(), create)
314    }
315}
316
317/// This trait is assigned to entity types that are marked as
318/// updatable in the CSDL specification.
319pub trait Updatable<V: Sync + Send + Serialize>: EntityTypeRef + for<'de> Deserialize<'de> {
320    /// Update an entity using `update` as payload.
321    fn update<B: Bmc>(
322        &self,
323        bmc: &B,
324        update: &V,
325    ) -> impl Future<Output = Result<ModificationResponse<Self>, B::Error>> + Send {
326        bmc.update::<V, Self>(self.odata_id(), self.etag(), update)
327    }
328}
329
330/// This trait is assigned to entity types that are marked as
331/// deletable in the CSDL specification.
332pub trait Deletable: EntityTypeRef + for<'de> Deserialize<'de> {
333    /// Delete current entity.
334    fn delete<B: Bmc>(
335        &self,
336        bmc: &B,
337    ) -> impl Future<Output = Result<ModificationResponse<Self>, B::Error>> + Send {
338        bmc.delete::<Self>(self.odata_id())
339    }
340}
341
342/// This trait is assigned to updatable entity types to support
343/// @Redfish.Settings workflow.
344pub trait RedfishSettings<E: EntityTypeRef>: Sized {
345    /// Reference to the enity type object.
346    fn settings_object(&self) -> Option<NavProperty<E>>;
347}
348
349/// Trait for converting enum variants to `snake_case` strings
350pub trait ToSnakeCase {
351    /// Convert this enum variant to a `snake_case` string
352    fn to_snake_case(&self) -> &'static str;
353}
354
355/// Trait for types that can be used as filter properties in `OData` queries
356pub trait FilterProperty {
357    /// Returns the `OData` property path for this property
358    fn property_path(&self) -> &str;
359}
360
361#[cfg(test)]
362mod tests {
363    use super::*;
364
365    fn assert_entity(
366        response: ModificationResponse<u32>,
367        expected: u32,
368    ) -> Result<(), &'static str> {
369        let ModificationResponse::Entity(value) = response else {
370            return Err("expected an entity response");
371        };
372
373        assert_eq!(value, expected);
374
375        Ok(())
376    }
377
378    fn assert_task<T>(response: ModificationResponse<T>) -> Result<(), &'static str> {
379        let ModificationResponse::Task(task) = response else {
380            return Err("expected a task response");
381        };
382
383        assert_eq!(
384            task.location.0.to_string(),
385            "/redfish/v1/TaskService/Tasks/1"
386        );
387
388        Ok(())
389    }
390
391    fn assert_empty<T>(response: ModificationResponse<T>) -> Result<(), &'static str> {
392        if !matches!(response, ModificationResponse::Empty) {
393            return Err("expected an empty response");
394        }
395
396        Ok(())
397    }
398
399    fn task_response() -> ModificationResponse<()> {
400        ModificationResponse::Task(AsyncTask {
401            location: ODataId::from("/redfish/v1/TaskService/Tasks/1".to_string()).into(),
402            retry_after: None,
403        })
404    }
405
406    #[test]
407    fn map_entity_maps_entity_and_preserves_task_and_empty() -> Result<(), &'static str> {
408        assert_entity(
409            ModificationResponse::Entity(21_u32).map_entity(|value| value * 2),
410            42,
411        )?;
412
413        assert_task(task_response().map_entity(|()| 42_u32))?;
414        assert_empty(ModificationResponse::<()>::Empty.map_entity(|()| 42_u32))?;
415
416        Ok(())
417    }
418
419    #[test]
420    fn try_map_entity_maps_entity_and_propagates_error() -> Result<(), &'static str> {
421        assert_entity(
422            ModificationResponse::Entity(21_u32).try_map_entity(|value| Ok(value * 2))?,
423            42,
424        )?;
425
426        let error = ModificationResponse::Entity(21_u32)
427            .try_map_entity(|_| Err::<u32, _>("mapping failed"));
428
429        assert!(matches!(error, Err("mapping failed")));
430
431        Ok(())
432    }
433
434    #[test]
435    fn try_map_entity_preserves_task_and_empty() -> Result<(), &'static str> {
436        assert_task(task_response().try_map_entity(|()| Ok::<u32, &'static str>(42))?)?;
437
438        assert_empty(
439            ModificationResponse::<()>::Empty.try_map_entity(|()| Ok::<u32, &'static str>(42))?,
440        )?;
441
442        Ok(())
443    }
444
445    #[tokio::test]
446    async fn try_map_entity_async_maps_entity_and_preserves_task_and_empty(
447    ) -> Result<(), &'static str> {
448        assert_entity(
449            ModificationResponse::Entity(21_u32)
450                .try_map_entity_async(|value| async move { Ok(value * 2) })
451                .await?,
452            42,
453        )?;
454
455        assert_task(
456            task_response()
457                .try_map_entity_async(|()| async { Ok::<u32, &'static str>(42) })
458                .await?,
459        )?;
460
461        assert_empty(
462            ModificationResponse::<()>::Empty
463                .try_map_entity_async(|()| async { Ok::<u32, &'static str>(42) })
464                .await?,
465        )?;
466
467        Ok(())
468    }
469
470    #[tokio::test]
471    async fn try_map_entity_async_propagates_mapper_error() {
472        let response = ModificationResponse::Entity(21_u32);
473
474        let mapped = response
475            .try_map_entity_async(|_| async { Err::<u32, _>("mapping failed") })
476            .await;
477
478        assert!(matches!(mapped, Err("mapping failed")));
479    }
480}