Skip to main content

weatherkit/
service.rs

1//! WeatherKit service types and query helpers.
2
3use core::ffi::{c_char, c_void};
4use std::time::{SystemTime, UNIX_EPOCH};
5
6use serde::Deserialize;
7
8use crate::availability_kind::WeatherAvailability;
9use crate::changes::{HistoricalComparisons, WeatherChanges};
10use crate::current_weather::CurrentWeather;
11use crate::daily_forecast::{DailyForecast, DayForecast};
12use crate::error::WeatherKitError;
13use crate::ffi;
14use crate::hourly_forecast::HourlyForecast;
15use crate::minute_forecast::MinuteForecastCollection;
16use crate::moon_events::MoonEvents;
17use crate::pressure::Pressure;
18use crate::private::{error_from_status, parse_json_from_handle};
19use crate::statistics::{
20    DailyWeatherStatistics, DailyWeatherStatisticsQuery, DailyWeatherStatisticsResult,
21    DailyWeatherSummary, DailyWeatherSummaryQuery, DailyWeatherSummaryResult,
22    DayPrecipitationStatistics, DayPrecipitationSummary, DayTemperatureStatistics,
23    DayTemperatureSummary, HourTemperatureStatistics, HourlyWeatherStatistics,
24    HourlyWeatherStatisticsQuery, MonthPrecipitationStatistics, MonthTemperatureStatistics,
25    MonthlyWeatherStatistics, MonthlyWeatherStatisticsQuery, MonthlyWeatherStatisticsResult,
26};
27use crate::sun_events::SunEvents;
28use crate::weather_alert::{alerts_from_owned_ptr, WeatherAlert};
29use crate::weather_attribution::WeatherAttribution;
30
31/// Represents a WeatherKit location coordinate.
32#[derive(Debug, Clone, Copy, PartialEq, Deserialize)]
33#[serde(rename_all = "camelCase")]
34pub struct CLLocation {
35    /// Matches the WeatherKit latitude value.
36    pub latitude: f64,
37    /// Matches the WeatherKit longitude value.
38    pub longitude: f64,
39}
40
41impl CLLocation {
42    /// Creates a WeatherKit location value from latitude and longitude.
43    pub const fn new(latitude: f64, longitude: f64) -> Self {
44        Self {
45            latitude,
46            longitude,
47        }
48    }
49
50    fn validate(&self) -> Result<(), WeatherKitError> {
51        if !self.latitude.is_finite() || !self.longitude.is_finite() {
52            return Err(WeatherKitError::bridge(
53                -1,
54                "latitude and longitude must be finite numbers",
55            ));
56        }
57        if !(-90.0..=90.0).contains(&self.latitude) {
58            return Err(WeatherKitError::bridge(
59                -1,
60                format!("latitude {} is outside -90..=90", self.latitude),
61            ));
62        }
63        if !(-180.0..=180.0).contains(&self.longitude) {
64            return Err(WeatherKitError::bridge(
65                -1,
66                format!("longitude {} is outside -180..=180", self.longitude),
67            ));
68        }
69        Ok(())
70    }
71}
72
73/// Represents a WeatherKit date interval.
74#[derive(Debug, Clone, PartialEq, Eq)]
75pub struct DateInterval {
76    /// Matches the WeatherKit start value.
77    pub start: SystemTime,
78    /// Matches the WeatherKit end value.
79    pub end: SystemTime,
80}
81
82impl DateInterval {
83    /// Creates a WeatherKit date interval after validating its bounds.
84    pub fn new(start: SystemTime, end: SystemTime) -> Result<Self, WeatherKitError> {
85        if start > end {
86            return Err(WeatherKitError::bridge(
87                -1,
88                "date interval start must not be after end",
89            ));
90        }
91        Ok(Self { start, end })
92    }
93
94    fn start_seconds(&self) -> Result<f64, WeatherKitError> {
95        unix_seconds(self.start)
96    }
97
98    fn end_seconds(&self) -> Result<f64, WeatherKitError> {
99        unix_seconds(self.end)
100    }
101}
102
103/// Represents WeatherKit response metadata.
104#[derive(Debug, Clone, PartialEq, Deserialize)]
105#[serde(rename_all = "camelCase")]
106pub struct WeatherMetadata {
107    /// Matches the WeatherKit date value.
108    pub date: String,
109    /// Matches the WeatherKit expiration date value.
110    pub expiration_date: String,
111    /// Matches the WeatherKit location value.
112    pub location: CLLocation,
113}
114
115/// Represents the aggregate WeatherKit weather payload.
116#[derive(Debug, Clone, PartialEq, Deserialize)]
117#[serde(rename_all = "camelCase")]
118pub struct Weather {
119    /// Matches the WeatherKit current weather value.
120    pub current_weather: CurrentWeather,
121    /// Matches the WeatherKit hourly forecast value.
122    pub hourly_forecast: Vec<crate::hourly_forecast::HourForecast>,
123    /// Matches the WeatherKit daily forecast value.
124    pub daily_forecast: Vec<DayForecast>,
125    /// Matches the WeatherKit minute forecast value.
126    pub minute_forecast: Option<Vec<crate::minute_forecast::MinuteForecast>>,
127    /// Matches the WeatherKit weather alerts value.
128    #[serde(default)]
129    pub weather_alerts: Vec<WeatherAlert>,
130    /// Matches the WeatherKit availability value.
131    pub availability: WeatherAvailability,
132}
133
134/// Describes a WeatherKit query to fetch.
135#[derive(Debug, Clone, PartialEq, Eq)]
136pub enum WeatherQuery {
137    /// Requests the WeatherKit current weather payload.
138    Current,
139    /// Requests the WeatherKit minute forecast payload.
140    Minute,
141    /// Requests the WeatherKit hourly forecast payload.
142    Hourly,
143    /// Requests the WeatherKit hourly forecast for the supplied interval.
144    HourlyIn(DateInterval),
145    /// Requests the WeatherKit daily forecast payload.
146    Daily,
147    /// Requests the WeatherKit daily forecast for the supplied interval.
148    DailyIn(DateInterval),
149    /// Requests the WeatherKit weather alerts payload.
150    Alerts,
151    /// Requests the WeatherKit availability payload.
152    Availability,
153    /// Requests the WeatherKit weather changes payload.
154    Changes,
155    /// Requests the WeatherKit historical comparisons payload.
156    HistoricalComparisons,
157}
158
159/// Wraps a WeatherKit query result.
160#[derive(Debug, Clone, PartialEq)]
161pub enum WeatherQueryResult {
162    /// Carries the WeatherKit current weather payload.
163    CurrentWeather(Box<CurrentWeather>),
164    /// Carries the optional WeatherKit minute forecast payload.
165    MinuteForecast(Option<Box<MinuteForecastCollection>>),
166    /// Carries the WeatherKit hourly forecast payload.
167    HourlyForecast(Box<HourlyForecast>),
168    /// Carries the WeatherKit daily forecast payload.
169    DailyForecast(Box<DailyForecast>),
170    /// Carries the WeatherKit weather alerts payload.
171    WeatherAlerts(Vec<WeatherAlert>),
172    /// Carries the WeatherKit availability payload.
173    Availability(Box<WeatherAvailability>),
174    /// Carries the optional WeatherKit weather changes payload.
175    WeatherChanges(Option<Box<WeatherChanges>>),
176    /// Carries the optional WeatherKit historical comparisons payload.
177    HistoricalComparisons(Option<Box<HistoricalComparisons>>),
178}
179
180impl WeatherQuery {
181    fn fetch(
182        &self,
183        service: WeatherService,
184        location: &CLLocation,
185    ) -> Result<WeatherQueryResult, WeatherKitError> {
186        match self {
187            Self::Current => service
188                .current_weather(location)
189                .map(Box::new)
190                .map(WeatherQueryResult::CurrentWeather),
191            Self::Minute => service
192                .minute_forecast(location)
193                .map(|forecast| forecast.map(Box::new))
194                .map(WeatherQueryResult::MinuteForecast),
195            Self::Hourly => service
196                .hourly_forecast(location)
197                .map(Box::new)
198                .map(WeatherQueryResult::HourlyForecast),
199            Self::HourlyIn(interval) => service
200                .hourly_forecast_in(location, interval.clone())
201                .map(Box::new)
202                .map(WeatherQueryResult::HourlyForecast),
203            Self::Daily => service
204                .daily_forecast(location)
205                .map(Box::new)
206                .map(WeatherQueryResult::DailyForecast),
207            Self::DailyIn(interval) => service
208                .daily_forecast_in(location, interval.clone())
209                .map(Box::new)
210                .map(WeatherQueryResult::DailyForecast),
211            Self::Alerts => service
212                .weather_alerts(location)
213                .map(WeatherQueryResult::WeatherAlerts),
214            Self::Availability => service
215                .availability(location)
216                .map(Box::new)
217                .map(WeatherQueryResult::Availability),
218            Self::Changes => service
219                .weather_changes(location)
220                .map(|changes| changes.map(Box::new))
221                .map(WeatherQueryResult::WeatherChanges),
222            Self::HistoricalComparisons => service
223                .historical_comparisons(location)
224                .map(|comparisons| comparisons.map(Box::new))
225                .map(WeatherQueryResult::HistoricalComparisons),
226        }
227    }
228}
229
230/// Wraps a WeatherKit service handle.
231#[derive(Debug, Clone, Copy, Default)]
232pub struct WeatherService {
233    kind: ServiceKind,
234}
235
236#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
237enum ServiceKind {
238    #[default]
239    Shared,
240    Owned,
241}
242
243struct ServiceHandle {
244    ptr: *mut c_void,
245}
246
247type LocationFetchFn =
248    unsafe extern "C" fn(*mut c_void, f64, f64, *mut *mut c_void, *mut *mut c_char) -> i32;
249type IntervalFetchFn = unsafe extern "C" fn(
250    *mut c_void,
251    f64,
252    f64,
253    i32,
254    f64,
255    f64,
256    *mut *mut c_void,
257    *mut *mut c_char,
258) -> i32;
259type ScopedQueryFetchFn = unsafe extern "C" fn(
260    *mut c_void,
261    f64,
262    f64,
263    i32,
264    i32,
265    f64,
266    f64,
267    i64,
268    i64,
269    *mut *mut c_void,
270    *mut *mut c_char,
271) -> i32;
272type ServiceFetchFn = unsafe extern "C" fn(*mut c_void, *mut *mut c_void, *mut *mut c_char) -> i32;
273
274#[derive(Debug, Clone, Copy)]
275enum QueryScope<'a> {
276    None,
277    Interval(&'a DateInterval),
278    Index { start: i64, end: i64 },
279}
280
281impl ServiceHandle {
282    fn acquire(kind: ServiceKind) -> Result<Self, WeatherKitError> {
283        // SAFETY: both FFI functions return a freshly retained opaque pointer
284        // (or null on failure); the null-check immediately below guards use.
285        let ptr = unsafe {
286            match kind {
287                ServiceKind::Shared => ffi::service::wk_weather_service_shared(),
288                ServiceKind::Owned => ffi::service::wk_weather_service_new(),
289            }
290        };
291        if ptr.is_null() {
292            Err(WeatherKitError::bridge(
293                -1,
294                "failed to acquire WeatherService handle",
295            ))
296        } else {
297            Ok(Self { ptr })
298        }
299    }
300
301    fn as_ptr(&self) -> *mut c_void {
302        self.ptr
303    }
304}
305
306impl Drop for ServiceHandle {
307    fn drop(&mut self) {
308        if !self.ptr.is_null() {
309            // SAFETY: ptr is non-null, was validated in `acquire`, and is set
310            // to null immediately after so it cannot be double-released.
311            unsafe {
312                ffi::service::wk_weather_service_release(self.ptr);
313            }
314            self.ptr = core::ptr::null_mut();
315        }
316    }
317}
318
319impl WeatherService {
320    /// Returns a WeatherKit service that reuses the shared native service.
321    pub const fn shared() -> Self {
322        Self {
323            kind: ServiceKind::Shared,
324        }
325    }
326
327    /// Returns a WeatherKit service with an owned native service handle.
328    pub const fn new() -> Self {
329        Self {
330            kind: ServiceKind::Owned,
331        }
332    }
333
334    pub(crate) const fn is_owned(self) -> bool {
335        matches!(self.kind, ServiceKind::Owned)
336    }
337
338    /// Fetches WeatherKit attribution information.
339    pub fn attribution(&self) -> Result<WeatherAttribution, WeatherKitError> {
340        let ptr = self.fetch_service_handle(
341            ffi::service::wk_weather_service_attribution,
342            "WeatherService.attribution",
343        )?;
344        WeatherAttribution::from_owned_ptr(ptr)
345    }
346
347    /// Fetches the aggregate WeatherKit weather payload.
348    pub fn weather(&self, location: &CLLocation) -> Result<Weather, WeatherKitError> {
349        let ptr = self.fetch_location_handle(
350            location,
351            ffi::service::wk_weather_service_weather,
352            "WeatherService.weather(for:)",
353        )?;
354        parse_json_from_handle(
355            ptr,
356            ffi::service::wk_weather_release,
357            ffi::service::wk_weather_copy_json,
358            "weather",
359        )
360    }
361
362    /// Fetches the WeatherKit current weather payload.
363    pub fn current_weather(
364        &self,
365        location: &CLLocation,
366    ) -> Result<CurrentWeather, WeatherKitError> {
367        let ptr = self.fetch_location_handle(
368            location,
369            ffi::service::wk_weather_service_current_weather,
370            "WeatherService.weather(for: including: .current)",
371        )?;
372        CurrentWeather::from_owned_ptr(ptr)
373    }
374
375    /// Fetches the WeatherKit hourly forecast.
376    pub fn hourly_forecast(
377        &self,
378        location: &CLLocation,
379    ) -> Result<HourlyForecast, WeatherKitError> {
380        let ptr = self.fetch_interval_handle(
381            location,
382            None,
383            ffi::service::wk_weather_service_hourly_forecast,
384            "WeatherService.weather(for: including: .hourly)",
385        )?;
386        HourlyForecast::from_owned_ptr(ptr)
387    }
388
389    /// Fetches the WeatherKit hourly forecast for the given interval.
390    pub fn hourly_forecast_in(
391        &self,
392        location: &CLLocation,
393        interval: DateInterval,
394    ) -> Result<HourlyForecast, WeatherKitError> {
395        let ptr = self.fetch_interval_handle(
396            location,
397            Some(&interval),
398            ffi::service::wk_weather_service_hourly_forecast,
399            "WeatherService.weather(for: including: .hourly(startDate:endDate))",
400        )?;
401        HourlyForecast::from_owned_ptr(ptr)
402    }
403
404    /// Fetches the WeatherKit daily forecast.
405    pub fn daily_forecast(&self, location: &CLLocation) -> Result<DailyForecast, WeatherKitError> {
406        let ptr = self.fetch_interval_handle(
407            location,
408            None,
409            ffi::service::wk_weather_service_daily_forecast,
410            "WeatherService.weather(for: including: .daily)",
411        )?;
412        DailyForecast::from_owned_ptr(ptr)
413    }
414
415    /// Fetches the WeatherKit daily forecast for the given interval.
416    pub fn daily_forecast_in(
417        &self,
418        location: &CLLocation,
419        interval: DateInterval,
420    ) -> Result<DailyForecast, WeatherKitError> {
421        let ptr = self.fetch_interval_handle(
422            location,
423            Some(&interval),
424            ffi::service::wk_weather_service_daily_forecast,
425            "WeatherService.weather(for: including: .daily(startDate:endDate))",
426        )?;
427        DailyForecast::from_owned_ptr(ptr)
428    }
429
430    /// Fetches the optional WeatherKit minute forecast.
431    pub fn minute_forecast(
432        &self,
433        location: &CLLocation,
434    ) -> Result<Option<MinuteForecastCollection>, WeatherKitError> {
435        let ptr = self.fetch_location_handle(
436            location,
437            ffi::service::wk_weather_service_minute_forecast,
438            "WeatherService.weather(for: including: .minute)",
439        )?;
440        MinuteForecastCollection::option_from_owned_ptr(ptr)
441    }
442
443    /// Fetches WeatherKit weather alerts.
444    pub fn weather_alerts(
445        &self,
446        location: &CLLocation,
447    ) -> Result<Vec<WeatherAlert>, WeatherKitError> {
448        let ptr = self.fetch_location_handle(
449            location,
450            ffi::service::wk_weather_service_weather_alerts,
451            "WeatherService.weather(for: including: .alerts)",
452        )?;
453        alerts_from_owned_ptr(ptr)
454    }
455
456    /// Fetches WeatherKit data availability for the location.
457    pub fn availability(
458        &self,
459        location: &CLLocation,
460    ) -> Result<WeatherAvailability, WeatherKitError> {
461        let ptr = self.fetch_location_handle(
462            location,
463            ffi::service::wk_weather_service_availability,
464            "WeatherService.weather(for: including: .availability)",
465        )?;
466        WeatherAvailability::from_owned_ptr(ptr)
467    }
468
469    /// Fetches one WeatherKit query result.
470    pub fn weather_including(
471        &self,
472        location: &CLLocation,
473        query: WeatherQuery,
474    ) -> Result<WeatherQueryResult, WeatherKitError> {
475        query.fetch(*self, location)
476    }
477
478    /// Fetches 2 WeatherKit query results in order.
479    pub fn weather_including2(
480        &self,
481        location: &CLLocation,
482        query1: WeatherQuery,
483        query2: WeatherQuery,
484    ) -> Result<(WeatherQueryResult, WeatherQueryResult), WeatherKitError> {
485        Ok((
486            query1.fetch(*self, location)?,
487            query2.fetch(*self, location)?,
488        ))
489    }
490
491    /// Fetches 3 WeatherKit query results in order.
492    pub fn weather_including3(
493        &self,
494        location: &CLLocation,
495        query1: WeatherQuery,
496        query2: WeatherQuery,
497        query3: WeatherQuery,
498    ) -> Result<(WeatherQueryResult, WeatherQueryResult, WeatherQueryResult), WeatherKitError> {
499        Ok((
500            query1.fetch(*self, location)?,
501            query2.fetch(*self, location)?,
502            query3.fetch(*self, location)?,
503        ))
504    }
505
506    /// Fetches 4 WeatherKit query results in order.
507    pub fn weather_including4(
508        &self,
509        location: &CLLocation,
510        query1: WeatherQuery,
511        query2: WeatherQuery,
512        query3: WeatherQuery,
513        query4: WeatherQuery,
514    ) -> Result<
515        (
516            WeatherQueryResult,
517            WeatherQueryResult,
518            WeatherQueryResult,
519            WeatherQueryResult,
520        ),
521        WeatherKitError,
522    > {
523        Ok((
524            query1.fetch(*self, location)?,
525            query2.fetch(*self, location)?,
526            query3.fetch(*self, location)?,
527            query4.fetch(*self, location)?,
528        ))
529    }
530
531    /// Fetches 5 WeatherKit query results in order.
532    pub fn weather_including5(
533        &self,
534        location: &CLLocation,
535        query1: WeatherQuery,
536        query2: WeatherQuery,
537        query3: WeatherQuery,
538        query4: WeatherQuery,
539        query5: WeatherQuery,
540    ) -> Result<
541        (
542            WeatherQueryResult,
543            WeatherQueryResult,
544            WeatherQueryResult,
545            WeatherQueryResult,
546            WeatherQueryResult,
547        ),
548        WeatherKitError,
549    > {
550        Ok((
551            query1.fetch(*self, location)?,
552            query2.fetch(*self, location)?,
553            query3.fetch(*self, location)?,
554            query4.fetch(*self, location)?,
555            query5.fetch(*self, location)?,
556        ))
557    }
558
559    /// Fetches 6 WeatherKit query results in order.
560    #[allow(clippy::too_many_arguments)]
561    pub fn weather_including6(
562        &self,
563        location: &CLLocation,
564        query1: WeatherQuery,
565        query2: WeatherQuery,
566        query3: WeatherQuery,
567        query4: WeatherQuery,
568        query5: WeatherQuery,
569        query6: WeatherQuery,
570    ) -> Result<
571        (
572            WeatherQueryResult,
573            WeatherQueryResult,
574            WeatherQueryResult,
575            WeatherQueryResult,
576            WeatherQueryResult,
577            WeatherQueryResult,
578        ),
579        WeatherKitError,
580    > {
581        Ok((
582            query1.fetch(*self, location)?,
583            query2.fetch(*self, location)?,
584            query3.fetch(*self, location)?,
585            query4.fetch(*self, location)?,
586            query5.fetch(*self, location)?,
587            query6.fetch(*self, location)?,
588        ))
589    }
590
591    /// Fetches an arbitrary list of WeatherKit query results in order.
592    pub fn weather_including_many<I>(
593        &self,
594        location: &CLLocation,
595        queries: I,
596    ) -> Result<Vec<WeatherQueryResult>, WeatherKitError>
597    where
598        I: IntoIterator<Item = WeatherQuery>,
599    {
600        queries
601            .into_iter()
602            .map(|query| query.fetch(*self, location))
603            .collect()
604    }
605
606    /// Fetches the optional WeatherKit weather changes payload.
607    pub fn weather_changes(
608        &self,
609        location: &CLLocation,
610    ) -> Result<Option<WeatherChanges>, WeatherKitError> {
611        let ptr = self.fetch_location_handle(
612            location,
613            ffi::changes::wk_weather_service_weather_changes,
614            "WeatherService.weather(for: including: .changes)",
615        )?;
616        WeatherChanges::option_from_owned_ptr(ptr)
617    }
618
619    /// Fetches the optional WeatherKit historical comparisons payload.
620    pub fn historical_comparisons(
621        &self,
622        location: &CLLocation,
623    ) -> Result<Option<HistoricalComparisons>, WeatherKitError> {
624        let ptr = self.fetch_location_handle(
625            location,
626            ffi::changes::wk_weather_service_historical_comparisons,
627            "WeatherService.weather(for: including: .historicalComparisons)",
628        )?;
629        HistoricalComparisons::option_from_owned_ptr(ptr)
630    }
631
632    /// Fetches WeatherKit daily statistics for the requested query.
633    pub fn daily_statistics(
634        &self,
635        location: &CLLocation,
636        query: DailyWeatherStatisticsQuery,
637    ) -> Result<DailyWeatherStatisticsResult, WeatherKitError> {
638        self.daily_statistics_with_scope(location, query, QueryScope::None)
639    }
640
641    /// Fetches WeatherKit daily statistics for the given interval.
642    pub fn daily_statistics_in(
643        &self,
644        location: &CLLocation,
645        interval: DateInterval,
646        query: DailyWeatherStatisticsQuery,
647    ) -> Result<DailyWeatherStatisticsResult, WeatherKitError> {
648        self.daily_statistics_with_scope(location, query, QueryScope::Interval(&interval))
649    }
650
651    /// Fetches WeatherKit daily statistics for the given day range.
652    pub fn daily_statistics_between_days(
653        &self,
654        location: &CLLocation,
655        start_day: i64,
656        end_day: i64,
657        query: DailyWeatherStatisticsQuery,
658    ) -> Result<DailyWeatherStatisticsResult, WeatherKitError> {
659        self.daily_statistics_with_scope(
660            location,
661            query,
662            QueryScope::Index {
663                start: start_day,
664                end: end_day,
665            },
666        )
667    }
668
669    /// Fetches the WeatherKit daily summary for the requested query.
670    pub fn daily_summary(
671        &self,
672        location: &CLLocation,
673        query: DailyWeatherSummaryQuery,
674    ) -> Result<DailyWeatherSummaryResult, WeatherKitError> {
675        self.daily_summary_with_scope(location, query, QueryScope::None)
676    }
677
678    /// Fetches the WeatherKit daily summary for the given interval.
679    pub fn daily_summary_in(
680        &self,
681        location: &CLLocation,
682        interval: DateInterval,
683        query: DailyWeatherSummaryQuery,
684    ) -> Result<DailyWeatherSummaryResult, WeatherKitError> {
685        self.daily_summary_with_scope(location, query, QueryScope::Interval(&interval))
686    }
687
688    /// Fetches WeatherKit hourly statistics for the requested query.
689    pub fn hourly_statistics(
690        &self,
691        location: &CLLocation,
692        query: HourlyWeatherStatisticsQuery,
693    ) -> Result<HourlyWeatherStatistics<HourTemperatureStatistics>, WeatherKitError> {
694        self.hourly_statistics_with_scope(location, query, QueryScope::None)
695    }
696
697    /// Fetches WeatherKit hourly statistics for the given interval.
698    pub fn hourly_statistics_in(
699        &self,
700        location: &CLLocation,
701        interval: DateInterval,
702        query: HourlyWeatherStatisticsQuery,
703    ) -> Result<HourlyWeatherStatistics<HourTemperatureStatistics>, WeatherKitError> {
704        self.hourly_statistics_with_scope(location, query, QueryScope::Interval(&interval))
705    }
706
707    /// Fetches WeatherKit hourly statistics for the given hour range.
708    pub fn hourly_statistics_between_hours(
709        &self,
710        location: &CLLocation,
711        start_hour: i64,
712        end_hour: i64,
713        query: HourlyWeatherStatisticsQuery,
714    ) -> Result<HourlyWeatherStatistics<HourTemperatureStatistics>, WeatherKitError> {
715        self.hourly_statistics_with_scope(
716            location,
717            query,
718            QueryScope::Index {
719                start: start_hour,
720                end: end_hour,
721            },
722        )
723    }
724
725    /// Fetches WeatherKit monthly statistics for the requested query.
726    pub fn monthly_statistics(
727        &self,
728        location: &CLLocation,
729        query: MonthlyWeatherStatisticsQuery,
730    ) -> Result<MonthlyWeatherStatisticsResult, WeatherKitError> {
731        self.monthly_statistics_with_scope(location, query, QueryScope::None)
732    }
733
734    /// Fetches WeatherKit monthly statistics for the given interval.
735    pub fn monthly_statistics_in(
736        &self,
737        location: &CLLocation,
738        interval: DateInterval,
739        query: MonthlyWeatherStatisticsQuery,
740    ) -> Result<MonthlyWeatherStatisticsResult, WeatherKitError> {
741        self.monthly_statistics_with_scope(location, query, QueryScope::Interval(&interval))
742    }
743
744    /// Fetches WeatherKit monthly statistics for the given month range.
745    pub fn monthly_statistics_between_months(
746        &self,
747        location: &CLLocation,
748        start_month: i64,
749        end_month: i64,
750        query: MonthlyWeatherStatisticsQuery,
751    ) -> Result<MonthlyWeatherStatisticsResult, WeatherKitError> {
752        self.monthly_statistics_with_scope(
753            location,
754            query,
755            QueryScope::Index {
756                start: start_month,
757                end: end_month,
758            },
759        )
760    }
761
762    /// Returns today's WeatherKit sun events from the daily forecast.
763    pub fn sun_events(&self, location: &CLLocation) -> Result<SunEvents, WeatherKitError> {
764        let forecast = self.daily_forecast(location)?;
765        forecast
766            .forecast
767            .first()
768            .map(|day| day.sun.clone())
769            .ok_or_else(|| WeatherKitError::bridge(-1, "daily forecast returned no days"))
770    }
771
772    /// Returns today's WeatherKit moon events from the daily forecast.
773    pub fn moon_events(&self, location: &CLLocation) -> Result<MoonEvents, WeatherKitError> {
774        let forecast = self.daily_forecast(location)?;
775        forecast
776            .forecast
777            .first()
778            .map(|day| day.moon.clone())
779            .ok_or_else(|| WeatherKitError::bridge(-1, "daily forecast returned no days"))
780    }
781
782    /// Returns the current WeatherKit pressure reading.
783    pub fn pressure(&self, location: &CLLocation) -> Result<Pressure, WeatherKitError> {
784        Ok(self.current_weather(location)?.pressure_reading())
785    }
786
787    fn daily_statistics_with_scope(
788        &self,
789        location: &CLLocation,
790        query: DailyWeatherStatisticsQuery,
791        scope: QueryScope<'_>,
792    ) -> Result<DailyWeatherStatisticsResult, WeatherKitError> {
793        let ptr = self.fetch_scoped_query_handle(
794            location,
795            query.query_kind(),
796            scope,
797            ffi::statistics::wk_weather_service_daily_statistics,
798            "WeatherService.dailyStatistics",
799        )?;
800        match query {
801            DailyWeatherStatisticsQuery::Temperature => {
802                Ok(DailyWeatherStatisticsResult::Temperature(
803                    DailyWeatherStatistics::<DayTemperatureStatistics>::from_owned_ptr(ptr)?,
804                ))
805            }
806            DailyWeatherStatisticsQuery::Precipitation => {
807                Ok(DailyWeatherStatisticsResult::Precipitation(
808                    DailyWeatherStatistics::<DayPrecipitationStatistics>::from_owned_ptr(ptr)?,
809                ))
810            }
811        }
812    }
813
814    fn daily_summary_with_scope(
815        &self,
816        location: &CLLocation,
817        query: DailyWeatherSummaryQuery,
818        scope: QueryScope<'_>,
819    ) -> Result<DailyWeatherSummaryResult, WeatherKitError> {
820        let ptr = self.fetch_scoped_query_handle(
821            location,
822            query.query_kind(),
823            scope,
824            ffi::statistics::wk_weather_service_daily_summary,
825            "WeatherService.dailySummary",
826        )?;
827        match query {
828            DailyWeatherSummaryQuery::Temperature => Ok(DailyWeatherSummaryResult::Temperature(
829                DailyWeatherSummary::<DayTemperatureSummary>::from_owned_ptr(ptr)?,
830            )),
831            DailyWeatherSummaryQuery::Precipitation => {
832                Ok(DailyWeatherSummaryResult::Precipitation(
833                    DailyWeatherSummary::<DayPrecipitationSummary>::from_owned_ptr(ptr)?,
834                ))
835            }
836        }
837    }
838
839    fn hourly_statistics_with_scope(
840        &self,
841        location: &CLLocation,
842        query: HourlyWeatherStatisticsQuery,
843        scope: QueryScope<'_>,
844    ) -> Result<HourlyWeatherStatistics<HourTemperatureStatistics>, WeatherKitError> {
845        let ptr = self.fetch_scoped_query_handle(
846            location,
847            query.query_kind(),
848            scope,
849            ffi::statistics::wk_weather_service_hourly_statistics,
850            "WeatherService.hourlyStatistics",
851        )?;
852        HourlyWeatherStatistics::<HourTemperatureStatistics>::from_owned_ptr(ptr)
853    }
854
855    fn monthly_statistics_with_scope(
856        &self,
857        location: &CLLocation,
858        query: MonthlyWeatherStatisticsQuery,
859        scope: QueryScope<'_>,
860    ) -> Result<MonthlyWeatherStatisticsResult, WeatherKitError> {
861        let ptr = self.fetch_scoped_query_handle(
862            location,
863            query.query_kind(),
864            scope,
865            ffi::statistics::wk_weather_service_monthly_statistics,
866            "WeatherService.monthlyStatistics",
867        )?;
868        match query {
869            MonthlyWeatherStatisticsQuery::Temperature => {
870                Ok(MonthlyWeatherStatisticsResult::Temperature(
871                    MonthlyWeatherStatistics::<MonthTemperatureStatistics>::from_owned_ptr(ptr)?,
872                ))
873            }
874            MonthlyWeatherStatisticsQuery::Precipitation => {
875                Ok(MonthlyWeatherStatisticsResult::Precipitation(
876                    MonthlyWeatherStatistics::<MonthPrecipitationStatistics>::from_owned_ptr(ptr)?,
877                ))
878            }
879        }
880    }
881
882    fn fetch_scoped_query_handle(
883        &self,
884        location: &CLLocation,
885        query_kind: i32,
886        scope: QueryScope<'_>,
887        call: ScopedQueryFetchFn,
888        context: &str,
889    ) -> Result<*mut c_void, WeatherKitError> {
890        location.validate()?;
891        let service = ServiceHandle::acquire(self.kind)?;
892        let mut out_handle = core::ptr::null_mut();
893        let mut out_error = core::ptr::null_mut();
894        let (scope_kind, start_seconds, end_seconds, start_index, end_index) = match scope {
895            QueryScope::None => (0, 0.0, 0.0, 0, 0),
896            QueryScope::Interval(interval) => {
897                (1, interval.start_seconds()?, interval.end_seconds()?, 0, 0)
898            }
899            QueryScope::Index { start, end } => {
900                validate_index_range(start, end, context)?;
901                (2, 0.0, 0.0, start, end)
902            }
903        };
904        let status = unsafe {
905            // SAFETY: service.as_ptr() is a valid retained handle from
906            // ServiceHandle::acquire; out_handle and out_error are valid
907            // stack-allocated output pointers.
908            call(
909                service.as_ptr(),
910                location.latitude,
911                location.longitude,
912                query_kind,
913                scope_kind,
914                start_seconds,
915                end_seconds,
916                start_index,
917                end_index,
918                &mut out_handle,
919                &mut out_error,
920            )
921        };
922        if status != ffi::status::OK {
923            // SAFETY: error_from_status takes ownership of out_error (C string
924            // allocated by the Swift bridge) and frees it via wk_string_free.
925            return Err(unsafe { error_from_status(status, out_error) });
926        }
927        if out_handle.is_null() {
928            return Err(WeatherKitError::bridge(
929                -1,
930                format!("missing handle for {context}"),
931            ));
932        }
933        Ok(out_handle)
934    }
935
936    fn fetch_service_handle(
937        &self,
938        call: ServiceFetchFn,
939        context: &str,
940    ) -> Result<*mut c_void, WeatherKitError> {
941        let service = ServiceHandle::acquire(self.kind)?;
942        let mut out_handle = core::ptr::null_mut();
943        let mut out_error = core::ptr::null_mut();
944        // SAFETY: service.as_ptr() is a valid retained handle; out_handle and
945        // out_error are valid stack-allocated output pointers.
946        let status = unsafe { call(service.as_ptr(), &mut out_handle, &mut out_error) };
947        if status != ffi::status::OK {
948            // SAFETY: error_from_status takes ownership of the C string and frees it.
949            return Err(unsafe { error_from_status(status, out_error) });
950        }
951        if out_handle.is_null() {
952            return Err(WeatherKitError::bridge(
953                -1,
954                format!("missing handle for {context}"),
955            ));
956        }
957        Ok(out_handle)
958    }
959
960    fn fetch_location_handle(
961        &self,
962        location: &CLLocation,
963        call: LocationFetchFn,
964        context: &str,
965    ) -> Result<*mut c_void, WeatherKitError> {
966        location.validate()?;
967        let service = ServiceHandle::acquire(self.kind)?;
968        let mut out_handle = core::ptr::null_mut();
969        let mut out_error = core::ptr::null_mut();
970        // SAFETY: service.as_ptr() is a valid retained handle; out_handle and
971        // out_error are valid stack-allocated output pointers.
972        let status = unsafe {
973            call(
974                service.as_ptr(),
975                location.latitude,
976                location.longitude,
977                &mut out_handle,
978                &mut out_error,
979            )
980        };
981        if status != ffi::status::OK {
982            // SAFETY: error_from_status takes ownership of the C string and frees it.
983            return Err(unsafe { error_from_status(status, out_error) });
984        }
985        if out_handle.is_null() {
986            return Err(WeatherKitError::bridge(
987                -1,
988                format!("missing handle for {context}"),
989            ));
990        }
991        Ok(out_handle)
992    }
993
994    fn fetch_interval_handle(
995        &self,
996        location: &CLLocation,
997        interval: Option<&DateInterval>,
998        call: IntervalFetchFn,
999        context: &str,
1000    ) -> Result<*mut c_void, WeatherKitError> {
1001        location.validate()?;
1002        let service = ServiceHandle::acquire(self.kind)?;
1003        let mut out_handle = core::ptr::null_mut();
1004        let mut out_error = core::ptr::null_mut();
1005        let (has_range, start_seconds, end_seconds) = if let Some(interval) = interval {
1006            (1, interval.start_seconds()?, interval.end_seconds()?)
1007        } else {
1008            (0, 0.0, 0.0)
1009        };
1010        // SAFETY: service.as_ptr() is a valid retained handle; out_handle and
1011        // out_error are valid stack-allocated output pointers.
1012        let status = unsafe {
1013            call(
1014                service.as_ptr(),
1015                location.latitude,
1016                location.longitude,
1017                has_range,
1018                start_seconds,
1019                end_seconds,
1020                &mut out_handle,
1021                &mut out_error,
1022            )
1023        };
1024        if status != ffi::status::OK {
1025            // SAFETY: error_from_status takes ownership of the C string and frees it.
1026            return Err(unsafe { error_from_status(status, out_error) });
1027        }
1028        if out_handle.is_null() {
1029            return Err(WeatherKitError::bridge(
1030                -1,
1031                format!("missing handle for {context}"),
1032            ));
1033        }
1034        Ok(out_handle)
1035    }
1036}
1037
1038fn validate_index_range(start: i64, end: i64, context: &str) -> Result<(), WeatherKitError> {
1039    if start > end {
1040        return Err(WeatherKitError::bridge(
1041            -1,
1042            format!("{context} start index must not be after end index"),
1043        ));
1044    }
1045    Ok(())
1046}
1047
1048fn unix_seconds(time: SystemTime) -> Result<f64, WeatherKitError> {
1049    let duration = time.duration_since(UNIX_EPOCH).map_err(|error| {
1050        WeatherKitError::bridge(-1, format!("time {time:?} is before UNIX_EPOCH: {error}"))
1051    })?;
1052    Ok(duration.as_secs_f64())
1053}