lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README | LICENSE

calendar.rs (8714B)


      1 #[cfg(not(feature = "std"))]
      2 use alloc::{string::String, vec::Vec};
      3 
      4 use crate::social::{
      5     RadrootsCalendarDateValue, RadrootsCalendarEventFreeBusy, RadrootsCalendarEventRsvpStatus,
      6     RadrootsCalendarParticipant, RadrootsSocialLocation, RadrootsSocialTarget,
      7 };
      8 
      9 #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
     10 #[derive(Clone, Debug)]
     11 pub struct RadrootsCalendar {
     12     pub d_tag: String,
     13     pub title: String,
     14     pub events: Vec<RadrootsSocialTarget>,
     15     #[cfg_attr(
     16         feature = "serde",
     17         serde(default, skip_serializing_if = "Option::is_none")
     18     )]
     19     pub description: Option<String>,
     20     #[cfg_attr(
     21         feature = "serde",
     22         serde(default, skip_serializing_if = "Option::is_none")
     23     )]
     24     pub summary: Option<String>,
     25     #[cfg_attr(
     26         feature = "serde",
     27         serde(default, skip_serializing_if = "Option::is_none")
     28     )]
     29     pub image: Option<String>,
     30 }
     31 
     32 #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
     33 #[derive(Clone, Debug)]
     34 pub struct RadrootsCalendarDateEvent {
     35     pub d_tag: String,
     36     pub title: String,
     37     pub start: String,
     38     #[cfg_attr(
     39         feature = "serde",
     40         serde(default, skip_serializing_if = "Option::is_none")
     41     )]
     42     pub description: Option<String>,
     43     #[cfg_attr(
     44         feature = "serde",
     45         serde(default, skip_serializing_if = "Option::is_none")
     46     )]
     47     pub end: Option<String>,
     48     #[cfg_attr(
     49         feature = "serde",
     50         serde(default, skip_serializing_if = "Option::is_none")
     51     )]
     52     pub days: Option<Vec<RadrootsCalendarDateValue>>,
     53     #[cfg_attr(
     54         feature = "serde",
     55         serde(default, skip_serializing_if = "Option::is_none")
     56     )]
     57     pub location: Option<RadrootsSocialLocation>,
     58     #[cfg_attr(
     59         feature = "serde",
     60         serde(default, skip_serializing_if = "Option::is_none")
     61     )]
     62     pub summary: Option<String>,
     63     #[cfg_attr(
     64         feature = "serde",
     65         serde(default, skip_serializing_if = "Option::is_none")
     66     )]
     67     pub image: Option<String>,
     68     #[cfg_attr(
     69         feature = "serde",
     70         serde(default, skip_serializing_if = "Option::is_none")
     71     )]
     72     pub participants: Option<Vec<RadrootsCalendarParticipant>>,
     73 }
     74 
     75 #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
     76 #[derive(Clone, Debug)]
     77 pub struct RadrootsCalendarTimeEvent {
     78     pub d_tag: String,
     79     pub title: String,
     80     pub start: u64,
     81     #[cfg_attr(
     82         feature = "serde",
     83         serde(default, skip_serializing_if = "Vec::is_empty")
     84     )]
     85     pub dates: Vec<RadrootsCalendarDateValue>,
     86     #[cfg_attr(
     87         feature = "serde",
     88         serde(default, skip_serializing_if = "Option::is_none")
     89     )]
     90     pub description: Option<String>,
     91     #[cfg_attr(
     92         feature = "serde",
     93         serde(default, skip_serializing_if = "Option::is_none")
     94     )]
     95     pub end: Option<u64>,
     96     #[cfg_attr(
     97         feature = "serde",
     98         serde(default, skip_serializing_if = "Option::is_none")
     99     )]
    100     pub start_tzid: Option<String>,
    101     #[cfg_attr(
    102         feature = "serde",
    103         serde(default, skip_serializing_if = "Option::is_none")
    104     )]
    105     pub end_tzid: Option<String>,
    106     #[cfg_attr(
    107         feature = "serde",
    108         serde(default, skip_serializing_if = "Option::is_none")
    109     )]
    110     pub location: Option<RadrootsSocialLocation>,
    111     #[cfg_attr(
    112         feature = "serde",
    113         serde(default, skip_serializing_if = "Option::is_none")
    114     )]
    115     pub summary: Option<String>,
    116     #[cfg_attr(
    117         feature = "serde",
    118         serde(default, skip_serializing_if = "Option::is_none")
    119     )]
    120     pub image: Option<String>,
    121     #[cfg_attr(
    122         feature = "serde",
    123         serde(default, skip_serializing_if = "Option::is_none")
    124     )]
    125     pub participants: Option<Vec<RadrootsCalendarParticipant>>,
    126 }
    127 
    128 #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
    129 #[derive(Clone, Debug)]
    130 pub struct RadrootsCalendarEventRsvp {
    131     pub d_tag: String,
    132     pub event: RadrootsSocialTarget,
    133     #[cfg_attr(
    134         feature = "serde",
    135         serde(default, skip_serializing_if = "Option::is_none")
    136     )]
    137     pub event_id: Option<String>,
    138     pub status: RadrootsCalendarEventRsvpStatus,
    139     #[cfg_attr(
    140         feature = "serde",
    141         serde(default, skip_serializing_if = "Option::is_none")
    142     )]
    143     pub free_busy: Option<RadrootsCalendarEventFreeBusy>,
    144     #[cfg_attr(
    145         feature = "serde",
    146         serde(default, skip_serializing_if = "Option::is_none")
    147     )]
    148     pub note: Option<String>,
    149     #[cfg_attr(
    150         feature = "serde",
    151         serde(default, skip_serializing_if = "Option::is_none")
    152     )]
    153     pub participants: Option<Vec<RadrootsCalendarParticipant>>,
    154 }
    155 
    156 #[cfg(all(test, feature = "std", feature = "serde"))]
    157 mod tests {
    158     use super::*;
    159 
    160     #[test]
    161     fn date_event_represents_all_day_event_fields() {
    162         let event = RadrootsCalendarDateEvent {
    163             d_tag: "market-day".to_string(),
    164             title: "market day".to_string(),
    165             start: "2026-06-20".to_string(),
    166             description: Some("Farm stand pickup window.".to_string()),
    167             end: None,
    168             days: Some(vec![RadrootsCalendarDateValue {
    169                 value: "2026-06-20".to_string(),
    170             }]),
    171             location: Some(RadrootsSocialLocation {
    172                 name: Some("farm stand".to_string()),
    173                 geohash: Some("c23nb62w20st".to_string()),
    174             }),
    175             summary: Some("weekly pickup".to_string()),
    176             image: None,
    177             participants: None,
    178         };
    179 
    180         assert_eq!(event.d_tag, "market-day");
    181         assert_eq!(event.start, "2026-06-20");
    182         assert_eq!(event.days.expect("days")[0].value, "2026-06-20");
    183     }
    184 
    185     #[test]
    186     fn time_event_represents_timestamped_event_fields() {
    187         let event = RadrootsCalendarTimeEvent {
    188             d_tag: "wash-pack".to_string(),
    189             title: "wash pack shift".to_string(),
    190             start: 1_781_895_600,
    191             dates: vec![RadrootsCalendarDateValue {
    192                 value: "2026-06-20".to_string(),
    193             }],
    194             description: Some("Pack CSA shares before pickup.".to_string()),
    195             end: Some(1_781_899_200),
    196             start_tzid: Some("America/Vancouver".to_string()),
    197             end_tzid: Some("America/Vancouver".to_string()),
    198             location: None,
    199             summary: None,
    200             image: None,
    201             participants: Some(vec![RadrootsCalendarParticipant {
    202                 pubkey: "a".repeat(64),
    203                 relay: None,
    204                 role: Some("host".to_string()),
    205             }]),
    206         };
    207 
    208         assert_eq!(event.start, 1_781_895_600);
    209         assert_eq!(event.end, Some(1_781_899_200));
    210         assert_eq!(event.start_tzid.as_deref(), Some("America/Vancouver"));
    211         assert_eq!(event.participants.expect("participants").len(), 1);
    212     }
    213 
    214     #[test]
    215     fn calendar_collection_represents_event_address_refs() {
    216         let calendar = RadrootsCalendar {
    217             d_tag: "farm-calendar".to_string(),
    218             title: "farm calendar".to_string(),
    219             events: vec![RadrootsSocialTarget::Address {
    220                 address: "31923:pubkey:wash-pack".to_string(),
    221                 author: None,
    222                 event_kind: Some(31923),
    223                 relays: None,
    224             }],
    225             description: Some("Shared farm operations schedule.".to_string()),
    226             summary: None,
    227             image: None,
    228         };
    229 
    230         assert_eq!(calendar.d_tag, "farm-calendar");
    231         assert_eq!(calendar.events.len(), 1);
    232         assert!(matches!(
    233             calendar.events[0],
    234             RadrootsSocialTarget::Address { .. }
    235         ));
    236     }
    237 
    238     #[test]
    239     fn rsvp_represents_status_and_free_busy_state() {
    240         let rsvp = RadrootsCalendarEventRsvp {
    241             d_tag: "rsvp-1".to_string(),
    242             event: RadrootsSocialTarget::Address {
    243                 address: "31923:pubkey:wash-pack".to_string(),
    244                 author: Some("a".repeat(64)),
    245                 event_kind: Some(31923),
    246                 relays: None,
    247             },
    248             event_id: Some("b".repeat(64)),
    249             status: RadrootsCalendarEventRsvpStatus::Tentative,
    250             free_busy: Some(RadrootsCalendarEventFreeBusy::Busy),
    251             note: Some("depends on harvest".to_string()),
    252             participants: None,
    253         };
    254 
    255         assert_eq!(rsvp.status, RadrootsCalendarEventRsvpStatus::Tentative);
    256         assert_eq!(
    257             rsvp.event_id.as_deref(),
    258             Some("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb")
    259         );
    260         assert_eq!(rsvp.free_busy, Some(RadrootsCalendarEventFreeBusy::Busy));
    261     }
    262 }