ridewithgps_client/
events.rs

1//! Event-related types and methods
2
3use crate::{PaginatedResponse, Photo, Result, RideWithGpsClient, Visibility};
4use serde::{Deserialize, Serialize};
5
6/// Event organizer information
7#[derive(Debug, Clone, Deserialize, Serialize)]
8pub struct Organizer {
9    /// Organizer ID
10    pub id: Option<u64>,
11
12    /// Organizer name
13    pub name: Option<String>,
14
15    /// Created timestamp
16    pub created_at: Option<String>,
17
18    /// Updated timestamp
19    pub updated_at: Option<String>,
20}
21
22/// An event
23#[derive(Debug, Clone, Deserialize, Serialize)]
24pub struct Event {
25    /// Event ID
26    pub id: u64,
27
28    /// Event name
29    pub name: Option<String>,
30
31    /// Event description
32    pub description: Option<String>,
33
34    /// Event location
35    pub location: Option<String>,
36
37    /// Latitude
38    pub lat: Option<f64>,
39
40    /// Longitude
41    pub lng: Option<f64>,
42
43    /// Event visibility
44    pub visibility: Option<Visibility>,
45
46    /// API URL
47    pub url: Option<String>,
48
49    /// HTML/web URL
50    pub html_url: Option<String>,
51
52    /// Time zone
53    pub time_zone: Option<String>,
54
55    /// Start date
56    pub start_date: Option<String>,
57
58    /// Start time (e.g., "09:00")
59    pub start_time: Option<String>,
60
61    /// End date
62    pub end_date: Option<String>,
63
64    /// End time (e.g., "17:00")
65    pub end_time: Option<String>,
66
67    /// Whether it's an all-day event
68    pub all_day: Option<bool>,
69
70    /// Event start date/time (combined)
71    pub starts_at: Option<String>,
72
73    /// Event end date/time (combined)
74    pub ends_at: Option<String>,
75
76    /// Registration opens at
77    pub registration_opens_at: Option<String>,
78
79    /// Registration closes at
80    pub registration_closes_at: Option<String>,
81
82    /// User ID of the event owner
83    pub user_id: Option<u64>,
84
85    /// Created timestamp
86    pub created_at: Option<String>,
87
88    /// Updated timestamp
89    pub updated_at: Option<String>,
90
91    /// Event URL slug
92    pub slug: Option<String>,
93
94    /// Logo URL
95    pub logo_url: Option<String>,
96
97    /// Banner URL
98    pub banner_url: Option<String>,
99
100    /// Whether registration is required
101    pub registration_required: Option<bool>,
102
103    /// Maximum attendees
104    pub max_attendees: Option<u32>,
105
106    /// Current number of attendees
107    pub attendee_count: Option<u32>,
108
109    /// Event organizers (included when fetching a specific event)
110    pub organizers: Option<Vec<Organizer>>,
111
112    /// Photos (included when fetching a specific event)
113    pub photos: Option<Vec<Photo>>,
114}
115
116/// Parameters for listing events
117#[derive(Debug, Clone, Default, Serialize)]
118pub struct ListEventsParams {
119    /// Filter by event name
120    #[serde(skip_serializing_if = "Option::is_none")]
121    pub name: Option<String>,
122
123    /// Filter by visibility
124    #[serde(skip_serializing_if = "Option::is_none")]
125    pub visibility: Option<Visibility>,
126
127    /// Page number
128    #[serde(skip_serializing_if = "Option::is_none")]
129    pub page: Option<u32>,
130
131    /// Page size
132    #[serde(skip_serializing_if = "Option::is_none")]
133    pub page_size: Option<u32>,
134}
135
136/// Request to create or update an event
137#[derive(Debug, Clone, Serialize)]
138pub struct EventRequest {
139    /// Event name
140    #[serde(skip_serializing_if = "Option::is_none")]
141    pub name: Option<String>,
142
143    /// Event description
144    #[serde(skip_serializing_if = "Option::is_none")]
145    pub description: Option<String>,
146
147    /// Event location
148    #[serde(skip_serializing_if = "Option::is_none")]
149    pub location: Option<String>,
150
151    /// Event visibility
152    #[serde(skip_serializing_if = "Option::is_none")]
153    pub visibility: Option<Visibility>,
154
155    /// Event start date/time
156    #[serde(skip_serializing_if = "Option::is_none")]
157    pub starts_at: Option<String>,
158
159    /// Event end date/time
160    #[serde(skip_serializing_if = "Option::is_none")]
161    pub ends_at: Option<String>,
162
163    /// Registration opens at
164    #[serde(skip_serializing_if = "Option::is_none")]
165    pub registration_opens_at: Option<String>,
166
167    /// Registration closes at
168    #[serde(skip_serializing_if = "Option::is_none")]
169    pub registration_closes_at: Option<String>,
170
171    /// Whether registration is required
172    #[serde(skip_serializing_if = "Option::is_none")]
173    pub registration_required: Option<bool>,
174
175    /// Maximum attendees
176    #[serde(skip_serializing_if = "Option::is_none")]
177    pub max_attendees: Option<u32>,
178}
179
180impl RideWithGpsClient {
181    /// List events
182    ///
183    /// # Arguments
184    ///
185    /// * `params` - Optional parameters for filtering and pagination
186    ///
187    /// # Example
188    ///
189    /// ```rust,no_run
190    /// use ridewithgps_client::{RideWithGpsClient, ListEventsParams};
191    ///
192    /// let client = RideWithGpsClient::new(
193    ///     "https://ridewithgps.com",
194    ///     "your-api-key",
195    ///     Some("your-auth-token")
196    /// );
197    ///
198    /// let events = client.list_events(None).unwrap();
199    /// println!("Found {} events", events.results.len());
200    /// ```
201    pub fn list_events(
202        &self,
203        params: Option<&ListEventsParams>,
204    ) -> Result<PaginatedResponse<Event>> {
205        let mut url = "/api/v1/events.json".to_string();
206
207        if let Some(params) = params {
208            let query = serde_json::to_value(params)?;
209            if let Some(obj) = query.as_object() {
210                if !obj.is_empty() {
211                    let query_str = serde_urlencoded::to_string(obj).map_err(|e| {
212                        crate::Error::ApiError(format!("Failed to encode query: {}", e))
213                    })?;
214                    url.push('?');
215                    url.push_str(&query_str);
216                }
217            }
218        }
219
220        self.get(&url)
221    }
222
223    /// Create a new event
224    ///
225    /// # Arguments
226    ///
227    /// * `event` - The event data
228    ///
229    /// # Example
230    ///
231    /// ```rust,no_run
232    /// use ridewithgps_client::{RideWithGpsClient, EventRequest, Visibility};
233    ///
234    /// let client = RideWithGpsClient::new(
235    ///     "https://ridewithgps.com",
236    ///     "your-api-key",
237    ///     Some("your-auth-token")
238    /// );
239    ///
240    /// let event_req = EventRequest {
241    ///     name: Some("My Event".to_string()),
242    ///     description: Some("A great ride".to_string()),
243    ///     location: Some("San Francisco, CA".to_string()),
244    ///     visibility: Some(Visibility::Public),
245    ///     starts_at: Some("2025-06-01T09:00:00".to_string()),
246    ///     ends_at: Some("2025-06-01T17:00:00".to_string()),
247    ///     registration_opens_at: None,
248    ///     registration_closes_at: None,
249    ///     registration_required: Some(false),
250    ///     max_attendees: None,
251    /// };
252    ///
253    /// let event = client.create_event(&event_req).unwrap();
254    /// println!("Created event: {}", event.id);
255    /// ```
256    pub fn create_event(&self, event: &EventRequest) -> Result<Event> {
257        #[derive(Deserialize)]
258        struct EventWrapper {
259            event: Event,
260        }
261
262        let wrapper: EventWrapper = self.post("/api/v1/events.json", event)?;
263        Ok(wrapper.event)
264    }
265
266    /// Get a specific event by ID
267    ///
268    /// # Arguments
269    ///
270    /// * `id` - The event ID
271    ///
272    /// # Example
273    ///
274    /// ```rust,no_run
275    /// use ridewithgps_client::RideWithGpsClient;
276    ///
277    /// let client = RideWithGpsClient::new(
278    ///     "https://ridewithgps.com",
279    ///     "your-api-key",
280    ///     None
281    /// );
282    ///
283    /// let event = client.get_event(12345).unwrap();
284    /// println!("Event: {:?}", event);
285    /// ```
286    pub fn get_event(&self, id: u64) -> Result<Event> {
287        #[derive(Deserialize)]
288        struct EventWrapper {
289            event: Event,
290        }
291
292        let wrapper: EventWrapper = self.get(&format!("/api/v1/events/{}.json", id))?;
293        Ok(wrapper.event)
294    }
295
296    /// Update an event
297    ///
298    /// # Arguments
299    ///
300    /// * `id` - The event ID
301    /// * `event` - The updated event data
302    ///
303    /// # Example
304    ///
305    /// ```rust,no_run
306    /// use ridewithgps_client::{RideWithGpsClient, EventRequest};
307    ///
308    /// let client = RideWithGpsClient::new(
309    ///     "https://ridewithgps.com",
310    ///     "your-api-key",
311    ///     Some("your-auth-token")
312    /// );
313    ///
314    /// let event_req = EventRequest {
315    ///     name: Some("Updated Event Name".to_string()),
316    ///     description: None,
317    ///     location: None,
318    ///     visibility: None,
319    ///     starts_at: None,
320    ///     ends_at: None,
321    ///     registration_opens_at: None,
322    ///     registration_closes_at: None,
323    ///     registration_required: None,
324    ///     max_attendees: None,
325    /// };
326    ///
327    /// let event = client.update_event(12345, &event_req).unwrap();
328    /// println!("Updated event: {:?}", event);
329    /// ```
330    pub fn update_event(&self, id: u64, event: &EventRequest) -> Result<Event> {
331        #[derive(Deserialize)]
332        struct EventWrapper {
333            event: Event,
334        }
335
336        let wrapper: EventWrapper = self.put(&format!("/api/v1/events/{}.json", id), event)?;
337        Ok(wrapper.event)
338    }
339
340    /// Delete an event
341    ///
342    /// # Arguments
343    ///
344    /// * `id` - The event ID
345    ///
346    /// # Example
347    ///
348    /// ```rust,no_run
349    /// use ridewithgps_client::RideWithGpsClient;
350    ///
351    /// let client = RideWithGpsClient::new(
352    ///     "https://ridewithgps.com",
353    ///     "your-api-key",
354    ///     Some("your-auth-token")
355    /// );
356    ///
357    /// client.delete_event(12345).unwrap();
358    /// ```
359    pub fn delete_event(&self, id: u64) -> Result<()> {
360        self.delete(&format!("/api/v1/events/{}.json", id))
361    }
362}
363
364#[cfg(test)]
365mod tests {
366    use super::*;
367
368    #[test]
369    fn test_event_deserialization() {
370        let json = r#"{
371            "id": 789,
372            "name": "Test Event",
373            "location": "Portland, OR",
374            "visibility": "public",
375            "starts_at": "2025-06-01T09:00:00",
376            "attendee_count": 25
377        }"#;
378
379        let event: Event = serde_json::from_str(json).unwrap();
380        assert_eq!(event.id, 789);
381        assert_eq!(event.name.as_deref(), Some("Test Event"));
382        assert_eq!(event.location.as_deref(), Some("Portland, OR"));
383        assert_eq!(event.visibility, Some(Visibility::Public));
384        assert_eq!(event.attendee_count, Some(25));
385    }
386
387    #[test]
388    fn test_event_request_serialization() {
389        let req = EventRequest {
390            name: Some("My Event".to_string()),
391            description: Some("Fun ride".to_string()),
392            location: None,
393            visibility: Some(Visibility::Public),
394            starts_at: None,
395            ends_at: None,
396            registration_opens_at: None,
397            registration_closes_at: None,
398            registration_required: Some(true),
399            max_attendees: Some(100),
400        };
401
402        let json = serde_json::to_value(&req).unwrap();
403        assert_eq!(json.get("name").unwrap(), "My Event");
404        assert_eq!(json.get("visibility").unwrap(), "public");
405        assert_eq!(json.get("registration_required").unwrap(), true);
406        assert_eq!(json.get("max_attendees").unwrap(), 100);
407    }
408
409    #[test]
410    fn test_event_wrapper_deserialization() {
411        let json = r#"{
412            "event": {
413                "id": 999,
414                "name": "Wrapped Event",
415                "location": "Portland, OR",
416                "visibility": "public"
417            }
418        }"#;
419
420        #[derive(Deserialize)]
421        struct EventWrapper {
422            event: Event,
423        }
424
425        let wrapper: EventWrapper = serde_json::from_str(json).unwrap();
426        assert_eq!(wrapper.event.id, 999);
427        assert_eq!(wrapper.event.name.as_deref(), Some("Wrapped Event"));
428        assert_eq!(wrapper.event.location.as_deref(), Some("Portland, OR"));
429    }
430
431    #[test]
432    fn test_event_with_dates_and_times() {
433        let json = r#"{
434            "id": 555,
435            "name": "Time Test Event",
436            "start_date": "2025-06-01",
437            "start_time": "09:00",
438            "end_date": "2025-06-01",
439            "end_time": "17:00",
440            "all_day": false,
441            "time_zone": "America/Los_Angeles",
442            "starts_at": "2025-06-01T09:00:00-07:00",
443            "ends_at": "2025-06-01T17:00:00-07:00"
444        }"#;
445
446        let event: Event = serde_json::from_str(json).unwrap();
447        assert_eq!(event.id, 555);
448        assert_eq!(event.start_date.as_deref(), Some("2025-06-01"));
449        assert_eq!(event.start_time.as_deref(), Some("09:00"));
450        assert_eq!(event.all_day, Some(false));
451        assert_eq!(event.time_zone.as_deref(), Some("America/Los_Angeles"));
452    }
453
454    #[test]
455    fn test_organizer_deserialization() {
456        let json = r#"{
457            "id": 42,
458            "name": "Bike Club",
459            "created_at": "2020-01-01T00:00:00Z"
460        }"#;
461
462        let organizer: Organizer = serde_json::from_str(json).unwrap();
463        assert_eq!(organizer.id, Some(42));
464        assert_eq!(organizer.name.as_deref(), Some("Bike Club"));
465    }
466}