Skip to main content

weatherkit/
service.rs

1use core::ffi::{c_char, c_void};
2use std::time::{SystemTime, UNIX_EPOCH};
3
4use serde::Deserialize;
5
6use crate::availability_kind::WeatherAvailability;
7use crate::current_weather::CurrentWeather;
8use crate::daily_forecast::{DailyForecast, DayForecast};
9use crate::error::WeatherKitError;
10use crate::ffi;
11use crate::hourly_forecast::HourlyForecast;
12use crate::minute_forecast::MinuteForecastCollection;
13use crate::moon_events::MoonEvents;
14use crate::pressure::Pressure;
15use crate::private::{error_from_status, parse_json_from_handle};
16use crate::sun_events::SunEvents;
17use crate::weather_alert::{alerts_from_owned_ptr, WeatherAlert};
18use crate::weather_attribution::WeatherAttribution;
19
20#[derive(Debug, Clone, Copy, PartialEq, Deserialize)]
21#[serde(rename_all = "camelCase")]
22pub struct CLLocation {
23    pub latitude: f64,
24    pub longitude: f64,
25}
26
27impl CLLocation {
28    pub const fn new(latitude: f64, longitude: f64) -> Self {
29        Self {
30            latitude,
31            longitude,
32        }
33    }
34
35    fn validate(&self) -> Result<(), WeatherKitError> {
36        if !self.latitude.is_finite() || !self.longitude.is_finite() {
37            return Err(WeatherKitError::bridge(
38                -1,
39                "latitude and longitude must be finite numbers",
40            ));
41        }
42        if !(-90.0..=90.0).contains(&self.latitude) {
43            return Err(WeatherKitError::bridge(
44                -1,
45                format!("latitude {} is outside -90..=90", self.latitude),
46            ));
47        }
48        if !(-180.0..=180.0).contains(&self.longitude) {
49            return Err(WeatherKitError::bridge(
50                -1,
51                format!("longitude {} is outside -180..=180", self.longitude),
52            ));
53        }
54        Ok(())
55    }
56}
57
58#[derive(Debug, Clone, PartialEq, Eq)]
59pub struct DateInterval {
60    pub start: SystemTime,
61    pub end: SystemTime,
62}
63
64impl DateInterval {
65    pub fn new(start: SystemTime, end: SystemTime) -> Result<Self, WeatherKitError> {
66        if start > end {
67            return Err(WeatherKitError::bridge(
68                -1,
69                "date interval start must not be after end",
70            ));
71        }
72        Ok(Self { start, end })
73    }
74
75    fn start_seconds(&self) -> Result<f64, WeatherKitError> {
76        unix_seconds(self.start)
77    }
78
79    fn end_seconds(&self) -> Result<f64, WeatherKitError> {
80        unix_seconds(self.end)
81    }
82}
83
84#[derive(Debug, Clone, PartialEq, Deserialize)]
85#[serde(rename_all = "camelCase")]
86pub struct WeatherMetadata {
87    pub date: String,
88    pub expiration_date: String,
89    pub location: CLLocation,
90}
91
92#[derive(Debug, Clone, PartialEq, Deserialize)]
93#[serde(rename_all = "camelCase")]
94pub struct Weather {
95    pub current_weather: CurrentWeather,
96    pub hourly_forecast: Vec<crate::hourly_forecast::HourForecast>,
97    pub daily_forecast: Vec<DayForecast>,
98    pub minute_forecast: Option<Vec<crate::minute_forecast::MinuteForecast>>,
99    #[serde(default)]
100    pub weather_alerts: Vec<WeatherAlert>,
101    pub availability: WeatherAvailability,
102}
103
104#[derive(Debug, Clone, Copy, Default)]
105pub struct WeatherService {
106    kind: ServiceKind,
107}
108
109#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
110enum ServiceKind {
111    #[default]
112    Shared,
113    Owned,
114}
115
116struct ServiceHandle {
117    ptr: *mut c_void,
118}
119
120type LocationFetchFn =
121    unsafe extern "C" fn(*mut c_void, f64, f64, *mut *mut c_void, *mut *mut c_char) -> i32;
122type IntervalFetchFn = unsafe extern "C" fn(
123    *mut c_void,
124    f64,
125    f64,
126    i32,
127    f64,
128    f64,
129    *mut *mut c_void,
130    *mut *mut c_char,
131) -> i32;
132type ServiceFetchFn = unsafe extern "C" fn(*mut c_void, *mut *mut c_void, *mut *mut c_char) -> i32;
133
134impl ServiceHandle {
135    fn acquire(kind: ServiceKind) -> Result<Self, WeatherKitError> {
136        let ptr = unsafe {
137            match kind {
138                ServiceKind::Shared => ffi::service::wk_weather_service_shared(),
139                ServiceKind::Owned => ffi::service::wk_weather_service_new(),
140            }
141        };
142        if ptr.is_null() {
143            Err(WeatherKitError::bridge(
144                -1,
145                "failed to acquire WeatherService handle",
146            ))
147        } else {
148            Ok(Self { ptr })
149        }
150    }
151
152    fn as_ptr(&self) -> *mut c_void {
153        self.ptr
154    }
155}
156
157impl Drop for ServiceHandle {
158    fn drop(&mut self) {
159        if !self.ptr.is_null() {
160            unsafe {
161                ffi::service::wk_weather_service_release(self.ptr);
162            }
163            self.ptr = core::ptr::null_mut();
164        }
165    }
166}
167
168impl WeatherService {
169    pub const fn shared() -> Self {
170        Self {
171            kind: ServiceKind::Shared,
172        }
173    }
174
175    pub const fn new() -> Self {
176        Self {
177            kind: ServiceKind::Owned,
178        }
179    }
180
181    pub fn attribution(&self) -> Result<WeatherAttribution, WeatherKitError> {
182        let ptr = self.fetch_service_handle(
183            ffi::service::wk_weather_service_attribution,
184            "WeatherService.attribution",
185        )?;
186        WeatherAttribution::from_owned_ptr(ptr)
187    }
188
189    pub fn weather(&self, location: &CLLocation) -> Result<Weather, WeatherKitError> {
190        let ptr = self.fetch_location_handle(
191            location,
192            ffi::service::wk_weather_service_weather,
193            "WeatherService.weather(for:)",
194        )?;
195        parse_json_from_handle(
196            ptr,
197            ffi::service::wk_weather_release,
198            ffi::service::wk_weather_copy_json,
199            "weather",
200        )
201    }
202
203    pub fn current_weather(
204        &self,
205        location: &CLLocation,
206    ) -> Result<CurrentWeather, WeatherKitError> {
207        let ptr = self.fetch_location_handle(
208            location,
209            ffi::service::wk_weather_service_current_weather,
210            "WeatherService.weather(for: including: .current)",
211        )?;
212        CurrentWeather::from_owned_ptr(ptr)
213    }
214
215    pub fn hourly_forecast(
216        &self,
217        location: &CLLocation,
218    ) -> Result<HourlyForecast, WeatherKitError> {
219        let ptr = self.fetch_interval_handle(
220            location,
221            None,
222            ffi::service::wk_weather_service_hourly_forecast,
223            "WeatherService.weather(for: including: .hourly)",
224        )?;
225        HourlyForecast::from_owned_ptr(ptr)
226    }
227
228    pub fn hourly_forecast_in(
229        &self,
230        location: &CLLocation,
231        interval: DateInterval,
232    ) -> Result<HourlyForecast, WeatherKitError> {
233        let ptr = self.fetch_interval_handle(
234            location,
235            Some(&interval),
236            ffi::service::wk_weather_service_hourly_forecast,
237            "WeatherService.weather(for: including: .hourly(startDate:endDate))",
238        )?;
239        HourlyForecast::from_owned_ptr(ptr)
240    }
241
242    pub fn daily_forecast(&self, location: &CLLocation) -> Result<DailyForecast, WeatherKitError> {
243        let ptr = self.fetch_interval_handle(
244            location,
245            None,
246            ffi::service::wk_weather_service_daily_forecast,
247            "WeatherService.weather(for: including: .daily)",
248        )?;
249        DailyForecast::from_owned_ptr(ptr)
250    }
251
252    pub fn daily_forecast_in(
253        &self,
254        location: &CLLocation,
255        interval: DateInterval,
256    ) -> Result<DailyForecast, WeatherKitError> {
257        let ptr = self.fetch_interval_handle(
258            location,
259            Some(&interval),
260            ffi::service::wk_weather_service_daily_forecast,
261            "WeatherService.weather(for: including: .daily(startDate:endDate))",
262        )?;
263        DailyForecast::from_owned_ptr(ptr)
264    }
265
266    pub fn minute_forecast(
267        &self,
268        location: &CLLocation,
269    ) -> Result<Option<MinuteForecastCollection>, WeatherKitError> {
270        let ptr = self.fetch_location_handle(
271            location,
272            ffi::service::wk_weather_service_minute_forecast,
273            "WeatherService.weather(for: including: .minute)",
274        )?;
275        MinuteForecastCollection::option_from_owned_ptr(ptr)
276    }
277
278    pub fn weather_alerts(
279        &self,
280        location: &CLLocation,
281    ) -> Result<Vec<WeatherAlert>, WeatherKitError> {
282        let ptr = self.fetch_location_handle(
283            location,
284            ffi::service::wk_weather_service_weather_alerts,
285            "WeatherService.weather(for: including: .alerts)",
286        )?;
287        alerts_from_owned_ptr(ptr)
288    }
289
290    pub fn availability(
291        &self,
292        location: &CLLocation,
293    ) -> Result<WeatherAvailability, WeatherKitError> {
294        let ptr = self.fetch_location_handle(
295            location,
296            ffi::service::wk_weather_service_availability,
297            "WeatherService.weather(for: including: .availability)",
298        )?;
299        WeatherAvailability::from_owned_ptr(ptr)
300    }
301
302    pub fn sun_events(&self, location: &CLLocation) -> Result<SunEvents, WeatherKitError> {
303        let forecast = self.daily_forecast(location)?;
304        forecast
305            .forecast
306            .first()
307            .map(|day| day.sun.clone())
308            .ok_or_else(|| WeatherKitError::bridge(-1, "daily forecast returned no days"))
309    }
310
311    pub fn moon_events(&self, location: &CLLocation) -> Result<MoonEvents, WeatherKitError> {
312        let forecast = self.daily_forecast(location)?;
313        forecast
314            .forecast
315            .first()
316            .map(|day| day.moon.clone())
317            .ok_or_else(|| WeatherKitError::bridge(-1, "daily forecast returned no days"))
318    }
319
320    pub fn pressure(&self, location: &CLLocation) -> Result<Pressure, WeatherKitError> {
321        Ok(self.current_weather(location)?.pressure_reading())
322    }
323
324    fn fetch_service_handle(
325        &self,
326        call: ServiceFetchFn,
327        context: &str,
328    ) -> Result<*mut c_void, WeatherKitError> {
329        let service = ServiceHandle::acquire(self.kind)?;
330        let mut out_handle = core::ptr::null_mut();
331        let mut out_error = core::ptr::null_mut();
332        let status = unsafe { call(service.as_ptr(), &mut out_handle, &mut out_error) };
333        if status != ffi::status::OK {
334            return Err(unsafe { error_from_status(status, out_error) });
335        }
336        if out_handle.is_null() {
337            return Err(WeatherKitError::bridge(
338                -1,
339                format!("missing handle for {context}"),
340            ));
341        }
342        Ok(out_handle)
343    }
344
345    fn fetch_location_handle(
346        &self,
347        location: &CLLocation,
348        call: LocationFetchFn,
349        context: &str,
350    ) -> Result<*mut c_void, WeatherKitError> {
351        location.validate()?;
352        let service = ServiceHandle::acquire(self.kind)?;
353        let mut out_handle = core::ptr::null_mut();
354        let mut out_error = core::ptr::null_mut();
355        let status = unsafe {
356            call(
357                service.as_ptr(),
358                location.latitude,
359                location.longitude,
360                &mut out_handle,
361                &mut out_error,
362            )
363        };
364        if status != ffi::status::OK {
365            return Err(unsafe { error_from_status(status, out_error) });
366        }
367        if out_handle.is_null() {
368            return Err(WeatherKitError::bridge(
369                -1,
370                format!("missing handle for {context}"),
371            ));
372        }
373        Ok(out_handle)
374    }
375
376    fn fetch_interval_handle(
377        &self,
378        location: &CLLocation,
379        interval: Option<&DateInterval>,
380        call: IntervalFetchFn,
381        context: &str,
382    ) -> Result<*mut c_void, WeatherKitError> {
383        location.validate()?;
384        let service = ServiceHandle::acquire(self.kind)?;
385        let mut out_handle = core::ptr::null_mut();
386        let mut out_error = core::ptr::null_mut();
387        let (has_range, start_seconds, end_seconds) = if let Some(interval) = interval {
388            (1, interval.start_seconds()?, interval.end_seconds()?)
389        } else {
390            (0, 0.0, 0.0)
391        };
392        let status = unsafe {
393            call(
394                service.as_ptr(),
395                location.latitude,
396                location.longitude,
397                has_range,
398                start_seconds,
399                end_seconds,
400                &mut out_handle,
401                &mut out_error,
402            )
403        };
404        if status != ffi::status::OK {
405            return Err(unsafe { error_from_status(status, out_error) });
406        }
407        if out_handle.is_null() {
408            return Err(WeatherKitError::bridge(
409                -1,
410                format!("missing handle for {context}"),
411            ));
412        }
413        Ok(out_handle)
414    }
415}
416
417fn unix_seconds(time: SystemTime) -> Result<f64, WeatherKitError> {
418    let duration = time.duration_since(UNIX_EPOCH).map_err(|error| {
419        WeatherKitError::bridge(-1, format!("time {time:?} is before UNIX_EPOCH: {error}"))
420    })?;
421    Ok(duration.as_secs_f64())
422}