aimcal_ical/property/
calendar.rs

1// SPDX-FileCopyrightText: 2025-2026 Zexin Yuan <aim@yzx9.xyz>
2//
3// SPDX-License-Identifier: Apache-2.0
4
5//! Calendar Properties (RFC 5545 Section 3.7)
6//!
7//! This module contains property types for the "Calendar Properties"
8//! section of RFC 5545. All types implement `kind()` methods and validate
9//! their property kind during conversion from `ParsedProperty`:
10//!
11//! - 3.7.1: `CalendarScale` - Calendar scale (GREGORIAN)
12//! - 3.7.2: `Method` - iTIP method (PUBLISH, REQUEST, etc.)
13//! - 3.7.3: `ProductId` - Product identifier (vendor/product info)
14//! - 3.7.4: `Version` - iCalendar version (2.0)
15
16use std::convert::TryFrom;
17
18use crate::keyword::{
19    KW_CALSCALE_GREGORIAN, KW_METHOD_ADD, KW_METHOD_CANCEL, KW_METHOD_COUNTER,
20    KW_METHOD_DECLINECOUNTER, KW_METHOD_PUBLISH, KW_METHOD_REFRESH, KW_METHOD_REPLY,
21    KW_METHOD_REQUEST, KW_VERSION_2_0,
22};
23use crate::parameter::Parameter;
24use crate::property::PropertyKind;
25use crate::property::common::take_single_text;
26use crate::string_storage::{Segments, StringStorage};
27use crate::syntax::RawParameter;
28use crate::typed::{ParsedProperty, TypedError};
29use crate::value::ValueText;
30
31define_prop_value_enum! {
32    /// Calendar scale value (RFC 5545 Section 3.7.1)
33    #[derive(Default)]
34    pub enum CalendarScaleValue {
35        /// Gregorian calendar
36        #[default]
37        Gregorian => KW_CALSCALE_GREGORIAN
38    }
39}
40
41/// Calendar scale specification (RFC 5545 Section 3.7.1)
42#[derive(Debug, Clone, Default)]
43pub struct CalendarScale<S: StringStorage> {
44    /// Calendar scale value
45    pub value: CalendarScaleValue,
46    /// X-name parameters (custom experimental parameters)
47    pub x_parameters: Vec<RawParameter<S>>,
48    /// Unrecognized / Non-standard parameters (preserved for round-trip)
49    pub retained_parameters: Vec<Parameter<S>>,
50    /// Span of the property in the source
51    pub span: S::Span,
52}
53
54impl<'src> TryFrom<ParsedProperty<'src>> for CalendarScale<Segments<'src>> {
55    type Error = Vec<TypedError<'src>>;
56
57    fn try_from(prop: ParsedProperty<'src>) -> Result<Self, Self::Error> {
58        if !matches!(prop.kind, PropertyKind::CalScale) {
59            return Err(vec![TypedError::PropertyUnexpectedKind {
60                expected: PropertyKind::CalScale,
61                found: prop.kind,
62                span: prop.span,
63            }]);
64        }
65
66        let mut x_parameters = Vec::new();
67        let mut retained_parameters = Vec::new();
68
69        for param in prop.parameters {
70            match param {
71                Parameter::XName(raw) => x_parameters.push(raw),
72                p @ Parameter::Unrecognized { .. } => retained_parameters.push(p),
73                p => {
74                    // Preserve other parameters not used by this property for round-trip
75                    retained_parameters.push(p);
76                }
77            }
78        }
79
80        let value_span = prop.value.span();
81        let text = take_single_text(&PropertyKind::CalScale, prop.value)?;
82        let value = text.try_into().map_err(|text| {
83            vec![TypedError::PropertyInvalidValue {
84                property: PropertyKind::CalScale,
85                value: format!("Unsupported calendar scale: {text}"),
86                span: value_span,
87            }]
88        })?;
89
90        Ok(CalendarScale {
91            value,
92            x_parameters,
93            retained_parameters,
94            span: prop.span,
95        })
96    }
97}
98
99impl CalendarScale<Segments<'_>> {
100    /// Convert borrowed `CalendarScale` to owned `CalendarScale`
101    #[must_use]
102    pub fn to_owned(&self) -> CalendarScale<String> {
103        CalendarScale {
104            value: self.value,
105            x_parameters: self
106                .x_parameters
107                .iter()
108                .map(RawParameter::to_owned)
109                .collect(),
110            retained_parameters: self
111                .retained_parameters
112                .iter()
113                .map(Parameter::to_owned)
114                .collect(),
115            span: (),
116        }
117    }
118}
119
120define_prop_value_enum! {
121    /// Method value for iCalendar objects (RFC 5545 Section 3.7.2)
122    #[derive(Default)]
123    pub enum MethodValue {
124        /// Publish an event (most common)
125        #[default]
126        Publish         => KW_METHOD_PUBLISH,
127        /// Request an event
128        Request         => KW_METHOD_REQUEST,
129        /// Reply to an event
130        Reply           => KW_METHOD_REPLY,
131        /// Add an event
132        Add             => KW_METHOD_ADD,
133        /// Cancel an event
134        Cancel          => KW_METHOD_CANCEL,
135        /// Refresh an event
136        Refresh         => KW_METHOD_REFRESH,
137        /// Counter an event
138        Counter         => KW_METHOD_COUNTER,
139        /// Decline counter
140        DeclineCounter  => KW_METHOD_DECLINECOUNTER,
141    }
142}
143
144/// Method type for iCalendar objects (RFC 5545 Section 3.7.2)
145#[derive(Debug, Clone, Default)]
146pub struct Method<S: StringStorage> {
147    /// Method value
148    pub value: MethodValue,
149    /// X-name parameters (custom experimental parameters)
150    pub x_parameters: Vec<RawParameter<S>>,
151    /// Unrecognized / Non-standard parameters (preserved for round-trip)
152    pub retained_parameters: Vec<Parameter<S>>,
153    /// Span of the property in the source
154    pub span: S::Span,
155}
156
157impl<'src> TryFrom<ParsedProperty<'src>> for Method<Segments<'src>> {
158    type Error = Vec<TypedError<'src>>;
159
160    fn try_from(prop: ParsedProperty<'src>) -> Result<Self, Self::Error> {
161        if !matches!(prop.kind, PropertyKind::Method) {
162            return Err(vec![TypedError::PropertyUnexpectedKind {
163                expected: PropertyKind::Method,
164                found: prop.kind,
165                span: prop.span,
166            }]);
167        }
168
169        let mut x_parameters = Vec::new();
170        let mut retained_parameters = Vec::new();
171
172        for param in prop.parameters {
173            match param {
174                Parameter::XName(raw) => x_parameters.push(raw),
175                p @ Parameter::Unrecognized { .. } => retained_parameters.push(p),
176                p => {
177                    // Preserve other parameters not used by this property for round-trip
178                    retained_parameters.push(p);
179                }
180            }
181        }
182
183        let value_span = prop.value.span();
184        let text = take_single_text(&PropertyKind::Method, prop.value)?;
185        let value = text.try_into().map_err(|text| {
186            vec![TypedError::PropertyInvalidValue {
187                property: PropertyKind::Method,
188                value: format!("Unsupported method type: {text}"),
189                span: value_span,
190            }]
191        })?;
192
193        Ok(Method {
194            value,
195            x_parameters,
196            retained_parameters,
197            span: prop.span,
198        })
199    }
200}
201
202impl Method<Segments<'_>> {
203    /// Convert borrowed `Method` to owned `Method`
204    #[must_use]
205    pub fn to_owned(&self) -> Method<String> {
206        Method {
207            value: self.value,
208            x_parameters: self
209                .x_parameters
210                .iter()
211                .map(RawParameter::to_owned)
212                .collect(),
213            retained_parameters: self
214                .retained_parameters
215                .iter()
216                .map(Parameter::to_owned)
217                .collect(),
218            span: (),
219        }
220    }
221}
222
223/// Product identifier that identifies the software that created the iCalendar data (RFC 5545 Section 3.7.3)
224#[derive(Debug, Clone, Default)]
225pub struct ProductId<S: StringStorage> {
226    /// The vendor of the implementation SHOULD assure that this is a globally
227    /// unique identifier; using some technique such as an FPI value, as
228    /// defined in [ISO.9070.1991].
229    pub value: ValueText<S>,
230    /// X-name parameters (custom experimental parameters)
231    pub x_parameters: Vec<RawParameter<S>>,
232    /// Unrecognized / Non-standard parameters (preserved for round-trip)
233    pub retained_parameters: Vec<Parameter<S>>,
234    /// Span of the property in the source
235    pub span: S::Span,
236}
237
238impl<'src> TryFrom<ParsedProperty<'src>> for ProductId<Segments<'src>> {
239    type Error = Vec<TypedError<'src>>;
240
241    fn try_from(prop: ParsedProperty<'src>) -> Result<Self, Self::Error> {
242        if !matches!(prop.kind, PropertyKind::ProdId) {
243            return Err(vec![TypedError::PropertyUnexpectedKind {
244                expected: PropertyKind::ProdId,
245                found: prop.kind,
246                span: prop.span,
247            }]);
248        }
249
250        let mut x_parameters = Vec::new();
251        let mut retained_parameters = Vec::new();
252        for param in prop.parameters {
253            match param {
254                Parameter::XName(raw) => x_parameters.push(raw),
255                p @ Parameter::Unrecognized { .. } => retained_parameters.push(p),
256                p => {
257                    // Preserve other parameters not used by this property for round-trip
258                    retained_parameters.push(p);
259                }
260            }
261        }
262
263        let value = take_single_text(&PropertyKind::ProdId, prop.value)?;
264        Ok(ProductId {
265            value,
266            x_parameters,
267            retained_parameters,
268            span: prop.span,
269        })
270    }
271}
272
273impl ProductId<Segments<'_>> {
274    /// Convert borrowed `ProductId` to owned `ProductId`
275    #[must_use]
276    pub fn to_owned(&self) -> ProductId<String> {
277        ProductId {
278            value: self.value.to_owned(),
279            x_parameters: self
280                .x_parameters
281                .iter()
282                .map(RawParameter::to_owned)
283                .collect(),
284            retained_parameters: self
285                .retained_parameters
286                .iter()
287                .map(Parameter::to_owned)
288                .collect(),
289            span: (),
290        }
291    }
292}
293
294define_prop_value_enum! {
295    /// iCalendar version value (RFC 5545 Section 3.7.4)
296    #[derive(Default)]
297    pub enum VersionValue {
298        /// Version 2.0 (most common)
299        #[default]
300        V2_0 => KW_VERSION_2_0,
301    }
302}
303
304/// iCalendar version specification (RFC 5545 Section 3.7.4)
305#[derive(Debug, Clone, Default)]
306pub struct Version<S: StringStorage> {
307    /// Version value
308    pub value: VersionValue,
309    /// X-name parameters (custom experimental parameters)
310    pub x_parameters: Vec<RawParameter<S>>,
311    /// Unrecognized / Non-standard parameters (preserved for round-trip)
312    pub retained_parameters: Vec<Parameter<S>>,
313    /// Span of the property in the source
314    pub span: S::Span,
315}
316
317impl<'src> TryFrom<ParsedProperty<'src>> for Version<Segments<'src>> {
318    type Error = Vec<TypedError<'src>>;
319
320    fn try_from(prop: ParsedProperty<'src>) -> Result<Self, Self::Error> {
321        if !matches!(prop.kind, PropertyKind::Version) {
322            return Err(vec![TypedError::PropertyUnexpectedKind {
323                expected: PropertyKind::Version,
324                found: prop.kind,
325                span: prop.span,
326            }]);
327        }
328
329        let mut x_parameters = Vec::new();
330        let mut retained_parameters = Vec::new();
331
332        for param in prop.parameters {
333            match param {
334                Parameter::XName(raw) => x_parameters.push(raw),
335                p @ Parameter::Unrecognized { .. } => retained_parameters.push(p),
336                p => {
337                    // Preserve other parameters not used by this property for round-trip
338                    retained_parameters.push(p);
339                }
340            }
341        }
342
343        let value_span = prop.value.span();
344        let text = take_single_text(&PropertyKind::Version, prop.value)?;
345        let value = text.try_into().map_err(|text| {
346            vec![TypedError::PropertyInvalidValue {
347                property: PropertyKind::Version,
348                value: format!("Unsupported iCalendar version: {text}"),
349                span: value_span,
350            }]
351        })?;
352
353        Ok(Version {
354            value,
355            x_parameters,
356            retained_parameters,
357            span: prop.span,
358        })
359    }
360}
361
362impl Version<Segments<'_>> {
363    /// Convert borrowed `Version` to owned `Version`
364    #[must_use]
365    pub fn to_owned(&self) -> Version<String> {
366        Version {
367            value: self.value,
368            x_parameters: self
369                .x_parameters
370                .iter()
371                .map(RawParameter::to_owned)
372                .collect(),
373            retained_parameters: self
374                .retained_parameters
375                .iter()
376                .map(Parameter::to_owned)
377                .collect(),
378            span: (),
379        }
380    }
381}