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 }