Skip to main content

rfc5545_types/
value.rs

1//! Compound property value types.
2
3use calendar_types::string::Uri;
4
5/// A latitude-longitude pair of geographic coordinates (RFC 5545 §3.8.1.6).
6///
7/// Both latitude and longitude are stored as `f64`, which provides sufficient precision
8/// for the six decimal places specified by RFC 5545 (accuracy to within one meter).
9#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
10pub struct Geo {
11    pub lat: f64,
12    pub lon: f64,
13}
14
15/// An attached object (RFC 5545 §3.8.1.1).
16///
17/// Either a URI reference or inline binary data (base64-encoded on the wire).
18#[derive(Debug, Clone, PartialEq, Eq)]
19pub enum Attachment {
20    Uri(Box<Uri>),
21    Binary(Vec<u8>),
22}
23
24/// An error indicating that a string is not a valid FMTTYPE value.
25#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
26pub enum InvalidFormatTypeError {
27    #[error("expected at least one character")]
28    EmptyString,
29    #[error("missing '/' separator between type and subtype")]
30    MissingSlash,
31    #[error("empty type part before '/'")]
32    EmptyType,
33    #[error("empty subtype part after '/'")]
34    EmptySubtype,
35}
36
37/// A media type/subtype pair (RFC 5545 §3.2.8, FMTTYPE parameter).
38///
39/// Format: `type/subtype` (e.g. `text/plain`, `image/png`).
40#[derive(PartialEq, Eq, PartialOrd, Ord, Hash, dizzy::DstNewtype)]
41#[dizzy(invariant = FormatType::str_is_format_type, error = InvalidFormatTypeError)]
42#[dizzy(constructor = pub new)]
43#[dizzy(getter = pub const as_str)]
44#[dizzy(derive(Debug, CloneBoxed, IntoBoxed))]
45#[dizzy(owned = pub FormatTypeBuf(String))]
46#[dizzy(derive_owned(Debug, IntoBoxed))]
47#[repr(transparent)]
48pub struct FormatType(str);
49
50impl FormatType {
51    fn str_is_format_type(s: &str) -> Result<(), InvalidFormatTypeError> {
52        if s.is_empty() {
53            return Err(InvalidFormatTypeError::EmptyString);
54        }
55
56        let (type_part, subtype) = s
57            .split_once('/')
58            .ok_or(InvalidFormatTypeError::MissingSlash)?;
59
60        if type_part.is_empty() {
61            return Err(InvalidFormatTypeError::EmptyType);
62        }
63        if subtype.is_empty() {
64            return Err(InvalidFormatTypeError::EmptySubtype);
65        }
66
67        Ok(())
68    }
69
70    /// Returns the type part (before `/`).
71    #[inline(always)]
72    pub fn type_part(&self) -> &str {
73        self.as_str()
74            .split_once('/')
75            .expect("FormatType must contain /")
76            .0
77    }
78
79    /// Returns the subtype part (after `/`).
80    #[inline(always)]
81    pub fn subtype(&self) -> &str {
82        self.as_str()
83            .split_once('/')
84            .expect("FormatType must contain /")
85            .1
86    }
87}
88
89impl std::fmt::Display for FormatType {
90    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
91        f.write_str(self.as_str())
92    }
93}
94
95impl PartialEq for FormatTypeBuf {
96    fn eq(&self, other: &Self) -> bool {
97        **self == **other
98    }
99}
100
101impl Eq for FormatTypeBuf {}
102
103impl std::fmt::Display for FormatTypeBuf {
104    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
105        f.write_str(self.as_str())
106    }
107}
108
109/// A value of the STRUCTURED-DATA property (RFC 9073 §6.6).
110#[derive(Debug, Clone, PartialEq, Eq)]
111pub enum StructuredDataValue {
112    Text(String),
113    Binary(Vec<u8>),
114    Uri(Box<Uri>),
115}
116
117/// A value of the STYLED-DESCRIPTION property (RFC 9073 §6.5).
118#[derive(Debug, Clone, PartialEq, Eq)]
119pub enum StyledDescriptionValue {
120    Text(String),
121    Uri(Box<Uri>),
122    Iana { value_type: String, value: String },
123}