use crate::Bmc;
use crate::Creatable;
use crate::Deletable;
use crate::EntityTypeRef;
use crate::Expandable;
use crate::FilterQuery;
use crate::ODataETag;
use crate::ODataId;
use crate::Updatable;
use serde::de;
use serde::de::Deserializer;
use serde::Deserialize;
use serde::Serialize;
use std::sync::Arc;
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(deny_unknown_fields)]
pub struct Reference {
#[serde(rename = "@odata.id")]
odata_id: ODataId,
}
impl<T: EntityTypeRef> From<&NavProperty<T>> for Reference {
fn from(v: &NavProperty<T>) -> Self {
Self {
odata_id: v.id().clone(),
}
}
}
impl From<&Self> for Reference {
fn from(v: &Self) -> Self {
Self {
odata_id: v.odata_id.clone(),
}
}
}
impl From<&ReferenceLeaf> for Reference {
fn from(v: &ReferenceLeaf) -> Self {
Self {
odata_id: v.odata_id.clone(),
}
}
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct ReferenceLeaf {
#[serde(rename = "@odata.id")]
pub odata_id: ODataId,
}
#[derive(Debug)]
pub struct Expanded<T>(Arc<T>);
impl<'de, T> Deserialize<'de> for Expanded<T>
where
T: Deserialize<'de>,
{
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
T::deserialize(deserializer).map(Arc::new).map(Expanded)
}
}
#[derive(Debug)]
pub enum NavProperty<T: EntityTypeRef> {
Expanded(Expanded<T>),
Reference(Reference),
}
impl<'de, T> Deserialize<'de> for NavProperty<T>
where
T: EntityTypeRef + for<'dt> Deserialize<'dt>,
{
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let value = serde_json::Value::deserialize(deserializer)?;
let is_reference = value
.as_object()
.is_some_and(|obj| obj.len() == 1 && obj.contains_key("@odata.id"));
if is_reference {
let reference = serde_json::from_value::<Reference>(value)
.map_err(|err| de::Error::custom(err.to_string()))?;
Ok(Self::Reference(reference))
} else {
let expanded = serde_json::from_value::<T>(value)
.map_err(|err| de::Error::custom(err.to_string()))?;
Ok(Self::Expanded(Expanded(Arc::new(expanded))))
}
}
}
impl<T: EntityTypeRef> EntityTypeRef for NavProperty<T> {
fn odata_id(&self) -> &ODataId {
match self {
Self::Expanded(v) => v.0.odata_id(),
Self::Reference(r) => &r.odata_id,
}
}
fn etag(&self) -> Option<&ODataETag> {
match self {
Self::Expanded(v) => v.0.etag(),
Self::Reference(_) => None,
}
}
}
impl<C, R, T: Creatable<C, R>> Creatable<C, R> for NavProperty<T>
where
C: Send + Sync + Serialize,
R: Send + Sync + for<'de> Deserialize<'de>,
{
}
impl<U, T: Updatable<U>> Updatable<U> for NavProperty<T> where U: Sync + Send + Sized + Serialize {}
impl<T: Deletable> Deletable for NavProperty<T> {}
impl<T: Expandable> Expandable for NavProperty<T> {}
impl<T: EntityTypeRef> NavProperty<T> {
#[must_use]
pub const fn new_reference(odata_id: ODataId) -> Self {
Self::Reference(Reference { odata_id })
}
#[must_use]
pub fn to_reference(self) -> Self {
match self {
Self::Reference(_) => self,
Self::Expanded(_) => Self::new_reference(self.id().clone()),
}
}
#[must_use]
pub fn downcast<D: EntityTypeRef>(&self) -> NavProperty<D> {
NavProperty::<D>::new_reference(self.id().clone())
}
}
impl<T: EntityTypeRef> NavProperty<T> {
#[must_use]
pub fn id(&self) -> &ODataId {
match self {
Self::Reference(v) => &v.odata_id,
Self::Expanded(v) => v.0.odata_id(),
}
}
}
impl<T: EntityTypeRef + for<'de> Deserialize<'de> + 'static> NavProperty<T> {
pub async fn get<B: Bmc>(&self, bmc: &B) -> Result<Arc<T>, B::Error> {
match self {
Self::Expanded(v) => Ok(v.0.clone()),
Self::Reference(_) => bmc.get::<T>(self.id()).await,
}
}
#[allow(missing_docs)]
pub async fn filter<B: Bmc>(&self, bmc: &B, query: FilterQuery) -> Result<Arc<T>, B::Error> {
bmc.filter::<T>(self.id(), query).await
}
}
#[cfg(test)]
mod tests {
use super::NavProperty;
use crate::EntityTypeRef;
use crate::ODataETag;
use crate::ODataId;
use serde::Deserialize;
#[derive(Debug, Deserialize)]
struct DummyEntity {
#[serde(rename = "@odata.id")]
odata_id: ODataId,
#[serde(rename = "Name")]
name: String,
}
impl EntityTypeRef for DummyEntity {
fn odata_id(&self) -> &ODataId {
&self.odata_id
}
fn etag(&self) -> Option<&ODataETag> {
None
}
}
#[derive(Debug, Deserialize)]
struct DefaultIdEntity {
#[serde(rename = "@odata.id", default = "default_id")]
odata_id: ODataId,
#[serde(rename = "Name")]
name: String,
}
impl EntityTypeRef for DefaultIdEntity {
fn odata_id(&self) -> &ODataId {
&self.odata_id
}
fn etag(&self) -> Option<&ODataETag> {
None
}
}
fn default_id() -> ODataId {
"/default/id".to_string().into()
}
#[allow(dead_code)]
#[derive(Debug, Deserialize)]
struct StrictNameEntity {
#[serde(rename = "@odata.id")]
odata_id: ODataId,
#[serde(rename = "Name")]
name: u64,
}
impl EntityTypeRef for StrictNameEntity {
fn odata_id(&self) -> &ODataId {
&self.odata_id
}
fn etag(&self) -> Option<&ODataETag> {
None
}
}
#[test]
fn nav_property_reference_for_odata_id_only_object() {
let parsed: NavProperty<DummyEntity> =
serde_json::from_str(r#"{ "@odata.id": "/redfish/v1/Systems/System_1" }"#).unwrap();
match parsed {
NavProperty::Reference(reference) => {
assert_eq!(
reference.odata_id.to_string(),
"/redfish/v1/Systems/System_1"
);
}
NavProperty::Expanded(_) => panic!("expected reference variant"),
}
}
#[test]
fn nav_property_expanded_for_object_with_extra_fields() {
let parsed: NavProperty<DummyEntity> = serde_json::from_str(
r#"{
"@odata.id": "/redfish/v1/Systems/System_1",
"Name": "System_1"
}"#,
)
.unwrap();
match parsed {
NavProperty::Expanded(expanded) => {
assert_eq!(
expanded.0.odata_id.to_string(),
"/redfish/v1/Systems/System_1"
);
assert_eq!(expanded.0.name, "System_1");
}
NavProperty::Reference(_) => panic!("expected expanded variant"),
}
}
#[test]
fn nav_property_object_without_odata_id_uses_expanded_path() {
let parsed: NavProperty<DefaultIdEntity> =
serde_json::from_str(r#"{ "Name": "NoIdObject" }"#).unwrap();
match parsed {
NavProperty::Expanded(expanded) => {
assert_eq!(expanded.0.odata_id.to_string(), "/default/id");
assert_eq!(expanded.0.name, "NoIdObject");
}
NavProperty::Reference(_) => panic!("expected expanded variant"),
}
}
#[test]
fn nav_property_parse_error_for_non_reference_comes_from_t() {
let err = serde_json::from_str::<NavProperty<StrictNameEntity>>(
r#"{
"@odata.id": "/redfish/v1/Systems/System_1",
"Name": "not-a-number"
}"#,
)
.unwrap_err()
.to_string();
assert!(
err.contains("invalid type: string") && err.contains("u64"),
"unexpected error: {}",
err
);
}
}