1use core::{error, fmt, ops, str};
22
23use alloc::{
24 borrow::Cow,
25 string::{String, ToString},
26 vec::Vec,
27};
28
29use crate::{
30 param::IcalParam,
31 value::{IcalValue, owned},
32};
33
34#[derive(Debug)]
36pub struct ParseIcalPropKindError(
37 String,
39);
40
41impl fmt::Display for ParseIcalPropKindError {
42 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
43 write!(f, "Cannot parse iCalendar property `{}`", self.0)
44 }
45}
46
47impl error::Error for ParseIcalPropKindError {}
48
49#[derive(Clone, Debug, PartialEq, Eq)]
51pub struct IcalProp<'a> {
52 pub name: IcalPropName<'a>,
54 pub params: Vec<IcalParam<'a>>,
56 pub value: IcalValue<'a>,
58}
59
60#[derive(Clone, Debug, PartialEq, Eq)]
65pub enum IcalPropName<'a> {
66 Kind(IcalPropKind),
68 Unknown(Cow<'a, str>),
70}
71
72impl ops::Deref for IcalPropName<'_> {
73 type Target = str;
74
75 fn deref(&self) -> &Self::Target {
78 match self {
79 Self::Kind(kind) => kind,
80 Self::Unknown(name) => name,
81 }
82 }
83}
84
85impl From<IcalPropKind> for IcalPropName<'_> {
86 fn from(kind: IcalPropKind) -> Self {
87 Self::Kind(kind)
88 }
89}
90
91impl From<&IcalPropKind> for IcalPropName<'_> {
92 fn from(kind: &IcalPropKind) -> Self {
93 Self::Kind(*kind)
94 }
95}
96
97impl<'a> From<Cow<'a, str>> for IcalPropName<'a> {
98 fn from(name: Cow<'a, str>) -> Self {
99 match name.parse().ok() {
100 Some(kind) => Self::Kind(kind),
101 None => Self::Unknown(name),
102 }
103 }
104}
105
106impl<'a> From<&'a str> for IcalPropName<'a> {
107 fn from(name: &'a str) -> Self {
108 Cow::Borrowed(name).into()
109 }
110}
111
112impl IcalPropName<'_> {
113 pub fn into_owned(self) -> IcalPropName<'static> {
116 match self {
117 Self::Kind(kind) => IcalPropName::Kind(kind),
118 Self::Unknown(name) => IcalPropName::Unknown(owned(name)),
119 }
120 }
121}
122
123impl IcalProp<'_> {
124 pub fn into_owned(self) -> IcalProp<'static> {
128 IcalProp {
129 name: self.name.into_owned(),
130 params: self.params.into_iter().map(IcalParam::into_owned).collect(),
131 value: self.value.into_owned(),
132 }
133 }
134}
135
136#[derive(Clone, Copy, Debug, PartialEq, Eq)]
142pub enum IcalPropKind {
143 CalScale,
145 Method,
147 ProdId,
149 Attach,
151 Categories,
153 Class,
155 Comment,
157 Description,
159 Geo,
161 Location,
163 PercentComplete,
165 Priority,
167 Resources,
169 Status,
171 Summary,
173 Completed,
175 DtEnd,
177 Due,
179 DtStart,
181 Duration,
183 FreeBusy,
185 Transp,
187 TzId,
189 TzName,
191 TzOffsetFrom,
193 TzOffsetTo,
195 TzUrl,
197 Attendee,
199 Contact,
201 Organizer,
203 RecurrenceId,
205 RelatedTo,
207 Url,
209 Uid,
211 ExDate,
213 RDate,
215 RRule,
217 ExRule,
219 Action,
221 Repeat,
223 Trigger,
225 Created,
227 DtStamp,
229 LastModified,
231 Sequence,
233 RequestStatus,
235 Name,
237 RefreshInterval,
239 Source,
241 Color,
243 Image,
245 Conference,
247 ParticipantType,
249 ResourceType,
251 CalendarAddress,
253 LocationType,
255 StructuredData,
257 Link,
259 Refid,
261 Concept,
263 BusyType,
265 StyledDescription,
267 Acknowledged,
269 Proximity,
271 Tz,
273 AAlarm,
275 DAlarm,
277 MAlarm,
279 PAlarm,
281 RNum,
283}
284
285impl IcalPropKind {
286 pub const ALL: [Self; 70] = [
289 Self::CalScale,
290 Self::Method,
291 Self::ProdId,
292 Self::Attach,
293 Self::Categories,
294 Self::Class,
295 Self::Comment,
296 Self::Description,
297 Self::Geo,
298 Self::Location,
299 Self::PercentComplete,
300 Self::Priority,
301 Self::Resources,
302 Self::Status,
303 Self::Summary,
304 Self::Completed,
305 Self::DtEnd,
306 Self::Due,
307 Self::DtStart,
308 Self::Duration,
309 Self::FreeBusy,
310 Self::Transp,
311 Self::TzId,
312 Self::TzName,
313 Self::TzOffsetFrom,
314 Self::TzOffsetTo,
315 Self::TzUrl,
316 Self::Attendee,
317 Self::Contact,
318 Self::Organizer,
319 Self::RecurrenceId,
320 Self::RelatedTo,
321 Self::Url,
322 Self::Uid,
323 Self::ExDate,
324 Self::RDate,
325 Self::RRule,
326 Self::ExRule,
327 Self::Action,
328 Self::Repeat,
329 Self::Trigger,
330 Self::Created,
331 Self::DtStamp,
332 Self::LastModified,
333 Self::Sequence,
334 Self::RequestStatus,
335 Self::Name,
336 Self::RefreshInterval,
337 Self::Source,
338 Self::Color,
339 Self::Image,
340 Self::Conference,
341 Self::ParticipantType,
342 Self::ResourceType,
343 Self::CalendarAddress,
344 Self::LocationType,
345 Self::StructuredData,
346 Self::Link,
347 Self::Refid,
348 Self::Concept,
349 Self::BusyType,
350 Self::StyledDescription,
351 Self::Acknowledged,
352 Self::Proximity,
353 Self::Tz,
354 Self::AAlarm,
355 Self::DAlarm,
356 Self::MAlarm,
357 Self::PAlarm,
358 Self::RNum,
359 ];
360}
361
362impl str::FromStr for IcalPropKind {
363 type Err = ParseIcalPropKindError;
364
365 fn from_str(kind: &str) -> Result<Self, Self::Err> {
367 let kind = match kind {
368 kind if kind.eq_ignore_ascii_case("CALSCALE") => Self::CalScale,
369 kind if kind.eq_ignore_ascii_case("METHOD") => Self::Method,
370 kind if kind.eq_ignore_ascii_case("PRODID") => Self::ProdId,
371 kind if kind.eq_ignore_ascii_case("ATTACH") => Self::Attach,
372 kind if kind.eq_ignore_ascii_case("CATEGORIES") => Self::Categories,
373 kind if kind.eq_ignore_ascii_case("CLASS") => Self::Class,
374 kind if kind.eq_ignore_ascii_case("COMMENT") => Self::Comment,
375 kind if kind.eq_ignore_ascii_case("DESCRIPTION") => Self::Description,
376 kind if kind.eq_ignore_ascii_case("GEO") => Self::Geo,
377 kind if kind.eq_ignore_ascii_case("LOCATION") => Self::Location,
378 kind if kind.eq_ignore_ascii_case("PERCENT-COMPLETE") => Self::PercentComplete,
379 kind if kind.eq_ignore_ascii_case("PRIORITY") => Self::Priority,
380 kind if kind.eq_ignore_ascii_case("RESOURCES") => Self::Resources,
381 kind if kind.eq_ignore_ascii_case("STATUS") => Self::Status,
382 kind if kind.eq_ignore_ascii_case("SUMMARY") => Self::Summary,
383 kind if kind.eq_ignore_ascii_case("COMPLETED") => Self::Completed,
384 kind if kind.eq_ignore_ascii_case("DTEND") => Self::DtEnd,
385 kind if kind.eq_ignore_ascii_case("DUE") => Self::Due,
386 kind if kind.eq_ignore_ascii_case("DTSTART") => Self::DtStart,
387 kind if kind.eq_ignore_ascii_case("DURATION") => Self::Duration,
388 kind if kind.eq_ignore_ascii_case("FREEBUSY") => Self::FreeBusy,
389 kind if kind.eq_ignore_ascii_case("TRANSP") => Self::Transp,
390 kind if kind.eq_ignore_ascii_case("TZID") => Self::TzId,
391 kind if kind.eq_ignore_ascii_case("TZNAME") => Self::TzName,
392 kind if kind.eq_ignore_ascii_case("TZOFFSETFROM") => Self::TzOffsetFrom,
393 kind if kind.eq_ignore_ascii_case("TZOFFSETTO") => Self::TzOffsetTo,
394 kind if kind.eq_ignore_ascii_case("TZURL") => Self::TzUrl,
395 kind if kind.eq_ignore_ascii_case("ATTENDEE") => Self::Attendee,
396 kind if kind.eq_ignore_ascii_case("CONTACT") => Self::Contact,
397 kind if kind.eq_ignore_ascii_case("ORGANIZER") => Self::Organizer,
398 kind if kind.eq_ignore_ascii_case("RECURRENCE-ID") => Self::RecurrenceId,
399 kind if kind.eq_ignore_ascii_case("RELATED-TO") => Self::RelatedTo,
400 kind if kind.eq_ignore_ascii_case("URL") => Self::Url,
401 kind if kind.eq_ignore_ascii_case("UID") => Self::Uid,
402 kind if kind.eq_ignore_ascii_case("EXDATE") => Self::ExDate,
403 kind if kind.eq_ignore_ascii_case("RDATE") => Self::RDate,
404 kind if kind.eq_ignore_ascii_case("RRULE") => Self::RRule,
405 kind if kind.eq_ignore_ascii_case("EXRULE") => Self::ExRule,
406 kind if kind.eq_ignore_ascii_case("ACTION") => Self::Action,
407 kind if kind.eq_ignore_ascii_case("REPEAT") => Self::Repeat,
408 kind if kind.eq_ignore_ascii_case("TRIGGER") => Self::Trigger,
409 kind if kind.eq_ignore_ascii_case("CREATED") => Self::Created,
410 kind if kind.eq_ignore_ascii_case("DTSTAMP") => Self::DtStamp,
411 kind if kind.eq_ignore_ascii_case("LAST-MODIFIED") => Self::LastModified,
412 kind if kind.eq_ignore_ascii_case("SEQUENCE") => Self::Sequence,
413 kind if kind.eq_ignore_ascii_case("REQUEST-STATUS") => Self::RequestStatus,
414 kind if kind.eq_ignore_ascii_case("NAME") => Self::Name,
415 kind if kind.eq_ignore_ascii_case("REFRESH-INTERVAL") => Self::RefreshInterval,
416 kind if kind.eq_ignore_ascii_case("SOURCE") => Self::Source,
417 kind if kind.eq_ignore_ascii_case("COLOR") => Self::Color,
418 kind if kind.eq_ignore_ascii_case("IMAGE") => Self::Image,
419 kind if kind.eq_ignore_ascii_case("CONFERENCE") => Self::Conference,
420 kind if kind.eq_ignore_ascii_case("PARTICIPANT-TYPE") => Self::ParticipantType,
421 kind if kind.eq_ignore_ascii_case("RESOURCE-TYPE") => Self::ResourceType,
422 kind if kind.eq_ignore_ascii_case("CALENDAR-ADDRESS") => Self::CalendarAddress,
423 kind if kind.eq_ignore_ascii_case("LOCATION-TYPE") => Self::LocationType,
424 kind if kind.eq_ignore_ascii_case("STRUCTURED-DATA") => Self::StructuredData,
425 kind if kind.eq_ignore_ascii_case("LINK") => Self::Link,
426 kind if kind.eq_ignore_ascii_case("REFID") => Self::Refid,
427 kind if kind.eq_ignore_ascii_case("CONCEPT") => Self::Concept,
428 kind if kind.eq_ignore_ascii_case("BUSYTYPE") => Self::BusyType,
429 kind if kind.eq_ignore_ascii_case("STYLED-DESCRIPTION") => Self::StyledDescription,
430 kind if kind.eq_ignore_ascii_case("ACKNOWLEDGED") => Self::Acknowledged,
431 kind if kind.eq_ignore_ascii_case("PROXIMITY") => Self::Proximity,
432 kind if kind.eq_ignore_ascii_case("TZ") => Self::Tz,
433 kind if kind.eq_ignore_ascii_case("AALARM") => Self::AAlarm,
434 kind if kind.eq_ignore_ascii_case("DALARM") => Self::DAlarm,
435 kind if kind.eq_ignore_ascii_case("MALARM") => Self::MAlarm,
436 kind if kind.eq_ignore_ascii_case("PALARM") => Self::PAlarm,
437 kind if kind.eq_ignore_ascii_case("RNUM") => Self::RNum,
438 _ => return Err(ParseIcalPropKindError(kind.to_string())),
439 };
440
441 Ok(kind)
442 }
443}
444
445impl ops::Deref for IcalPropKind {
446 type Target = str;
447
448 fn deref(&self) -> &Self::Target {
449 match self {
450 Self::CalScale => "CALSCALE",
451 Self::Method => "METHOD",
452 Self::ProdId => "PRODID",
453 Self::Attach => "ATTACH",
454 Self::Categories => "CATEGORIES",
455 Self::Class => "CLASS",
456 Self::Comment => "COMMENT",
457 Self::Description => "DESCRIPTION",
458 Self::Geo => "GEO",
459 Self::Location => "LOCATION",
460 Self::PercentComplete => "PERCENT-COMPLETE",
461 Self::Priority => "PRIORITY",
462 Self::Resources => "RESOURCES",
463 Self::Status => "STATUS",
464 Self::Summary => "SUMMARY",
465 Self::Completed => "COMPLETED",
466 Self::DtEnd => "DTEND",
467 Self::Due => "DUE",
468 Self::DtStart => "DTSTART",
469 Self::Duration => "DURATION",
470 Self::FreeBusy => "FREEBUSY",
471 Self::Transp => "TRANSP",
472 Self::TzId => "TZID",
473 Self::TzName => "TZNAME",
474 Self::TzOffsetFrom => "TZOFFSETFROM",
475 Self::TzOffsetTo => "TZOFFSETTO",
476 Self::TzUrl => "TZURL",
477 Self::Attendee => "ATTENDEE",
478 Self::Contact => "CONTACT",
479 Self::Organizer => "ORGANIZER",
480 Self::RecurrenceId => "RECURRENCE-ID",
481 Self::RelatedTo => "RELATED-TO",
482 Self::Url => "URL",
483 Self::Uid => "UID",
484 Self::ExDate => "EXDATE",
485 Self::RDate => "RDATE",
486 Self::RRule => "RRULE",
487 Self::ExRule => "EXRULE",
488 Self::Action => "ACTION",
489 Self::Repeat => "REPEAT",
490 Self::Trigger => "TRIGGER",
491 Self::Created => "CREATED",
492 Self::DtStamp => "DTSTAMP",
493 Self::LastModified => "LAST-MODIFIED",
494 Self::Sequence => "SEQUENCE",
495 Self::RequestStatus => "REQUEST-STATUS",
496 Self::Name => "NAME",
497 Self::RefreshInterval => "REFRESH-INTERVAL",
498 Self::Source => "SOURCE",
499 Self::Color => "COLOR",
500 Self::Image => "IMAGE",
501 Self::Conference => "CONFERENCE",
502 Self::ParticipantType => "PARTICIPANT-TYPE",
503 Self::ResourceType => "RESOURCE-TYPE",
504 Self::CalendarAddress => "CALENDAR-ADDRESS",
505 Self::LocationType => "LOCATION-TYPE",
506 Self::StructuredData => "STRUCTURED-DATA",
507 Self::Link => "LINK",
508 Self::Refid => "REFID",
509 Self::Concept => "CONCEPT",
510 Self::BusyType => "BUSYTYPE",
511 Self::StyledDescription => "STYLED-DESCRIPTION",
512 Self::Acknowledged => "ACKNOWLEDGED",
513 Self::Proximity => "PROXIMITY",
514 Self::Tz => "TZ",
515 Self::AAlarm => "AALARM",
516 Self::DAlarm => "DALARM",
517 Self::MAlarm => "MALARM",
518 Self::PAlarm => "PALARM",
519 Self::RNum => "RNUM",
520 }
521 }
522}
523
524#[cfg(test)]
525mod tests {
526 use core::str::FromStr;
527
528 use alloc::borrow::Cow;
529
530 use crate::{
531 param::IcalParam,
532 prop::{IcalProp, IcalPropKind, IcalPropName},
533 value::{IcalValue, text::IcalText},
534 };
535
536 #[test]
537 fn names_the_property_and_wraps_the_value() {
538 let prop = IcalProp {
539 name: IcalPropKind::Summary.into(),
540 params: [].into(),
541 value: IcalValue::Text(IcalText(Cow::Borrowed("Lunch"))),
542 };
543 assert_eq!(prop.name, IcalPropName::Kind(IcalPropKind::Summary));
544 assert_eq!(&*prop.name, "SUMMARY");
545 assert!(prop.params.is_empty());
546 }
547
548 #[test]
549 fn carries_the_given_parameters() {
550 let prop = IcalProp {
551 name: IcalPropKind::Attendee.into(),
552 params: [IcalParam::Role(Cow::Borrowed("CHAIR"))].into(),
553 value: IcalValue::CalAddress("mailto:a@b.example".into()),
554 };
555 assert_eq!(&*prop.name, "ATTENDEE");
556 assert_eq!(prop.params.len(), 1);
557 }
558
559 #[test]
560 fn round_trips_every_kind_through_its_wire_name() {
561 for kind in IcalPropKind::ALL {
562 assert_eq!(IcalPropKind::from_str(&kind).ok(), Some(kind));
563 }
564 assert_eq!(
567 IcalPropKind::from_str("summary").ok(),
568 Some(IcalPropKind::Summary),
569 );
570 assert!(IcalPropKind::from_str("X-CUSTOM").is_err());
571 }
572}