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