aimcal_ical/property/
alarm.rs

1// SPDX-FileCopyrightText: 2025-2026 Zexin Yuan <aim@yzx9.xyz>
2//
3// SPDX-License-Identifier: Apache-2.0
4
5//! Alarm Component Properties (RFC 5545 Section 3.8.6)
6//!
7//! This module contains property types for the "Alarm Component Properties"
8//! section of RFC 5545. All types implement `kind()` methods and validate
9//! their property kind during conversion from `ParsedProperty`:
10//!
11//! - 3.8.6.1: `Action` - Alarm action type (AUDIO, DISPLAY, EMAIL)
12//! - 3.8.6.2: `Repeat` - Alarm repeat count
13//! - 3.8.6.3: `Trigger` - Alarm trigger time or duration
14//!   - `TriggerValue` - Trigger value variant (duration or date-time)
15
16use std::convert::TryFrom;
17
18use crate::keyword::{KW_ACTION_AUDIO, KW_ACTION_DISPLAY, KW_ACTION_EMAIL, KW_ACTION_PROCEDURE};
19use crate::parameter::{AlarmTriggerRelationship, Parameter, ValueType};
20use crate::property::common::{take_single_text, take_single_value};
21use crate::property::{DateTime, PropertyKind};
22use crate::string_storage::{Segments, StringStorage};
23use crate::syntax::RawParameter;
24use crate::typed::{ParsedProperty, TypedError};
25use crate::value::{Value, ValueDuration};
26
27define_prop_value_enum! {
28    /// Alarm action value (RFC 5545 Section 3.8.6.1)
29    pub enum ActionValue {
30        /// Audio alarm
31        Audio => KW_ACTION_AUDIO,
32        /// Display alarm
33        Display => KW_ACTION_DISPLAY,
34        /// Email alarm
35        Email => KW_ACTION_EMAIL,
36    }
37}
38
39impl ActionValue {
40    /// Get the keyword string for this action value
41    #[must_use]
42    pub fn as_str(&self) -> &str {
43        match self {
44            Self::Audio => KW_ACTION_AUDIO,
45            Self::Display => KW_ACTION_DISPLAY,
46            Self::Email => KW_ACTION_EMAIL,
47        }
48    }
49}
50
51impl AsRef<str> for ActionValue {
52    fn as_ref(&self) -> &str {
53        self.as_str()
54    }
55}
56
57/// Alarm action (RFC 5545 Section 3.8.6.1)
58#[derive(Debug, Clone)]
59pub struct Action<S: StringStorage> {
60    /// Action value
61    pub value: ActionValue,
62    /// X-name parameters (custom experimental parameters)
63    pub x_parameters: Vec<RawParameter<S>>,
64    /// Unrecognized / Non-standard parameters (preserved for round-trip)
65    pub retained_parameters: Vec<Parameter<S>>,
66    /// Span of the property in the source
67    pub span: S::Span,
68}
69
70impl<'src> TryFrom<ParsedProperty<'src>> for Action<Segments<'src>> {
71    type Error = Vec<TypedError<'src>>;
72
73    fn try_from(prop: ParsedProperty<'src>) -> Result<Self, Self::Error> {
74        if !matches!(prop.kind, PropertyKind::Action) {
75            return Err(vec![TypedError::PropertyUnexpectedKind {
76                expected: PropertyKind::Action,
77                found: prop.kind,
78                span: prop.span,
79            }]);
80        }
81
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                Parameter::XName(raw) => x_parameters.push(raw),
88                p @ Parameter::Unrecognized { .. } => retained_parameters.push(p),
89                p => {
90                    // Preserve other parameters not used by this property for round-trip
91                    retained_parameters.push(p);
92                }
93            }
94        }
95
96        let value_span = prop.value.span();
97        let text = take_single_text(&PropertyKind::Action, prop.value)?;
98
99        // Check for deprecated PROCEDURE action first
100        if text.eq_str_ignore_ascii_case(KW_ACTION_PROCEDURE) {
101            return Err(vec![TypedError::PropertyInvalidValue {
102                property: PropertyKind::Action,
103                value: format!("{KW_ACTION_PROCEDURE} action has been deprecated"),
104                span: value_span,
105            }]);
106        }
107
108        let value = text.try_into().map_err(|text| {
109            vec![TypedError::PropertyInvalidValue {
110                property: PropertyKind::Action,
111                value: format!("Invalid alarm action: {text}"),
112                span: value_span,
113            }]
114        })?;
115
116        Ok(Action {
117            value,
118            x_parameters,
119            retained_parameters,
120            span: prop.span,
121        })
122    }
123}
124
125impl Action<Segments<'_>> {
126    /// Convert borrowed Action to owned Action
127    #[must_use]
128    pub fn to_owned(&self) -> Action<String> {
129        Action {
130            value: self.value,
131            x_parameters: self
132                .x_parameters
133                .iter()
134                .map(RawParameter::to_owned)
135                .collect(),
136            retained_parameters: self
137                .retained_parameters
138                .iter()
139                .map(Parameter::to_owned)
140                .collect(),
141            span: (),
142        }
143    }
144}
145
146/// Repeat Count (RFC 5545 Section 3.8.6.2)
147///
148/// This property defines the number of times the alarm should repeat.
149#[derive(Debug, Clone)]
150pub struct Repeat<S: StringStorage> {
151    /// Number of repetitions
152    pub value: u32,
153    /// X-name parameters (custom experimental parameters)
154    pub x_parameters: Vec<RawParameter<S>>,
155    /// Unrecognized / Non-standard parameters (preserved for round-trip)
156    pub retained_parameters: Vec<Parameter<S>>,
157    /// Span of the property in the source
158    pub span: S::Span,
159}
160
161impl<'src> TryFrom<ParsedProperty<'src>> for Repeat<Segments<'src>> {
162    type Error = Vec<TypedError<'src>>;
163
164    #[allow(clippy::cast_sign_loss)]
165    fn try_from(prop: ParsedProperty<'src>) -> Result<Self, Self::Error> {
166        if !matches!(prop.kind, PropertyKind::Repeat) {
167            return Err(vec![TypedError::PropertyUnexpectedKind {
168                expected: PropertyKind::Repeat,
169                found: prop.kind,
170                span: prop.span,
171            }]);
172        }
173
174        let mut x_parameters = Vec::new();
175        let mut retained_parameters = Vec::new();
176
177        for param in prop.parameters {
178            match param {
179                Parameter::XName(raw) => x_parameters.push(raw),
180                p @ Parameter::Unrecognized { .. } => retained_parameters.push(p),
181                p => {
182                    // Preserve other parameters not used by this property for round-trip
183                    retained_parameters.push(p);
184                }
185            }
186        }
187
188        let value_span = prop.value.span();
189        match prop.value {
190            Value::Integer { values, .. } if values.is_empty() => {
191                Err(vec![TypedError::PropertyMissingValue {
192                    property: prop.kind,
193                    span: prop.span,
194                }])
195            }
196            Value::Integer {
197                values: mut ints, ..
198            } if ints.len() == 1 => {
199                let i = ints.pop().unwrap();
200                if i >= 0 {
201                    Ok(Repeat {
202                        value: i as u32, // SAFETY: i < i32::MAX < u32::MAX
203                        x_parameters,
204                        retained_parameters,
205                        span: prop.span,
206                    })
207                } else {
208                    Err(vec![TypedError::PropertyInvalidValue {
209                        property: prop.kind,
210                        value: format!("Repeat count must be non-negative: {i}"),
211                        span: value_span,
212                    }])
213                }
214            }
215            Value::Integer { values: ints, .. } => {
216                Err(vec![TypedError::PropertyInvalidValueCount {
217                    property: PropertyKind::Repeat,
218                    expected: 1,
219                    found: ints.len(),
220                    span: value_span,
221                }])
222            }
223            v => {
224                const EXPECTED: &[ValueType<String>] = &[ValueType::Integer];
225                let span = v.span();
226                Err(vec![TypedError::PropertyUnexpectedValue {
227                    property: prop.kind,
228                    expected: EXPECTED,
229                    found: v.kind().into(),
230                    span,
231                }])
232            }
233        }
234    }
235}
236
237impl Repeat<Segments<'_>> {
238    /// Convert borrowed Repeat to owned Repeat
239    #[must_use]
240    pub fn to_owned(&self) -> Repeat<String> {
241        Repeat {
242            value: self.value,
243            x_parameters: self
244                .x_parameters
245                .iter()
246                .map(RawParameter::to_owned)
247                .collect(),
248            retained_parameters: self
249                .retained_parameters
250                .iter()
251                .map(Parameter::to_owned)
252                .collect(),
253            span: (),
254        }
255    }
256}
257
258/// Trigger for alarms (RFC 5545 Section 3.8.6.3)
259#[derive(Debug, Clone)]
260pub struct Trigger<S: StringStorage> {
261    /// When to trigger (relative or absolute)
262    pub value: TriggerValue<S>,
263    /// Related parameter for relative triggers
264    pub related: Option<AlarmTriggerRelationship>,
265    /// X-name parameters (custom experimental parameters)
266    pub x_parameters: Vec<RawParameter<S>>,
267    /// Unrecognized / Non-standard parameters (preserved for round-trip)
268    pub retained_parameters: Vec<Parameter<S>>,
269    /// Span of the property in the source
270    pub span: S::Span,
271}
272
273/// Trigger value (relative duration or absolute date/time)
274#[derive(Debug, Clone)]
275pub enum TriggerValue<S: StringStorage> {
276    /// Relative duration before/after the event
277    Duration(ValueDuration),
278    /// Absolute date/time
279    DateTime(DateTime<S>),
280}
281
282impl<'src> TryFrom<ParsedProperty<'src>> for Trigger<Segments<'src>> {
283    type Error = Vec<TypedError<'src>>;
284
285    fn try_from(prop: ParsedProperty<'src>) -> Result<Self, Self::Error> {
286        if !matches!(prop.kind, PropertyKind::Trigger) {
287            return Err(vec![TypedError::PropertyUnexpectedKind {
288                expected: PropertyKind::Trigger,
289                found: prop.kind,
290                span: prop.span,
291            }]);
292        }
293
294        let mut errors = Vec::new();
295
296        // Collect the RELATED parameter (optional, default is START)
297        let mut related = None;
298        let mut x_parameters = Vec::new();
299        let mut retained_parameters = Vec::new();
300
301        for param in prop.parameters {
302            match param {
303                p @ Parameter::AlarmTriggerRelationship { .. } if related.is_some() => {
304                    errors.push(TypedError::ParameterDuplicated {
305                        span: p.span(),
306                        parameter: p.kind().into(),
307                    });
308                }
309                Parameter::AlarmTriggerRelationship { value, .. } => related = Some(value),
310
311                Parameter::XName(raw) => x_parameters.push(raw),
312                p @ Parameter::Unrecognized { .. } => retained_parameters.push(p),
313                p => {
314                    // Preserve other parameters not used by this property for round-trip
315                    retained_parameters.push(p);
316                }
317            }
318        }
319
320        let value = take_single_value(&PropertyKind::Trigger, prop.value)?;
321
322        // Return all errors if any occurred
323        if !errors.is_empty() {
324            return Err(errors);
325        }
326
327        match value {
328            Value::Duration { values: durs, .. } if durs.len() == 1 => Ok(Trigger {
329                value: TriggerValue::Duration(durs.into_iter().next().unwrap()),
330                related: Some(related.unwrap_or(AlarmTriggerRelationship::Start)),
331                x_parameters,
332                retained_parameters,
333                span: prop.span,
334            }),
335            Value::DateTime { values: dts, .. } if dts.len() == 1 => {
336                let dt = dts.into_iter().next().unwrap();
337                Ok(Trigger {
338                    value: TriggerValue::DateTime(DateTime::Floating {
339                        date: dt.date,
340                        time: dt.time.into(),
341                        x_parameters: Vec::new(),
342                        retained_parameters: Vec::new(),
343                    }),
344                    related: None,
345                    x_parameters,
346                    retained_parameters,
347                    span: prop.span,
348                })
349            }
350            _ => Err(vec![TypedError::PropertyInvalidValue {
351                property: PropertyKind::Trigger,
352                value: "Expected duration or date-time value".to_string(),
353                span: value.span(),
354            }]),
355        }
356    }
357}
358
359impl Trigger<Segments<'_>> {
360    /// Convert borrowed Trigger to owned Trigger
361    #[must_use]
362    pub fn to_owned(&self) -> Trigger<String> {
363        Trigger {
364            value: self.value.to_owned(),
365            related: self.related,
366            x_parameters: self
367                .x_parameters
368                .iter()
369                .map(RawParameter::to_owned)
370                .collect(),
371            retained_parameters: self
372                .retained_parameters
373                .iter()
374                .map(Parameter::to_owned)
375                .collect(),
376            span: (),
377        }
378    }
379}
380
381impl TriggerValue<Segments<'_>> {
382    /// Convert borrowed `TriggerValue` to owned `TriggerValue`
383    #[must_use]
384    pub fn to_owned(&self) -> TriggerValue<String> {
385        match self {
386            TriggerValue::Duration(duration) => TriggerValue::Duration(*duration),
387            TriggerValue::DateTime(dt) => TriggerValue::DateTime(dt.to_owned()),
388        }
389    }
390}