1#![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
66pub mod action;
68pub mod bmc;
70pub mod deserialize;
72pub mod dynamic_properties;
74pub mod edm_date_time_offset;
76pub mod edm_duration;
78pub mod edm_primitive_type;
80pub mod nav_property;
82pub mod odata;
84pub mod query;
86pub 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
152pub trait EntityTypeRef: Send + Sync + Sized {
156 fn odata_id(&self) -> &ODataId;
158
159 fn etag(&self) -> Option<&ODataETag>;
161
162 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
171pub trait Expandable: EntityTypeRef + for<'de> Deserialize<'de> + 'static {
173 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
183pub type BoxTryStream<T, E> =
185 Pin<Box<dyn TryStream<Ok = T, Error = E, Item = Result<T, E>> + Send>>;
186
187#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
192#[repr(transparent)]
193pub struct AsyncTaskLocation(
194 pub ODataId,
196);
197
198impl From<ODataId> for AsyncTaskLocation {
199 fn from(value: ODataId) -> Self {
200 Self(value)
201 }
202}
203
204#[derive(Debug)]
206pub struct AsyncTask {
207 pub location: AsyncTaskLocation,
209
210 pub retry_after: Option<Duration>,
212}
213
214#[must_use = "mutating Redfish responses may contain an asynchronous task handle"]
216#[derive(Debug)]
217pub enum ModificationResponse<T> {
218 Entity(T),
220
221 Task(AsyncTask),
223
224 Empty,
226}
227
228impl<T> ModificationResponse<T> {
229 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 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 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
280pub struct SessionCreateResponse<T> {
284 pub entity: T,
286 pub auth_token: String,
288 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
302pub trait Creatable<V: Send + Sync + Serialize, R: Send + Sync + for<'de> Deserialize<'de>>:
305 EntityTypeRef
306{
307 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
317pub trait Updatable<V: Sync + Send + Serialize>: EntityTypeRef + for<'de> Deserialize<'de> {
320 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
330pub trait Deletable: EntityTypeRef + for<'de> Deserialize<'de> {
333 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
342pub trait RedfishSettings<E: EntityTypeRef>: Sized {
345 fn settings_object(&self) -> Option<NavProperty<E>>;
347}
348
349pub trait ToSnakeCase {
351 fn to_snake_case(&self) -> &'static str;
353}
354
355pub trait FilterProperty {
357 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}