use core::fmt::Display;
use core::fmt::Formatter;
use core::fmt::Result as FmtResult;
use serde::Deserialize;
use serde::Serialize;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[repr(transparent)]
pub struct ODataId(String);
impl ODataId {
#[must_use]
pub fn service_root() -> Self {
Self("/redfish/v1".into())
}
#[must_use]
pub fn last_segment(&self) -> Option<&str> {
let path = self.0.trim_end_matches('/');
path.rsplit_once('/')
.map(|(_, v)| v)
.or_else(|| (!path.is_empty()).then_some(path))
}
#[must_use]
pub fn is_path_prefix(&self, other: &Self) -> bool {
let prefix = self.0.trim_end_matches('/');
if prefix.is_empty() {
return self.0.starts_with('/') && other.0.starts_with('/');
}
let Some(suffix) = other.0.strip_prefix(prefix) else {
return false;
};
suffix.is_empty() || suffix.starts_with('/')
}
}
impl From<String> for ODataId {
fn from(s: String) -> Self {
Self(s)
}
}
impl Display for ODataId {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
self.0.fmt(f)
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[repr(transparent)]
pub struct ODataETag(String);
impl From<String> for ODataETag {
fn from(value: String) -> Self {
Self(value)
}
}
impl Display for ODataETag {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
self.0.fmt(f)
}
}
pub struct ODataType<'a> {
pub namespace: Vec<&'a str>,
pub type_name: &'a str,
}
impl ODataType<'_> {
#[must_use]
pub fn parse_from(v: &serde_json::Value) -> Option<ODataType<'_>> {
v.get("@odata.type")
.and_then(|v| v.as_str())
.and_then(|v| v.strip_prefix('#'))
.and_then(|v| {
let mut all = v.split('.').collect::<Vec<_>>();
all.pop().map(|type_name| ODataType {
namespace: all,
type_name,
})
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_from_returns_none_for_empty_odata_type() {
let value = serde_json::json!({ "@odata.type": "" });
let odata_type = ODataType::parse_from(&value);
assert!(odata_type.is_none());
}
#[test]
fn last_segment_returns_last_path_segment() {
let id = ODataId("/redfish/v1/Systems/1".into());
assert_eq!(id.last_segment(), Some("1"));
}
#[test]
fn last_segment_ignores_trailing_slash() {
let id = ODataId("/redfish/v1/Systems/1/".into());
assert_eq!(id.last_segment(), Some("1"));
}
#[test]
fn last_segment_handles_multiple_trailing_slashes() {
let id = ODataId("/redfish/v1/Systems/1///".into());
assert_eq!(id.last_segment(), Some("1"));
}
#[test]
fn last_segment_returns_none_for_empty_string() {
let id = ODataId("".into());
assert_eq!(id.last_segment(), None);
}
#[test]
fn last_segment_returns_none_for_root_path() {
let id = ODataId("/".into());
assert_eq!(id.last_segment(), None);
}
#[test]
fn last_segment_returns_none_for_multiple_root_slashes() {
let id = ODataId("///".into());
assert_eq!(id.last_segment(), None);
}
#[test]
fn last_segment_returns_segment_for_single_component_relative_path() {
let id = ODataId("redfish".into());
assert_eq!(id.last_segment(), Some("redfish"));
}
#[test]
fn last_segment_returns_last_segment_for_relative_path() {
let id = ODataId("redfish/v1/Systems/1".into());
assert_eq!(id.last_segment(), Some("1"));
}
#[test]
fn last_segment_handles_leading_slash_before_single_segment() {
let id = ODataId("/redfish".into());
assert_eq!(id.last_segment(), Some("redfish"));
}
#[test]
fn service_root_last_segment_is_v1() {
assert_eq!(ODataId::service_root().last_segment(), Some("v1"));
}
#[test]
fn is_path_prefix_accepts_child_path() {
let prefix = ODataId("/redfish/v1/TaskService/Tasks".into());
let id = ODataId("/redfish/v1/TaskService/Tasks/42".into());
assert!(prefix.is_path_prefix(&id));
}
#[test]
fn is_path_prefix_accepts_prefix_with_trailing_slash() {
let prefix = ODataId("/redfish/v1/TaskService/Tasks/".into());
let id = ODataId("/redfish/v1/TaskService/Tasks/42".into());
assert!(prefix.is_path_prefix(&id));
}
#[test]
fn is_path_prefix_rejects_matching_string_without_segment_boundary() {
let prefix = ODataId("/redfish/v1/TaskService/Tasks".into());
let id = ODataId("/redfish/v1/TaskService/TasksExtra/42".into());
assert!(!prefix.is_path_prefix(&id));
}
#[test]
fn is_path_prefix_accepts_exact_path_without_trailing_slash() {
let prefix = ODataId("/redfish/v1/TaskService/Tasks".into());
let id = ODataId("/redfish/v1/TaskService/Tasks".into());
assert!(prefix.is_path_prefix(&id));
}
#[test]
fn is_path_prefix_accepts_root_path() {
let prefix = ODataId("/".into());
let id = ODataId("/".into());
assert!(prefix.is_path_prefix(&id));
}
#[test]
fn is_path_prefix_accepts_root_child_path() {
let prefix = ODataId("/".into());
let id = ODataId("/redfish".into());
assert!(prefix.is_path_prefix(&id));
}
}