aimcal_ical/property/
common.rs

1// SPDX-FileCopyrightText: 2025-2026 Zexin Yuan <aim@yzx9.xyz>
2//
3// SPDX-License-Identifier: Apache-2.0
4
5//! Helper functions and types for property parsing.
6//!
7//! This module provides utility functions and common types for extracting
8//! and converting property values from typed properties to semantic types.
9
10use std::convert::TryFrom;
11
12use crate::parameter::{Parameter, ValueType};
13use crate::property::PropertyKind;
14use crate::string_storage::{Segments, StringStorage};
15use crate::syntax::RawParameter;
16use crate::typed::{ParsedProperty, TypedError};
17use crate::value::{Value, ValueText};
18
19/// Get the first value from a property, ensuring it has exactly one value
20pub fn take_single_value<'src>(
21    kind: &PropertyKind<Segments<'src>>,
22    value: Value<Segments<'src>>,
23) -> Result<Value<Segments<'src>>, Vec<TypedError<'src>>> {
24    if !value.len() == 1 {
25        return Err(vec![TypedError::PropertyInvalidValueCount {
26            property: kind.clone(),
27            expected: 1,
28            found: value.len(),
29            span: value.span(),
30        }]);
31    }
32
33    Ok(value)
34}
35
36/// Get a single calendar user address value from a property
37pub fn take_single_cal_address<'src>(
38    kind: &PropertyKind<Segments<'src>>,
39    value: Value<Segments<'src>>,
40) -> Result<Segments<'src>, Vec<TypedError<'src>>> {
41    const EXPECTED: &[ValueType<String>] = &[ValueType::CalendarUserAddress];
42    let value = take_single_value(kind, value)?;
43    match value {
44        Value::CalAddress { value, .. } => Ok(value),
45        v => Err(vec![TypedError::PropertyUnexpectedValue {
46            property: kind.clone(),
47            expected: EXPECTED,
48            found: v.kind().into(),
49            span: v.span(),
50        }]),
51    }
52}
53
54/// Get a single URI value from a property
55pub fn take_single_uri<'src>(
56    kind: &PropertyKind<Segments<'src>>,
57    value: Value<Segments<'src>>,
58) -> Result<Segments<'src>, Vec<TypedError<'src>>> {
59    const EXPECTED: &[ValueType<String>] = &[ValueType::Uri];
60    let value = take_single_value(kind, value)?;
61    match value {
62        Value::Uri { value, .. } => Ok(value),
63        v => Err(vec![TypedError::PropertyUnexpectedValue {
64            property: kind.clone(),
65            expected: EXPECTED,
66            found: v.kind().into(),
67            span: v.span(),
68        }]),
69    }
70}
71
72/// Get a single text value from a property
73pub fn take_single_text<'src>(
74    kind: &PropertyKind<Segments<'src>>,
75    value: Value<Segments<'src>>,
76) -> Result<ValueText<Segments<'src>>, Vec<TypedError<'src>>> {
77    const EXPECTED: &[ValueType<String>] = &[ValueType::Text];
78    let value = take_single_value(kind, value)?;
79
80    match value {
81        Value::Text { mut values, .. } if values.len() == 1 => Ok(values.pop().unwrap()),
82        Value::Text { ref values, .. } => {
83            let span = value.span();
84            Err(vec![TypedError::PropertyInvalidValueCount {
85                property: kind.clone(),
86                expected: 1,
87                found: values.len(),
88                span,
89            }])
90        }
91        v => {
92            let span = v.span();
93            Err(vec![TypedError::PropertyUnexpectedValue {
94                property: kind.clone(),
95                expected: EXPECTED,
96                found: v.kind().into(),
97                span,
98            }])
99        }
100    }
101}
102
103/// URI property with parameters
104///
105/// This type is used by properties that have a URI value type, such as:
106/// - 3.8.3.5: `TzUrl` - Time zone URL
107/// - 3.8.4.6: `Url` - Uniform Resource Locator
108#[derive(Debug, Clone)]
109pub struct UriProperty<S: StringStorage> {
110    /// The URI value
111    pub uri: S,
112    /// X-name parameters (custom experimental parameters)
113    pub x_parameters: Vec<RawParameter<S>>,
114    /// Unrecognized / Non-standard parameters (preserved for round-trip)
115    pub retained_parameters: Vec<Parameter<S>>,
116}
117
118impl<'src> TryFrom<ParsedProperty<'src>> for UriProperty<Segments<'src>> {
119    type Error = Vec<TypedError<'src>>;
120
121    fn try_from(prop: ParsedProperty<'src>) -> Result<Self, Self::Error> {
122        let mut x_parameters = Vec::new();
123        let mut retained_parameters = Vec::new();
124
125        for param in prop.parameters {
126            match param {
127                Parameter::XName(raw) => x_parameters.push(raw),
128                p @ Parameter::Unrecognized { .. } => retained_parameters.push(p),
129                p => {
130                    // Preserve other parameters not used by this property for round-trip
131                    retained_parameters.push(p);
132                }
133            }
134        }
135
136        let uri = take_single_uri(&prop.kind, prop.value)?;
137
138        Ok(UriProperty {
139            uri,
140            x_parameters,
141            retained_parameters,
142        })
143    }
144}
145
146impl UriProperty<Segments<'_>> {
147    /// Convert borrowed `UriProperty` to owned `UriProperty`
148    #[must_use]
149    pub fn to_owned(&self) -> UriProperty<String> {
150        UriProperty {
151            uri: self.uri.to_owned(),
152            x_parameters: self
153                .x_parameters
154                .iter()
155                .map(RawParameter::to_owned)
156                .collect(),
157            retained_parameters: self
158                .retained_parameters
159                .iter()
160                .map(Parameter::to_owned)
161                .collect(),
162        }
163    }
164}
165
166/// Plain text property without standard parameters
167///
168/// This is a helper type used by text properties that do NOT support any
169/// standard parameters (LANGUAGE, ALTREP, etc.):
170/// - 3.8.3.1: `TzId` - Time zone identifier
171/// - 3.8.4.7: `Uid` - Unique identifier
172///
173/// All standard parameters are preserved in `retained_parameters` for
174/// round-trip compatibility.
175#[derive(Debug, Clone)]
176pub struct TextOnly<S: StringStorage> {
177    /// The actual text content
178    pub content: ValueText<S>,
179    /// X-name parameters (custom experimental parameters)
180    pub x_parameters: Vec<RawParameter<S>>,
181    /// Unrecognized / Non-standard parameters (preserved for round-trip)
182    pub retained_parameters: Vec<Parameter<S>>,
183}
184
185impl<'src> TryFrom<ParsedProperty<'src>> for TextOnly<Segments<'src>> {
186    type Error = Vec<TypedError<'src>>;
187
188    fn try_from(prop: ParsedProperty<'src>) -> Result<Self, Self::Error> {
189        let content = take_single_text(&prop.kind, prop.value)?;
190
191        let mut x_parameters = Vec::new();
192        let mut retained_parameters = Vec::new();
193
194        for param in prop.parameters {
195            match param {
196                Parameter::XName(raw) => x_parameters.push(raw),
197                p @ Parameter::Unrecognized { .. } => retained_parameters.push(p),
198                p => {
199                    // ALL standard parameters go to retained_parameters for round-trip
200                    retained_parameters.push(p);
201                }
202            }
203        }
204
205        Ok(Self {
206            content,
207            x_parameters,
208            retained_parameters,
209        })
210    }
211}
212
213impl TextOnly<Segments<'_>> {
214    /// Convert borrowed `TextOnly` to owned `TextOnly`
215    #[must_use]
216    pub fn to_owned(&self) -> TextOnly<String> {
217        TextOnly {
218            content: self.content.to_owned(),
219            x_parameters: self
220                .x_parameters
221                .iter()
222                .map(RawParameter::to_owned)
223                .collect(),
224            retained_parameters: self
225                .retained_parameters
226                .iter()
227                .map(Parameter::to_owned)
228                .collect(),
229        }
230    }
231}
232
233/// Text with language parameter only
234///
235/// This is a helper type used by text properties that support ONLY the LANGUAGE
236/// parameter (not ALTREP):
237/// - 3.8.3.2: `TzName` - Time zone name
238/// - 3.8.8.3: `RequestStatus` - Request status
239///
240/// ALTREP and other standard parameters are preserved in `retained_parameters`
241/// for round-trip compatibility.
242#[derive(Debug, Clone)]
243pub struct TextWithLanguage<S: StringStorage> {
244    /// The actual text content
245    pub content: ValueText<S>,
246
247    /// Language code (optional)
248    pub language: Option<S>,
249
250    /// X-name parameters (custom experimental parameters)
251    pub x_parameters: Vec<RawParameter<S>>,
252
253    /// Unrecognized / Non-standard parameters (preserved for round-trip)
254    pub retained_parameters: Vec<Parameter<S>>,
255}
256
257impl<'src> TryFrom<ParsedProperty<'src>> for TextWithLanguage<Segments<'src>> {
258    type Error = Vec<TypedError<'src>>;
259
260    fn try_from(prop: ParsedProperty<'src>) -> Result<Self, Self::Error> {
261        let content = take_single_text(&prop.kind, prop.value)?;
262
263        let mut errors = Vec::new();
264        let mut language = None;
265        let mut x_parameters = Vec::new();
266        let mut retained_parameters = Vec::new();
267
268        for param in prop.parameters {
269            match param {
270                p @ Parameter::Language { .. } if language.is_some() => {
271                    errors.push(TypedError::ParameterDuplicated {
272                        span: p.span(),
273                        parameter: p.kind().into(),
274                    });
275                }
276                Parameter::Language { value, .. } => language = Some(value),
277
278                Parameter::XName(raw) => x_parameters.push(raw),
279                p @ Parameter::Unrecognized { .. } => retained_parameters.push(p),
280                p => {
281                    // Preserve other parameters not used by this property for round-trip
282                    retained_parameters.push(p);
283                }
284            }
285        }
286
287        // Return all errors if any occurred
288        if !errors.is_empty() {
289            return Err(errors);
290        }
291
292        Ok(Self {
293            content,
294            language,
295            x_parameters,
296            retained_parameters,
297        })
298    }
299}
300
301impl TextWithLanguage<Segments<'_>> {
302    /// Convert borrowed `TextWithLanguage` to owned `TextWithLanguage`
303    #[must_use]
304    pub fn to_owned(&self) -> TextWithLanguage<String> {
305        TextWithLanguage {
306            content: self.content.to_owned(),
307            language: self.language.as_ref().map(Segments::to_owned),
308            x_parameters: self
309                .x_parameters
310                .iter()
311                .map(RawParameter::to_owned)
312                .collect(),
313            retained_parameters: self
314                .retained_parameters
315                .iter()
316                .map(Parameter::to_owned)
317                .collect(),
318        }
319    }
320}
321
322/// Text with language and alternate representation information
323///
324/// This is a helper type used by text properties that support both LANGUAGE
325/// and ALTREP parameters:
326/// - 3.8.1.4: `Comment`
327/// - 3.8.1.5: `Description`
328/// - 3.8.1.7: `Location`
329/// - 3.8.1.10: `Resources` (multi-valued)
330/// - 3.8.1.12: `Summary`
331/// - 3.8.4.2: `Contact`
332#[derive(Debug, Clone)]
333pub struct Text<S: StringStorage> {
334    /// The actual text content
335    pub content: ValueText<S>,
336
337    /// Language code (optional)
338    pub language: Option<S>,
339
340    /// Alternate text representation URI (optional)
341    ///
342    /// Per RFC 5545 Section 3.2.1, this parameter specifies a URI that points
343    /// to an alternate representation for the textual property value.
344    pub altrep: Option<S>,
345
346    /// X-name parameters (custom experimental parameters)
347    pub x_parameters: Vec<RawParameter<S>>,
348
349    /// Unrecognized / Non-standard parameters (preserved for round-trip)
350    pub retained_parameters: Vec<Parameter<S>>,
351}
352
353impl<'src> TryFrom<ParsedProperty<'src>> for Text<Segments<'src>> {
354    type Error = Vec<TypedError<'src>>;
355
356    fn try_from(prop: ParsedProperty<'src>) -> Result<Self, Self::Error> {
357        let content = take_single_text(&prop.kind, prop.value)?;
358
359        let mut errors = Vec::new();
360
361        // Extract language, altrep, and unknown parameters
362        let mut language = None;
363        let mut altrep = None;
364        let mut x_parameters = Vec::new();
365        let mut retained_parameters = Vec::new();
366
367        for param in prop.parameters {
368            match param {
369                p @ Parameter::Language { .. } if language.is_some() => {
370                    errors.push(TypedError::ParameterDuplicated {
371                        span: p.span(),
372                        parameter: p.kind().into(),
373                    });
374                }
375                Parameter::Language { value, .. } => language = Some(value),
376
377                p @ Parameter::AlternateText { .. } if altrep.is_some() => {
378                    errors.push(TypedError::ParameterDuplicated {
379                        span: p.span(),
380                        parameter: p.kind().into(),
381                    });
382                }
383                Parameter::AlternateText { value, .. } => altrep = Some(value),
384
385                Parameter::XName(raw) => x_parameters.push(raw),
386                p @ Parameter::Unrecognized { .. } => retained_parameters.push(p),
387                p => {
388                    // Preserve other parameters not used by this property for round-trip
389                    retained_parameters.push(p);
390                }
391            }
392        }
393
394        // Return all errors if any occurred
395        if !errors.is_empty() {
396            return Err(errors);
397        }
398
399        Ok(Self {
400            content,
401            language,
402            altrep,
403            x_parameters,
404            retained_parameters,
405        })
406    }
407}
408
409impl Text<Segments<'_>> {
410    /// Convert borrowed Text to owned Text
411    #[must_use]
412    pub fn to_owned(&self) -> Text<String> {
413        Text {
414            content: self.content.to_owned(),
415            language: self.language.as_ref().map(Segments::to_owned),
416            altrep: self.altrep.as_ref().map(Segments::to_owned),
417            x_parameters: self
418                .x_parameters
419                .iter()
420                .map(RawParameter::to_owned)
421                .collect(),
422            retained_parameters: self
423                .retained_parameters
424                .iter()
425                .map(Parameter::to_owned)
426                .collect(),
427        }
428    }
429}
430
431impl Text<String> {
432    /// Create a new `Text<String>` from a string value.
433    ///
434    /// This constructor is provided for convenient construction of owned text properties.
435    /// The input string is treated as an unescaped text value with no parameters.
436    #[must_use]
437    pub fn new(value: String) -> Self {
438        Self {
439            content: ValueText::new(value),
440            language: None,
441            altrep: None,
442            x_parameters: Vec::new(),
443            retained_parameters: Vec::new(),
444        }
445    }
446}
447
448impl TextOnly<String> {
449    /// Create a new `TextOnly<String>` from a string value.
450    ///
451    /// This constructor is provided for convenient construction of owned text-only properties.
452    /// The input string is treated as an unescaped text value with no parameters.
453    #[must_use]
454    pub fn new(value: String) -> Self {
455        Self {
456            content: ValueText::new(value),
457            x_parameters: Vec::new(),
458            retained_parameters: Vec::new(),
459        }
460    }
461}
462
463/// Macro to define simple property wrappers with generic storage parameter.
464///
465/// This is similar to `simple_property_wrapper!` but generates generic wrappers
466/// that accept a storage parameter `S: StringStorage` instead of hardcoding
467/// the lifetime `'src`.
468///
469/// Usage:
470///
471/// ```ignore
472/// simple_property_wrapper!(
473///     Comment<S>: Text => Comment
474/// );
475/// ```
476///
477/// This generates:
478///
479/// ```ignore
480/// pub struct Comment<S: StringStorage>(pub Text<S>);
481/// ```
482macro_rules! simple_property_wrapper {
483    (
484        $(#[$meta:meta])*
485        $vis:vis $name:ident <S> => $inner:ident
486    ) => {
487        $(#[$meta])*
488        #[derive(Debug, Clone)]
489        $vis struct $name<S: StringStorage> {
490            /// Inner property value
491            pub inner: $inner<S>,
492            /// Span of the property in the source
493            pub span: S::Span,
494        }
495
496        impl<S> ::core::ops::Deref for $name<S>
497        where
498            S: StringStorage,
499        {
500            type Target = $inner<S>;
501
502            fn deref(&self) -> &Self::Target {
503                &self.inner
504            }
505        }
506
507        impl<S> ::core::ops::DerefMut for $name<S>
508        where
509            S: StringStorage,
510        {
511            fn deref_mut(&mut self) -> &mut Self::Target {
512                &mut self.inner
513            }
514        }
515
516        impl<'src> ::core::convert::TryFrom<crate::typed::ParsedProperty<'src>> for $name<crate::string_storage::Segments<'src>>
517        where
518            $inner<crate::string_storage::Segments<'src>>: ::core::convert::TryFrom<crate::typed::ParsedProperty<'src>, Error = Vec<crate::typed::TypedError<'src>>>,
519        {
520            type Error = Vec<crate::typed::TypedError<'src>>;
521
522            fn try_from(prop: crate::typed::ParsedProperty<'src>) -> Result<Self, Self::Error> {
523                if !matches!(prop.kind, crate::property::PropertyKind::$name) {
524                    return Err(vec![crate::typed::TypedError::PropertyUnexpectedKind {
525                        expected: crate::property::PropertyKind::$name,
526                        found: prop.kind,
527                        span: prop.span,
528                    }]);
529                }
530
531                let span = prop.span;
532                <$inner<crate::string_storage::Segments<'src>>>::try_from(prop).map(|inner| $name { inner, span })
533            }
534        }
535
536        impl $name<crate::string_storage::Segments<'_>> {
537            /// Convert borrowed type to owned type
538            #[must_use]
539            pub fn to_owned(&self) -> $name<String> {
540                $name {
541                    inner: self.inner.to_owned(),
542                    span: (),
543                }
544            }
545        }
546    };
547}
548
549/// Macro to define simple enums for property values.
550///
551/// This generates simple enums with Copy semantics for RFC 5545 parameter values
552/// that don't support extensions.
553macro_rules! define_prop_value_enum {
554    (
555        $(#[$meta:meta])*
556        $vis:vis enum $Name:ident {
557            $(
558                $(#[$vmeta:meta])*
559                $Variant:ident => $kw:ident
560            ),* $(,)?
561        }
562    ) => {
563        $(#[$meta])*
564        #[derive(Debug, Clone, Copy, PartialEq, Eq)]
565        #[allow(missing_docs)]
566        $vis enum $Name {
567            $(
568                $(#[$vmeta])*
569                $Variant,
570            )*
571        }
572
573
574        impl<'src> TryFrom<crate::value::ValueText<Segments<'src>>> for $Name {
575            type Error = crate::value::ValueText<Segments<'src>>;
576
577            fn try_from(segs: crate::value::ValueText<Segments<'src>>) -> Result<Self, Self::Error> {
578                $(
579                    if segs.eq_str_ignore_ascii_case($kw) {
580                        return Ok(Self::$Variant);
581                    }
582                )*
583                Err(segs)
584            }
585        }
586
587        impl ::core::fmt::Display for $Name {
588            fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result {
589                match self {
590                    $(
591                        Self::$Variant => $kw.fmt(f),
592                    )*
593                }
594            }
595        }
596    };
597}