aimcal_ical/property/
descriptive.rs

1// SPDX-FileCopyrightText: 2025-2026 Zexin Yuan <aim@yzx9.xyz>
2//
3// SPDX-License-Identifier: Apache-2.0
4
5//! Classification and Status Properties (RFC 5545 Section 3.8.1)
6//!
7//! - 3.8.1.1: `Attachment` - Attached documents or resources
8//! - 3.8.1.2: `Categories` - Categories or tags
9//! - 3.8.1.3: `Classification` - Access classification (PUBLIC, PRIVATE, CONFIDENTIAL)
10//! - 3.8.1.4: `Comment` - Non-processing comments
11//! - 3.8.1.5: `Description` - Detailed description
12//! - 3.8.1.6: `Geo` - Geographic position (latitude/longitude)
13//! - 3.8.1.7: `Location` - Venue location
14//! - 3.8.1.8: `PercentComplete` - Percent complete for todos (0-100)
15//! - 3.8.1.9: `Priority` - Priority level (0-9, undefined = 0)
16//! - 3.8.1.10: `Resources` - Resources
17//! - 3.8.1.11: `Status` - Component status (TENTATIVE, CONFIRMED, CANCELLED, etc.)
18//! - 3.8.1.12: `Summary` - Summary/subject
19
20use std::convert::TryFrom;
21
22use chumsky::input::{Input, Stream};
23use chumsky::prelude::*;
24use chumsky::{Parser, error::Rich, extra};
25
26use crate::keyword::{
27    KW_CLASS_CONFIDENTIAL, KW_CLASS_PRIVATE, KW_CLASS_PUBLIC, KW_STATUS_CANCELLED,
28    KW_STATUS_COMPLETED, KW_STATUS_CONFIRMED, KW_STATUS_DRAFT, KW_STATUS_FINAL,
29    KW_STATUS_IN_PROCESS, KW_STATUS_NEEDS_ACTION, KW_STATUS_TENTATIVE,
30};
31use crate::parameter::{Encoding, Parameter, ValueType};
32use crate::property::PropertyKind;
33use crate::property::common::{Text, take_single_text, take_single_value};
34use crate::string_storage::{Segments, StringStorage};
35use crate::syntax::RawParameter;
36use crate::typed::{ParsedProperty, TypedError};
37use crate::value::{Value, ValueText, values_float_semicolon};
38
39/// Attachment information (RFC 5545 Section 3.8.1.1)
40#[derive(Debug, Clone)]
41pub struct Attachment<S: StringStorage> {
42    /// URI or binary data
43    pub value: AttachmentValue<S>,
44    /// Format type (optional)
45    pub fmt_type: Option<S>,
46    /// Encoding (optional)
47    pub encoding: Option<Encoding>,
48    /// X-name parameters (custom experimental parameters)
49    pub x_parameters: Vec<RawParameter<S>>,
50    /// Unrecognized / Non-standard parameters (preserved for round-trip)
51    pub retained_parameters: Vec<Parameter<S>>,
52    /// Span of the property in the source
53    pub span: S::Span,
54}
55
56/// Attachment value (URI or binary)
57#[derive(Debug, Clone)]
58pub enum AttachmentValue<S: StringStorage> {
59    /// URI reference
60    Uri(S),
61    /// Binary data
62    Binary(S),
63}
64
65impl<'src> TryFrom<ParsedProperty<'src>> for Attachment<Segments<'src>> {
66    type Error = Vec<TypedError<'src>>;
67
68    fn try_from(prop: ParsedProperty<'src>) -> Result<Self, Self::Error> {
69        if !matches!(prop.kind, PropertyKind::Attach) {
70            return Err(vec![TypedError::PropertyUnexpectedKind {
71                expected: PropertyKind::Attach,
72                found: prop.kind,
73                span: prop.span,
74            }]);
75        }
76
77        let mut errors = Vec::new();
78
79        // Collect all optional parameters in a single pass
80        let mut fmt_type = None;
81        let mut encoding = None;
82        let mut x_parameters = Vec::new();
83        let mut retained_parameters = Vec::new();
84
85        for param in prop.parameters {
86            match param {
87                p @ Parameter::FormatType { .. } if fmt_type.is_some() => {
88                    errors.push(TypedError::ParameterDuplicated {
89                        span: p.span(),
90                        parameter: p.kind().into(),
91                    });
92                }
93                Parameter::FormatType { value, .. } => fmt_type = Some(value),
94
95                p @ Parameter::Encoding { .. } if encoding.is_some() => {
96                    errors.push(TypedError::ParameterDuplicated {
97                        span: p.span(),
98                        parameter: p.kind().into(),
99                    });
100                }
101                Parameter::Encoding { value, .. } => encoding = Some(value),
102
103                Parameter::XName(raw) => x_parameters.push(raw),
104                p @ Parameter::Unrecognized { .. } => retained_parameters.push(p),
105                p => {
106                    // Preserve other parameters not used by this property for round-trip
107                    retained_parameters.push(p);
108                }
109            }
110        }
111
112        // Get value
113        let value = match take_single_value(&PropertyKind::Attach, prop.value) {
114            Ok(Value::Binary { value, .. }) => Some(AttachmentValue::Binary(value)),
115            Ok(Value::Uri { value, .. }) => Some(AttachmentValue::Uri(value)),
116            Ok(v) => {
117                const EXPECTED: &[ValueType<String>] = &[ValueType::Uri, ValueType::Binary];
118                errors.push(TypedError::PropertyUnexpectedValue {
119                    property: prop.kind,
120                    expected: EXPECTED,
121                    found: v.kind().into(),
122                    span: v.span(),
123                });
124                None
125            }
126            Err(e) => {
127                errors.extend(e);
128                None
129            }
130        };
131
132        // Return all errors if any occurred
133        if !errors.is_empty() {
134            return Err(errors);
135        }
136
137        Ok(Attachment {
138            value: value.unwrap(), // SAFETY: checked errors above
139            fmt_type,
140            encoding,
141            x_parameters,
142            retained_parameters,
143            span: prop.span,
144        })
145    }
146}
147
148impl Attachment<Segments<'_>> {
149    /// Convert borrowed `Attachment` to owned `Attachment`
150    #[must_use]
151    pub fn to_owned(&self) -> Attachment<String> {
152        Attachment {
153            value: self.value.to_owned(),
154            fmt_type: self.fmt_type.as_ref().map(Segments::to_owned),
155            encoding: self.encoding,
156            x_parameters: self
157                .x_parameters
158                .iter()
159                .map(RawParameter::to_owned)
160                .collect(),
161            retained_parameters: self
162                .retained_parameters
163                .iter()
164                .map(Parameter::to_owned)
165                .collect(),
166            span: (),
167        }
168    }
169}
170
171impl AttachmentValue<Segments<'_>> {
172    /// Convert borrowed `AttachmentValue` to owned `AttachmentValue`
173    #[must_use]
174    pub fn to_owned(&self) -> AttachmentValue<String> {
175        match self {
176            AttachmentValue::Uri(uri) => AttachmentValue::Uri(uri.to_owned()),
177            AttachmentValue::Binary(data) => AttachmentValue::Binary(data.to_owned()),
178        }
179    }
180}
181
182define_prop_value_enum! {
183    /// Classification value (RFC 5545 Section 3.8.1.3)
184    #[derive(Default)]
185    pub enum ClassificationValue {
186        /// Public classification
187        #[default]
188        Public => KW_CLASS_PUBLIC,
189        /// Private classification
190        Private => KW_CLASS_PRIVATE,
191        /// Confidential classification
192        Confidential => KW_CLASS_CONFIDENTIAL,
193    }
194}
195
196/// Classification of calendar data (RFC 5545 Section 3.8.1.3)
197#[derive(Debug, Clone)]
198pub struct Classification<S: StringStorage> {
199    /// Classification value
200    pub value: ClassificationValue,
201    /// X-name parameters (custom experimental parameters)
202    pub x_parameters: Vec<RawParameter<S>>,
203    /// Unrecognized / Non-standard parameters (preserved for round-trip)
204    pub retained_parameters: Vec<Parameter<S>>,
205    /// Span of the property in the source
206    pub span: S::Span,
207}
208
209impl<'src> TryFrom<ParsedProperty<'src>> for Classification<Segments<'src>> {
210    type Error = Vec<TypedError<'src>>;
211
212    fn try_from(prop: ParsedProperty<'src>) -> Result<Self, Self::Error> {
213        if !matches!(prop.kind, PropertyKind::Class) {
214            return Err(vec![TypedError::PropertyUnexpectedKind {
215                expected: PropertyKind::Class,
216                found: prop.kind,
217                span: prop.span,
218            }]);
219        }
220
221        let mut x_parameters = Vec::new();
222        let mut retained_parameters = Vec::new();
223
224        for param in prop.parameters {
225            match param {
226                Parameter::XName(raw) => x_parameters.push(raw),
227                p @ Parameter::Unrecognized { .. } => retained_parameters.push(p),
228                p => {
229                    // Preserve other parameters not used by this property for round-trip
230                    retained_parameters.push(p);
231                }
232            }
233        }
234
235        let value_span = prop.value.span();
236        let text = take_single_text(&PropertyKind::Class, prop.value)?;
237        let value = text.try_into().map_err(|text| {
238            vec![TypedError::PropertyInvalidValue {
239                property: PropertyKind::Class,
240                value: format!("Invalid classification: {text}"),
241                span: value_span,
242            }]
243        })?;
244
245        Ok(Self {
246            value,
247            x_parameters,
248            retained_parameters,
249            span: prop.span,
250        })
251    }
252}
253
254impl Classification<Segments<'_>> {
255    /// Convert borrowed `Classification` to owned `Classification`
256    #[must_use]
257    pub fn to_owned(&self) -> Classification<String> {
258        Classification {
259            value: self.value,
260            x_parameters: self
261                .x_parameters
262                .iter()
263                .map(RawParameter::to_owned)
264                .collect(),
265            retained_parameters: self
266                .retained_parameters
267                .iter()
268                .map(Parameter::to_owned)
269                .collect(),
270            span: (),
271        }
272    }
273}
274
275simple_property_wrapper!(
276    /// Simple text property wrapper (RFC 5545 Section 3.8.1.4)
277    pub Comment<S> => Text
278);
279
280simple_property_wrapper!(
281    /// Simple text property wrapper (RFC 5545 Section 3.8.1.5)
282    pub Description<S> => Text
283);
284
285impl Description<String> {
286    /// Create a new `Description<String>` from a string value.
287    #[must_use]
288    pub fn new(value: String) -> Self {
289        Self {
290            inner: Text::new(value),
291            span: (),
292        }
293    }
294}
295
296/// Geographic position (RFC 5545 Section 3.8.1.6)
297#[derive(Debug, Clone)]
298pub struct Geo<S: StringStorage> {
299    /// Latitude
300    pub lat: f64,
301    /// Longitude
302    pub lon: f64,
303    /// X-name parameters (custom experimental parameters)
304    pub x_parameters: Vec<RawParameter<S>>,
305    /// Unrecognized / Non-standard parameters (preserved for round-trip)
306    pub retained_parameters: Vec<Parameter<S>>,
307    /// Span of the property in the source
308    pub span: S::Span,
309}
310
311impl<'src> TryFrom<ParsedProperty<'src>> for Geo<Segments<'src>> {
312    type Error = Vec<TypedError<'src>>;
313
314    fn try_from(prop: ParsedProperty<'src>) -> Result<Self, Self::Error> {
315        if !matches!(prop.kind, PropertyKind::Geo) {
316            return Err(vec![TypedError::PropertyUnexpectedKind {
317                expected: PropertyKind::Geo,
318                found: prop.kind,
319                span: prop.span,
320            }]);
321        }
322
323        let mut x_parameters = Vec::new();
324        let mut retained_parameters = Vec::new();
325
326        for param in prop.parameters {
327            match param {
328                Parameter::XName(raw) => x_parameters.push(raw),
329                p @ Parameter::Unrecognized { .. } => retained_parameters.push(p),
330                p => {
331                    // Preserve other parameters not used by this property for round-trip
332                    retained_parameters.push(p);
333                }
334            }
335        }
336
337        let value_span = prop.value.span();
338        let text = take_single_text(&PropertyKind::Geo, prop.value)?;
339
340        // Use the typed phase's float parser with semicolon separator
341        let stream = make_input(text.clone()); // TODO: avoid clone
342        let parser = values_float_semicolon::<_, extra::Err<Rich<char, _>>>();
343
344        match parser.parse(stream).into_result() {
345            Ok(result) => {
346                if result.len() != 2 {
347                    return Err(vec![TypedError::PropertyInvalidValue {
348                        property: PropertyKind::Geo,
349                        value: format!(
350                            "Expected exactly 2 float values (lat;long), got {}",
351                            result.len()
352                        ),
353                        span: value_span,
354                    }]);
355                }
356
357                Ok(Geo {
358                    lat: result.first().copied().unwrap_or_default(),
359                    lon: result.get(1).copied().unwrap_or_default(),
360                    x_parameters,
361                    retained_parameters,
362                    span: prop.span,
363                })
364            }
365            Err(_) => Err(vec![TypedError::PropertyInvalidValue {
366                property: PropertyKind::Geo,
367                value: format!("Expected 'lat;long' format with semicolon separator, got {text}"),
368                span: value_span,
369            }]),
370        }
371    }
372}
373
374impl Geo<Segments<'_>> {
375    /// Convert borrowed `Geo` to owned `Geo`
376    #[must_use]
377    pub fn to_owned(&self) -> Geo<String> {
378        Geo {
379            lat: self.lat,
380            lon: self.lon,
381            x_parameters: self
382                .x_parameters
383                .iter()
384                .map(RawParameter::to_owned)
385                .collect(),
386            retained_parameters: self
387                .retained_parameters
388                .iter()
389                .map(Parameter::to_owned)
390                .collect(),
391            span: (),
392        }
393    }
394}
395
396simple_property_wrapper!(
397    /// Simple text property wrapper (RFC 5545 Section 3.8.1.7)
398    pub Location<S> => Text
399);
400
401impl Location<String> {
402    /// Create a new `Location<String>` from a string value.
403    #[must_use]
404    pub fn new(value: String) -> Self {
405        Self {
406            inner: Text::new(value),
407            span: (),
408        }
409    }
410}
411
412define_prop_value_enum! {
413    /// Status value (RFC 5545 Section 3.8.1.11)
414    ///
415    /// This enum represents the status of calendar components such as events,
416    /// to-dos, and journal entries. Each variant corresponds to a specific status
417    /// defined in the iCalendar specification.
418    pub enum StatusValue {
419        /// Event is tentative
420        Tentative => KW_STATUS_TENTATIVE,
421        /// Event is confirmed
422        Confirmed => KW_STATUS_CONFIRMED,
423        /// To-do needs action
424        NeedsAction => KW_STATUS_NEEDS_ACTION,
425        /// To-do is completed
426        Completed => KW_STATUS_COMPLETED,
427        /// To-do is in process
428        InProcess => KW_STATUS_IN_PROCESS,
429        /// Journal entry is draft
430        Draft => KW_STATUS_DRAFT,
431        /// Journal entry is final
432        Final => KW_STATUS_FINAL,
433        /// Event/To-do/Journal is cancelled
434        Cancelled => KW_STATUS_CANCELLED,
435    }
436}
437
438/// Event/To-do/Journal status (RFC 5545 Section 3.8.1.11)
439#[derive(Debug, Clone)]
440pub struct Status<S: StringStorage> {
441    /// Status value
442    pub value: StatusValue,
443    /// X-name parameters (custom experimental parameters)
444    pub x_parameters: Vec<RawParameter<S>>,
445    /// Unrecognized / Non-standard parameters (preserved for round-trip)
446    pub retained_parameters: Vec<Parameter<S>>,
447    /// Span of the property in the source
448    pub span: S::Span,
449}
450
451impl<'src> TryFrom<ParsedProperty<'src>> for Status<Segments<'src>> {
452    type Error = Vec<TypedError<'src>>;
453
454    fn try_from(prop: ParsedProperty<'src>) -> Result<Self, Self::Error> {
455        if !matches!(prop.kind, PropertyKind::Status) {
456            return Err(vec![TypedError::PropertyUnexpectedKind {
457                expected: PropertyKind::Status,
458                found: prop.kind,
459                span: prop.span,
460            }]);
461        }
462
463        let mut x_parameters = Vec::new();
464        let mut retained_parameters = Vec::new();
465
466        for param in prop.parameters {
467            match param {
468                Parameter::XName(raw) => x_parameters.push(raw),
469                p @ Parameter::Unrecognized { .. } => retained_parameters.push(p),
470                p => {
471                    // Preserve other parameters not used by this property for round-trip
472                    retained_parameters.push(p);
473                }
474            }
475        }
476
477        let value_span = prop.value.span();
478        let text = take_single_text(&PropertyKind::Status, prop.value)?;
479        let value = text.try_into().map_err(|text| {
480            vec![TypedError::PropertyInvalidValue {
481                property: PropertyKind::Status,
482                value: format!("Invalid status: {text}"),
483                span: value_span,
484            }]
485        })?;
486
487        Ok(Self {
488            value,
489            x_parameters,
490            retained_parameters,
491            span: prop.span,
492        })
493    }
494}
495
496impl Status<Segments<'_>> {
497    /// Convert borrowed `Status` to owned `Status`
498    #[must_use]
499    pub fn to_owned(&self) -> Status<String> {
500        Status {
501            value: self.value,
502            x_parameters: self
503                .x_parameters
504                .iter()
505                .map(RawParameter::to_owned)
506                .collect(),
507            retained_parameters: self
508                .retained_parameters
509                .iter()
510                .map(Parameter::to_owned)
511                .collect(),
512            span: (),
513        }
514    }
515}
516
517impl Status<String> {
518    /// Create a new `Status<String>` from a status value.
519    #[must_use]
520    pub fn new(value: StatusValue) -> Self {
521        Self {
522            value,
523            x_parameters: Vec::new(),
524            retained_parameters: Vec::new(),
525            span: (),
526        }
527    }
528}
529
530/// Percent Complete (RFC 5545 Section 3.8.1.8)
531///
532/// This property defines the percent complete for a todo.
533/// Value must be between 0 and 100.
534#[derive(Debug, Clone)]
535pub struct PercentComplete<S: StringStorage> {
536    /// Percent complete (0-100)
537    pub value: u8,
538    /// X-name parameters (custom experimental parameters)
539    pub x_parameters: Vec<RawParameter<S>>,
540    /// Unrecognized / Non-standard parameters (preserved for round-trip)
541    pub retained_parameters: Vec<Parameter<S>>,
542    /// Span of the property in the source
543    pub span: S::Span,
544}
545
546impl<'src> TryFrom<ParsedProperty<'src>> for PercentComplete<Segments<'src>> {
547    type Error = Vec<TypedError<'src>>;
548
549    fn try_from(prop: ParsedProperty<'src>) -> Result<Self, Self::Error> {
550        if !matches!(prop.kind, PropertyKind::PercentComplete) {
551            return Err(vec![TypedError::PropertyUnexpectedKind {
552                expected: PropertyKind::PercentComplete,
553                found: prop.kind,
554                span: prop.span,
555            }]);
556        }
557
558        let mut x_parameters = Vec::new();
559        let mut retained_parameters = Vec::new();
560
561        for param in prop.parameters {
562            match param {
563                Parameter::XName(raw) => x_parameters.push(raw),
564                p @ Parameter::Unrecognized { .. } => retained_parameters.push(p),
565                p => {
566                    // Preserve other parameters not used by this property for round-trip
567                    retained_parameters.push(p);
568                }
569            }
570        }
571
572        let value_span = prop.value.span();
573        match take_single_value(&PropertyKind::PercentComplete, prop.value) {
574            #[expect(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
575            Ok(Value::Integer {
576                values: mut ints, ..
577            }) if ints.len() == 1 => {
578                let i = ints.pop().unwrap();
579                if (0..=100).contains(&i) {
580                    Ok(Self {
581                        value: i as u8,
582                        x_parameters,
583                        retained_parameters,
584                        span: prop.span,
585                    })
586                } else {
587                    Err(vec![TypedError::PropertyInvalidValue {
588                        property: prop.kind,
589                        value: "Percent complete must be 0-100".to_string(),
590                        span: value_span,
591                    }])
592                }
593            }
594            Ok(Value::Integer { .. }) => Err(vec![TypedError::PropertyInvalidValue {
595                property: prop.kind,
596                value: "Percent complete must be 0-100".to_string(),
597                span: value_span,
598            }]),
599            Ok(v) => {
600                const EXPECTED: &[ValueType<String>] = &[ValueType::Integer];
601                let span = v.span();
602                Err(vec![TypedError::PropertyUnexpectedValue {
603                    property: prop.kind,
604                    expected: EXPECTED,
605                    found: v.kind().into(),
606                    span,
607                }])
608            }
609            Err(e) => Err(e),
610        }
611    }
612}
613
614impl PercentComplete<Segments<'_>> {
615    /// Convert borrowed `PercentComplete` to owned `PercentComplete`
616    #[must_use]
617    pub fn to_owned(&self) -> PercentComplete<String> {
618        PercentComplete {
619            value: self.value,
620            x_parameters: self
621                .x_parameters
622                .iter()
623                .map(RawParameter::to_owned)
624                .collect(),
625            retained_parameters: self
626                .retained_parameters
627                .iter()
628                .map(Parameter::to_owned)
629                .collect(),
630            span: (),
631        }
632    }
633}
634
635impl PercentComplete<String> {
636    /// Create a new `PercentComplete<String>` from a percent value (0-100).
637    #[must_use]
638    pub fn new(value: u8) -> Self {
639        Self {
640            value,
641            x_parameters: Vec::new(),
642            retained_parameters: Vec::new(),
643            span: (),
644        }
645    }
646}
647
648/// Priority (RFC 5545 Section 3.8.1.9)
649///
650/// This property defines the priority for a calendar component.
651/// Value must be between 0 and 9, where 0 defines an undefined priority.
652#[derive(Debug, Clone)]
653pub struct Priority<S: StringStorage> {
654    /// Priority value (0-9, where 0 is undefined)
655    pub value: u8,
656    /// X-name parameters (custom experimental parameters)
657    pub x_parameters: Vec<RawParameter<S>>,
658    /// Unrecognized / Non-standard parameters (preserved for round-trip)
659    pub retained_parameters: Vec<Parameter<S>>,
660    /// Span of the property in the source
661    pub span: S::Span,
662}
663
664impl<'src> TryFrom<ParsedProperty<'src>> for Priority<Segments<'src>> {
665    type Error = Vec<TypedError<'src>>;
666
667    fn try_from(prop: ParsedProperty<'src>) -> Result<Self, Self::Error> {
668        if !matches!(prop.kind, PropertyKind::Priority) {
669            return Err(vec![TypedError::PropertyUnexpectedKind {
670                expected: PropertyKind::Priority,
671                found: prop.kind,
672                span: prop.span,
673            }]);
674        }
675
676        let mut x_parameters = Vec::new();
677        let mut retained_parameters = Vec::new();
678
679        for param in prop.parameters {
680            match param {
681                Parameter::XName(raw) => x_parameters.push(raw),
682                p @ Parameter::Unrecognized { .. } => retained_parameters.push(p),
683                p => {
684                    // Preserve other parameters not used by this property for round-trip
685                    retained_parameters.push(p);
686                }
687            }
688        }
689
690        let value_span = prop.value.span();
691        match take_single_value(&PropertyKind::Priority, prop.value) {
692            #[expect(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
693            Ok(Value::Integer {
694                values: mut ints, ..
695            }) if ints.len() == 1 => {
696                let i = ints.pop().unwrap();
697                if (0..=9).contains(&i) {
698                    Ok(Self {
699                        value: i as u8,
700                        x_parameters,
701                        retained_parameters,
702                        span: prop.span,
703                    })
704                } else {
705                    Err(vec![TypedError::PropertyInvalidValue {
706                        property: prop.kind,
707                        value: "Priority must be 0-9".to_string(),
708                        span: value_span,
709                    }])
710                }
711            }
712            Ok(Value::Integer { .. }) => Err(vec![TypedError::PropertyInvalidValue {
713                property: prop.kind,
714                value: "Priority must be 0-9".to_string(),
715                span: value_span,
716            }]),
717            Ok(v) => {
718                const EXPECTED: &[ValueType<String>] = &[ValueType::Integer];
719                let span = v.span();
720                Err(vec![TypedError::PropertyUnexpectedValue {
721                    property: prop.kind,
722                    expected: EXPECTED,
723                    found: v.kind().into(),
724                    span,
725                }])
726            }
727            Err(e) => Err(e),
728        }
729    }
730}
731
732impl Priority<Segments<'_>> {
733    /// Convert borrowed `Priority` to owned `Priority`
734    #[must_use]
735    pub fn to_owned(&self) -> Priority<String> {
736        Priority {
737            value: self.value,
738            x_parameters: self
739                .x_parameters
740                .iter()
741                .map(RawParameter::to_owned)
742                .collect(),
743            retained_parameters: self
744                .retained_parameters
745                .iter()
746                .map(Parameter::to_owned)
747                .collect(),
748            span: (),
749        }
750    }
751}
752
753impl Priority<String> {
754    /// Create a new `Priority<String>` from a priority value (0-9).
755    #[must_use]
756    pub fn new(value: u8) -> Self {
757        Self {
758            value,
759            x_parameters: Vec::new(),
760            retained_parameters: Vec::new(),
761            span: (),
762        }
763    }
764}
765
766/// Categories property (RFC 5545 Section 3.8.1.2)
767///
768/// This property defines the categories for a calendar component.
769///
770/// Per RFC 5545, CATEGORIES supports the LANGUAGE parameter but NOT ALTREP.
771#[derive(Debug, Clone)]
772pub struct Categories<S: StringStorage> {
773    /// List of category text values
774    pub values: Vec<ValueText<S>>,
775    /// Language code (optional, applied to all values)
776    pub language: Option<S>,
777    /// X-name parameters (custom experimental parameters)
778    pub x_parameters: Vec<RawParameter<S>>,
779    /// Unrecognized / Non-standard parameters (preserved for round-trip)
780    pub retained_parameters: Vec<Parameter<S>>,
781    /// Span of the property in the source
782    pub span: S::Span,
783}
784
785impl<'src> TryFrom<ParsedProperty<'src>> for Categories<Segments<'src>> {
786    type Error = Vec<TypedError<'src>>;
787
788    fn try_from(prop: ParsedProperty<'src>) -> Result<Self, Self::Error> {
789        if !matches!(prop.kind, PropertyKind::Categories) {
790            return Err(vec![TypedError::PropertyUnexpectedKind {
791                expected: PropertyKind::Categories,
792                found: prop.kind,
793                span: prop.span,
794            }]);
795        }
796
797        let mut language = None;
798        let mut x_parameters = Vec::new();
799        let mut retained_parameters = Vec::new();
800
801        for param in prop.parameters {
802            match param {
803                p @ Parameter::Language { .. } if language.is_some() => {
804                    return Err(vec![TypedError::ParameterDuplicated {
805                        span: p.span(),
806                        parameter: p.kind().into(),
807                    }]);
808                }
809                Parameter::Language { value, .. } => language = Some(value),
810
811                Parameter::XName(raw) => x_parameters.push(raw),
812                p @ Parameter::Unrecognized { .. } => retained_parameters.push(p),
813                p => {
814                    // Preserve other parameters not used by this property for round-trip
815                    retained_parameters.push(p);
816                }
817            }
818        }
819
820        let Value::Text { values, .. } = prop.value else {
821            const EXPECTED: &[ValueType<String>] = &[ValueType::Text];
822            let span = prop.value.span();
823            return Err(vec![TypedError::PropertyUnexpectedValue {
824                property: prop.kind,
825                expected: EXPECTED,
826                found: prop.value.kind().into(),
827                span,
828            }]);
829        };
830
831        Ok(Self {
832            values,
833            language,
834            x_parameters,
835            retained_parameters,
836            span: prop.span,
837        })
838    }
839}
840
841impl Categories<Segments<'_>> {
842    /// Convert borrowed `Categories` to owned `Categories`
843    #[must_use]
844    pub fn to_owned(&self) -> Categories<String> {
845        Categories {
846            values: self.values.iter().map(ValueText::to_owned).collect(),
847            language: self.language.as_ref().map(Segments::to_owned),
848            x_parameters: self
849                .x_parameters
850                .iter()
851                .map(RawParameter::to_owned)
852                .collect(),
853            retained_parameters: self
854                .retained_parameters
855                .iter()
856                .map(Parameter::to_owned)
857                .collect(),
858            span: (),
859        }
860    }
861}
862
863/// Resources property (RFC 5545 Section 3.8.1.10)
864///
865/// This property defines the equipment or resources anticipated for an activity.
866///
867/// Per RFC 5545, RESOURCES supports both LANGUAGE and ALTREP parameters.
868#[derive(Debug, Clone)]
869pub struct Resources<S: StringStorage> {
870    /// List of resource text values
871    pub values: Vec<ValueText<S>>,
872    /// Language code (optional, applied to all values)
873    pub language: Option<S>,
874    /// Alternate text representation URI (optional)
875    pub altrep: Option<S>,
876    /// X-name parameters (custom experimental parameters)
877    pub x_parameters: Vec<RawParameter<S>>,
878    /// Unrecognized / Non-standard parameters (preserved for round-trip)
879    pub retained_parameters: Vec<Parameter<S>>,
880    /// Span of the property in the source
881    pub span: S::Span,
882}
883
884impl<'src> TryFrom<ParsedProperty<'src>> for Resources<Segments<'src>> {
885    type Error = Vec<TypedError<'src>>;
886
887    fn try_from(prop: ParsedProperty<'src>) -> Result<Self, Self::Error> {
888        if !matches!(prop.kind, PropertyKind::Resources) {
889            return Err(vec![TypedError::PropertyUnexpectedKind {
890                expected: PropertyKind::Resources,
891                found: prop.kind,
892                span: prop.span,
893            }]);
894        }
895
896        let mut errors = Vec::new();
897        let mut language = None;
898        let mut altrep = None;
899        let mut x_parameters = Vec::new();
900        let mut retained_parameters = Vec::new();
901
902        for param in prop.parameters {
903            match param {
904                p @ Parameter::Language { .. } if language.is_some() => {
905                    errors.push(TypedError::ParameterDuplicated {
906                        span: p.span(),
907                        parameter: p.kind().into(),
908                    });
909                }
910                Parameter::Language { value, .. } => language = Some(value),
911
912                p @ Parameter::AlternateText { .. } if altrep.is_some() => {
913                    errors.push(TypedError::ParameterDuplicated {
914                        span: p.span(),
915                        parameter: p.kind().into(),
916                    });
917                }
918                Parameter::AlternateText { value, .. } => altrep = Some(value),
919
920                Parameter::XName(raw) => x_parameters.push(raw),
921                p @ Parameter::Unrecognized { .. } => retained_parameters.push(p),
922                p => {
923                    // Preserve other parameters not used by this property for round-trip
924                    retained_parameters.push(p);
925                }
926            }
927        }
928
929        if !errors.is_empty() {
930            return Err(errors);
931        }
932
933        let Value::Text { values, .. } = prop.value else {
934            const EXPECTED: &[ValueType<String>] = &[ValueType::Text];
935            let span = prop.value.span();
936            return Err(vec![TypedError::PropertyUnexpectedValue {
937                property: prop.kind,
938                expected: EXPECTED,
939                found: prop.value.kind().into(),
940                span,
941            }]);
942        };
943
944        Ok(Self {
945            values,
946            language,
947            altrep,
948            x_parameters,
949            retained_parameters,
950            span: prop.span,
951        })
952    }
953}
954
955impl Resources<Segments<'_>> {
956    /// Convert borrowed `Resources` to owned `Resources`
957    #[must_use]
958    pub fn to_owned(&self) -> Resources<String> {
959        Resources {
960            values: self.values.iter().map(ValueText::to_owned).collect(),
961            language: self.language.as_ref().map(Segments::to_owned),
962            altrep: self.altrep.as_ref().map(Segments::to_owned),
963            x_parameters: self
964                .x_parameters
965                .iter()
966                .map(RawParameter::to_owned)
967                .collect(),
968            retained_parameters: self
969                .retained_parameters
970                .iter()
971                .map(Parameter::to_owned)
972                .collect(),
973            span: (),
974        }
975    }
976}
977
978simple_property_wrapper!(
979    /// Simple text property wrapper (RFC 5545 Section 3.8.1.12)
980    pub Summary<S> => Text
981);
982
983impl Summary<String> {
984    /// Create a new `Summary<String>` from a string value.
985    #[must_use]
986    pub fn new(value: String) -> Self {
987        Self {
988            inner: Text::new(value),
989            span: (),
990        }
991    }
992}
993
994/// Create a parser input from `ValueText` with proper span tracking.
995///
996/// This function creates a properly-spanned input stream from a `ValueText`,
997/// enabling accurate error reporting during parsing.
998fn make_input(text: ValueText<Segments<'_>>) -> impl Input<'_, Token = char, Span = SimpleSpan> {
999    // Get EOI span directly from the ValueText without iteration
1000    let eoi = text.span().into();
1001
1002    // Create the parser stream
1003    Stream::from_iter(text.into_spanned_chars().map(|(c, s)| (c, s.into())))
1004        .map(eoi, |(c, s)| (c, s))
1005}