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    #[cfg(feature = "async")]
335    pub(crate) const fn is_owned(self) -> bool {
336        matches!(self.kind, ServiceKind::Owned)
337    }
338
339    /// Fetches WeatherKit attribution information.
340    pub fn attribution(&self) -> Result<WeatherAttribution, WeatherKitError> {
341        let ptr = self.fetch_service_handle(
342            ffi::service::wk_weather_service_attribution,
343            "WeatherService.attribution",
344        )?;
345        WeatherAttribution::from_owned_ptr(ptr)
346    }
347
348    /// Fetches the aggregate WeatherKit weather payload.
349    pub fn weather(&self, location: &CLLocation) -> Result<Weather, WeatherKitError> {
350        let ptr = self.fetch_location_handle(
351            location,
352            ffi::service::wk_weather_service_weather,
353            "WeatherService.weather(for:)",
354        )?;
355        parse_json_from_handle(
356            ptr,
357            ffi::service::wk_weather_release,
358            ffi::service::wk_weather_copy_json,
359            "weather",
360        )
361    }
362
363    /// Fetches the WeatherKit current weather payload.
364    pub fn current_weather(
365        &self,
366        location: &CLLocation,
367    ) -> Result<CurrentWeather, WeatherKitError> {
368        let ptr = self.fetch_location_handle(
369            location,
370            ffi::service::wk_weather_service_current_weather,
371            "WeatherService.weather(for: including: .current)",
372        )?;
373        CurrentWeather::from_owned_ptr(ptr)
374    }
375
376    /// Fetches the WeatherKit hourly forecast.
377    pub fn hourly_forecast(
378        &self,
379        location: &CLLocation,
380    ) -> Result<HourlyForecast, WeatherKitError> {
381        let ptr = self.fetch_interval_handle(
382            location,
383            None,
384            ffi::service::wk_weather_service_hourly_forecast,
385            "WeatherService.weather(for: including: .hourly)",
386        )?;
387        HourlyForecast::from_owned_ptr(ptr)
388    }
389
390    /// Fetches the WeatherKit hourly forecast for the given interval.
391    pub fn hourly_forecast_in(
392        &self,
393        location: &CLLocation,
394        interval: DateInterval,
395    ) -> Result<HourlyForecast, WeatherKitError> {
396        let ptr = self.fetch_interval_handle(
397            location,
398            Some(&interval),
399            ffi::service::wk_weather_service_hourly_forecast,
400            "WeatherService.weather(for: including: .hourly(startDate:endDate))",
401        )?;
402        HourlyForecast::from_owned_ptr(ptr)
403    }
404
405    /// Fetches the WeatherKit daily forecast.
406    pub fn daily_forecast(&self, location: &CLLocation) -> Result<DailyForecast, WeatherKitError> {
407        let ptr = self.fetch_interval_handle(
408            location,
409            None,
410            ffi::service::wk_weather_service_daily_forecast,
411            "WeatherService.weather(for: including: .daily)",
412        )?;
413        DailyForecast::from_owned_ptr(ptr)
414    }
415
416    /// Fetches the WeatherKit daily forecast for the given interval.
417    pub fn daily_forecast_in(
418        &self,
419        location: &CLLocation,
420        interval: DateInterval,
421    ) -> Result<DailyForecast, WeatherKitError> {
422        let ptr = self.fetch_interval_handle(
423            location,
424            Some(&interval),
425            ffi::service::wk_weather_service_daily_forecast,
426            "WeatherService.weather(for: including: .daily(startDate:endDate))",
427        )?;
428        DailyForecast::from_owned_ptr(ptr)
429    }
430
431    /// Fetches the optional WeatherKit minute forecast.
432    pub fn minute_forecast(
433        &self,
434        location: &CLLocation,
435    ) -> Result<Option<MinuteForecastCollection>, WeatherKitError> {
436        let ptr = self.fetch_location_handle(
437            location,
438            ffi::service::wk_weather_service_minute_forecast,
439            "WeatherService.weather(for: including: .minute)",
440        )?;
441        MinuteForecastCollection::option_from_owned_ptr(ptr)
442    }
443
444    /// Fetches WeatherKit weather alerts.
445    pub fn weather_alerts(
446        &self,
447        location: &CLLocation,
448    ) -> Result<Vec<WeatherAlert>, WeatherKitError> {
449        let ptr = self.fetch_location_handle(
450            location,
451            ffi::service::wk_weather_service_weather_alerts,
452            "WeatherService.weather(for: including: .alerts)",
453        )?;
454        alerts_from_owned_ptr(ptr)
455    }
456
457    /// Fetches WeatherKit data availability for the location.
458    pub fn availability(
459        &self,
460        location: &CLLocation,
461    ) -> Result<WeatherAvailability, WeatherKitError> {
462        let ptr = self.fetch_location_handle(
463            location,
464            ffi::service::wk_weather_service_availability,
465            "WeatherService.weather(for: including: .availability)",
466        )?;
467        WeatherAvailability::from_owned_ptr(ptr)
468    }
469
470    /// Fetches one WeatherKit query result.
471    pub fn weather_including(
472        &self,
473        location: &CLLocation,
474        query: WeatherQuery,
475    ) -> Result<WeatherQueryResult, WeatherKitError> {
476        query.fetch(*self, location)
477    }
478
479    /// Fetches 2 WeatherKit query results in order.
480    pub fn weather_including2(
481        &self,
482        location: &CLLocation,
483        query1: WeatherQuery,
484        query2: WeatherQuery,
485    ) -> Result<(WeatherQueryResult, WeatherQueryResult), WeatherKitError> {
486        Ok((
487            query1.fetch(*self, location)?,
488            query2.fetch(*self, location)?,
489        ))
490    }
491
492    /// Fetches 3 WeatherKit query results in order.
493    pub fn weather_including3(
494        &self,
495        location: &CLLocation,
496        query1: WeatherQuery,
497        query2: WeatherQuery,
498        query3: WeatherQuery,
499    ) -> Result<(WeatherQueryResult, WeatherQueryResult, WeatherQueryResult), WeatherKitError> {
500        Ok((
501            query1.fetch(*self, location)?,
502            query2.fetch(*self, location)?,
503            query3.fetch(*self, location)?,
504        ))
505    }
506
507    /// Fetches 4 WeatherKit query results in order.
508    pub fn weather_including4(
509        &self,
510        location: &CLLocation,
511        query1: WeatherQuery,
512        query2: WeatherQuery,
513        query3: WeatherQuery,
514        query4: WeatherQuery,
515    ) -> Result<
516        (
517            WeatherQueryResult,
518            WeatherQueryResult,
519            WeatherQueryResult,
520            WeatherQueryResult,
521        ),
522        WeatherKitError,
523    > {
524        Ok((
525            query1.fetch(*self, location)?,
526            query2.fetch(*self, location)?,
527            query3.fetch(*self, location)?,
528            query4.fetch(*self, location)?,
529        ))
530    }
531
532    /// Fetches 5 WeatherKit query results in order.
533    pub fn weather_including5(
534        &self,
535        location: &CLLocation,
536        query1: WeatherQuery,
537        query2: WeatherQuery,
538        query3: WeatherQuery,
539        query4: WeatherQuery,
540        query5: WeatherQuery,
541    ) -> Result<
542        (
543            WeatherQueryResult,
544            WeatherQueryResult,
545            WeatherQueryResult,
546            WeatherQueryResult,
547            WeatherQueryResult,
548        ),
549        WeatherKitError,
550    > {
551        Ok((
552            query1.fetch(*self, location)?,
553            query2.fetch(*self, location)?,
554            query3.fetch(*self, location)?,
555            query4.fetch(*self, location)?,
556            query5.fetch(*self, location)?,
557        ))
558    }
559
560    /// Fetches 6 WeatherKit query results in order.
561    #[allow(clippy::too_many_arguments)]
562    pub fn weather_including6(
563        &self,
564        location: &CLLocation,
565        query1: WeatherQuery,
566        query2: WeatherQuery,
567        query3: WeatherQuery,
568        query4: WeatherQuery,
569        query5: WeatherQuery,
570        query6: WeatherQuery,
571    ) -> Result<
572        (
573            WeatherQueryResult,
574            WeatherQueryResult,
575            WeatherQueryResult,
576            WeatherQueryResult,
577            WeatherQueryResult,
578            WeatherQueryResult,
579        ),
580        WeatherKitError,
581    > {
582        Ok((
583            query1.fetch(*self, location)?,
584            query2.fetch(*self, location)?,
585            query3.fetch(*self, location)?,
586            query4.fetch(*self, location)?,
587            query5.fetch(*self, location)?,
588            query6.fetch(*self, location)?,
589        ))
590    }
591
592    /// Fetches an arbitrary list of WeatherKit query results in order.
593    pub fn weather_including_many<I>(
594        &self,
595        location: &CLLocation,
596        queries: I,
597    ) -> Result<Vec<WeatherQueryResult>, WeatherKitError>
598    where
599        I: IntoIterator<Item = WeatherQuery>,
600    {
601        queries
602            .into_iter()
603            .map(|query| query.fetch(*self, location))
604            .collect()
605    }
606
607    /// Fetches the optional WeatherKit weather changes payload.
608    pub fn weather_changes(
609        &self,
610        location: &CLLocation,
611    ) -> Result<Option<WeatherChanges>, WeatherKitError> {
612        let ptr = self.fetch_location_handle(
613            location,
614            ffi::changes::wk_weather_service_weather_changes,
615            "WeatherService.weather(for: including: .changes)",
616        )?;
617        WeatherChanges::option_from_owned_ptr(ptr)
618    }
619
620    /// Fetches the optional WeatherKit historical comparisons payload.
621    pub fn historical_comparisons(
622        &self,
623        location: &CLLocation,
624    ) -> Result<Option<HistoricalComparisons>, WeatherKitError> {
625        let ptr = self.fetch_location_handle(
626            location,
627            ffi::changes::wk_weather_service_historical_comparisons,
628            "WeatherService.weather(for: including: .historicalComparisons)",
629        )?;
630        HistoricalComparisons::option_from_owned_ptr(ptr)
631    }
632
633    /// Fetches WeatherKit daily statistics for the requested query.
634    pub fn daily_statistics(
635        &self,
636        location: &CLLocation,
637        query: DailyWeatherStatisticsQuery,
638    ) -> Result<DailyWeatherStatisticsResult, WeatherKitError> {
639        self.daily_statistics_with_scope(location, query, QueryScope::None)
640    }
641
642    /// Fetches WeatherKit daily statistics for the given interval.
643    pub fn daily_statistics_in(
644        &self,
645        location: &CLLocation,
646        interval: DateInterval,
647        query: DailyWeatherStatisticsQuery,
648    ) -> Result<DailyWeatherStatisticsResult, WeatherKitError> {
649        self.daily_statistics_with_scope(location, query, QueryScope::Interval(&interval))
650    }
651
652    /// Fetches WeatherKit daily statistics for the given day range.
653    pub fn daily_statistics_between_days(
654        &self,
655        location: &CLLocation,
656        start_day: i64,
657        end_day: i64,
658        query: DailyWeatherStatisticsQuery,
659    ) -> Result<DailyWeatherStatisticsResult, WeatherKitError> {
660        self.daily_statistics_with_scope(
661            location,
662            query,
663            QueryScope::Index {
664                start: start_day,
665                end: end_day,
666            },
667        )
668    }
669
670    /// Fetches the WeatherKit daily summary for the requested query.
671    pub fn daily_summary(
672        &self,
673        location: &CLLocation,
674        query: DailyWeatherSummaryQuery,
675    ) -> Result<DailyWeatherSummaryResult, WeatherKitError> {
676        self.daily_summary_with_scope(location, query, QueryScope::None)
677    }
678
679    /// Fetches the WeatherKit daily summary for the given interval.
680    pub fn daily_summary_in(
681        &self,
682        location: &CLLocation,
683        interval: DateInterval,
684        query: DailyWeatherSummaryQuery,
685    ) -> Result<DailyWeatherSummaryResult, WeatherKitError> {
686        self.daily_summary_with_scope(location, query, QueryScope::Interval(&interval))
687    }
688
689    /// Fetches WeatherKit hourly statistics for the requested query.
690    pub fn hourly_statistics(
691        &self,
692        location: &CLLocation,
693        query: HourlyWeatherStatisticsQuery,
694    ) -> Result<HourlyWeatherStatistics<HourTemperatureStatistics>, WeatherKitError> {
695        self.hourly_statistics_with_scope(location, query, QueryScope::None)
696    }
697
698    /// Fetches WeatherKit hourly statistics for the given interval.
699    pub fn hourly_statistics_in(
700        &self,
701        location: &CLLocation,
702        interval: DateInterval,
703        query: HourlyWeatherStatisticsQuery,
704    ) -> Result<HourlyWeatherStatistics<HourTemperatureStatistics>, WeatherKitError> {
705        self.hourly_statistics_with_scope(location, query, QueryScope::Interval(&interval))
706    }
707
708    /// Fetches WeatherKit hourly statistics for the given hour range.
709    pub fn hourly_statistics_between_hours(
710        &self,
711        location: &CLLocation,
712        start_hour: i64,
713        end_hour: i64,
714        query: HourlyWeatherStatisticsQuery,
715    ) -> Result<HourlyWeatherStatistics<HourTemperatureStatistics>, WeatherKitError> {
716        self.hourly_statistics_with_scope(
717            location,
718            query,
719            QueryScope::Index {
720                start: start_hour,
721                end: end_hour,
722            },
723        )
724    }
725
726    /// Fetches WeatherKit monthly statistics for the requested query.
727    pub fn monthly_statistics(
728        &self,
729        location: &CLLocation,
730        query: MonthlyWeatherStatisticsQuery,
731    ) -> Result<MonthlyWeatherStatisticsResult, WeatherKitError> {
732        self.monthly_statistics_with_scope(location, query, QueryScope::None)
733    }
734
735    /// Fetches WeatherKit monthly statistics for the given interval.
736    pub fn monthly_statistics_in(
737        &self,
738        location: &CLLocation,
739        interval: DateInterval,
740        query: MonthlyWeatherStatisticsQuery,
741    ) -> Result<MonthlyWeatherStatisticsResult, WeatherKitError> {
742        self.monthly_statistics_with_scope(location, query, QueryScope::Interval(&interval))
743    }
744
745    /// Fetches WeatherKit monthly statistics for the given month range.
746    pub fn monthly_statistics_between_months(
747        &self,
748        location: &CLLocation,
749        start_month: i64,
750        end_month: i64,
751        query: MonthlyWeatherStatisticsQuery,
752    ) -> Result<MonthlyWeatherStatisticsResult, WeatherKitError> {
753        self.monthly_statistics_with_scope(
754            location,
755            query,
756            QueryScope::Index {
757                start: start_month,
758                end: end_month,
759            },
760        )
761    }
762
763    /// Returns today's WeatherKit sun events from the daily forecast.
764    pub fn sun_events(&self, location: &CLLocation) -> Result<SunEvents, WeatherKitError> {
765        let forecast = self.daily_forecast(location)?;
766        forecast
767            .forecast
768            .first()
769            .map(|day| day.sun.clone())
770            .ok_or_else(|| WeatherKitError::bridge(-1, "daily forecast returned no days"))
771    }
772
773    /// Returns today's WeatherKit moon events from the daily forecast.
774    pub fn moon_events(&self, location: &CLLocation) -> Result<MoonEvents, WeatherKitError> {
775        let forecast = self.daily_forecast(location)?;
776        forecast
777            .forecast
778            .first()
779            .map(|day| day.moon.clone())
780            .ok_or_else(|| WeatherKitError::bridge(-1, "daily forecast returned no days"))
781    }
782
783    /// Returns the current WeatherKit pressure reading.
784    pub fn pressure(&self, location: &CLLocation) -> Result<Pressure, WeatherKitError> {
785        Ok(self.current_weather(location)?.pressure_reading())
786    }
787
788    fn daily_statistics_with_scope(
789        &self,
790        location: &CLLocation,
791        query: DailyWeatherStatisticsQuery,
792        scope: QueryScope<'_>,
793    ) -> Result<DailyWeatherStatisticsResult, WeatherKitError> {
794        let ptr = self.fetch_scoped_query_handle(
795            location,
796            query.query_kind(),
797            scope,
798            ffi::statistics::wk_weather_service_daily_statistics,
799            "WeatherService.dailyStatistics",
800        )?;
801        match query {
802            DailyWeatherStatisticsQuery::Temperature => {
803                Ok(DailyWeatherStatisticsResult::Temperature(
804                    DailyWeatherStatistics::<DayTemperatureStatistics>::from_owned_ptr(ptr)?,
805                ))
806            }
807            DailyWeatherStatisticsQuery::Precipitation => {
808                Ok(DailyWeatherStatisticsResult::Precipitation(
809                    DailyWeatherStatistics::<DayPrecipitationStatistics>::from_owned_ptr(ptr)?,
810                ))
811            }
812        }
813    }
814
815    fn daily_summary_with_scope(
816        &self,
817        location: &CLLocation,
818        query: DailyWeatherSummaryQuery,
819        scope: QueryScope<'_>,
820    ) -> Result<DailyWeatherSummaryResult, WeatherKitError> {
821        let ptr = self.fetch_scoped_query_handle(
822            location,
823            query.query_kind(),
824            scope,
825            ffi::statistics::wk_weather_service_daily_summary,
826            "WeatherService.dailySummary",
827        )?;
828        match query {
829            DailyWeatherSummaryQuery::Temperature => Ok(DailyWeatherSummaryResult::Temperature(
830                DailyWeatherSummary::<DayTemperatureSummary>::from_owned_ptr(ptr)?,
831            )),
832            DailyWeatherSummaryQuery::Precipitation => {
833                Ok(DailyWeatherSummaryResult::Precipitation(
834                    DailyWeatherSummary::<DayPrecipitationSummary>::from_owned_ptr(ptr)?,
835                ))
836            }
837        }
838    }
839
840    fn hourly_statistics_with_scope(
841        &self,
842        location: &CLLocation,
843        query: HourlyWeatherStatisticsQuery,
844        scope: QueryScope<'_>,
845    ) -> Result<HourlyWeatherStatistics<HourTemperatureStatistics>, WeatherKitError> {
846        let ptr = self.fetch_scoped_query_handle(
847            location,
848            query.query_kind(),
849            scope,
850            ffi::statistics::wk_weather_service_hourly_statistics,
851            "WeatherService.hourlyStatistics",
852        )?;
853        HourlyWeatherStatistics::<HourTemperatureStatistics>::from_owned_ptr(ptr)
854    }
855
856    fn monthly_statistics_with_scope(
857        &self,
858        location: &CLLocation,
859        query: MonthlyWeatherStatisticsQuery,
860        scope: QueryScope<'_>,
861    ) -> Result<MonthlyWeatherStatisticsResult, WeatherKitError> {
862        let ptr = self.fetch_scoped_query_handle(
863            location,
864            query.query_kind(),
865            scope,
866            ffi::statistics::wk_weather_service_monthly_statistics,
867            "WeatherService.monthlyStatistics",
868        )?;
869        match query {
870            MonthlyWeatherStatisticsQuery::Temperature => {
871                Ok(MonthlyWeatherStatisticsResult::Temperature(
872                    MonthlyWeatherStatistics::<MonthTemperatureStatistics>::from_owned_ptr(ptr)?,
873                ))
874            }
875            MonthlyWeatherStatisticsQuery::Precipitation => {
876                Ok(MonthlyWeatherStatisticsResult::Precipitation(
877                    MonthlyWeatherStatistics::<MonthPrecipitationStatistics>::from_owned_ptr(ptr)?,
878                ))
879            }
880        }
881    }
882
883    fn fetch_scoped_query_handle(
884        &self,
885        location: &CLLocation,
886        query_kind: i32,
887        scope: QueryScope<'_>,
888        call: ScopedQueryFetchFn,
889        context: &str,
890    ) -> Result<*mut c_void, WeatherKitError> {
891        location.validate()?;
892        let service = ServiceHandle::acquire(self.kind)?;
893        let mut out_handle = core::ptr::null_mut();
894        let mut out_error = core::ptr::null_mut();
895        let (scope_kind, start_seconds, end_seconds, start_index, end_index) = match scope {
896            QueryScope::None => (0, 0.0, 0.0, 0, 0),
897            QueryScope::Interval(interval) => {
898                (1, interval.start_seconds()?, interval.end_seconds()?, 0, 0)
899            }
900            QueryScope::Index { start, end } => {
901                validate_index_range(start, end, context)?;
902                (2, 0.0, 0.0, start, end)
903            }
904        };
905        let status = unsafe {
906            // SAFETY: service.as_ptr() is a valid retained handle from
907            // ServiceHandle::acquire; out_handle and out_error are valid
908            // stack-allocated output pointers.
909            call(
910                service.as_ptr(),
911                location.latitude,
912                location.longitude,
913                query_kind,
914                scope_kind,
915                start_seconds,
916                end_seconds,
917                start_index,
918                end_index,
919                &mut out_handle,
920                &mut out_error,
921            )
922        };
923        if status != ffi::status::OK {
924            // SAFETY: error_from_status takes ownership of out_error (C string
925            // allocated by the Swift bridge) and frees it via wk_string_free.
926            return Err(unsafe { error_from_status(status, out_error) });
927        }
928        if out_handle.is_null() {
929            return Err(WeatherKitError::bridge(
930                -1,
931                format!("missing handle for {context}"),
932            ));
933        }
934        Ok(out_handle)
935    }
936
937    fn fetch_service_handle(
938        &self,
939        call: ServiceFetchFn,
940        context: &str,
941    ) -> Result<*mut c_void, WeatherKitError> {
942        let service = ServiceHandle::acquire(self.kind)?;
943        let mut out_handle = core::ptr::null_mut();
944        let mut out_error = core::ptr::null_mut();
945        // SAFETY: service.as_ptr() is a valid retained handle; out_handle and
946        // out_error are valid stack-allocated output pointers.
947        let status = unsafe { call(service.as_ptr(), &mut out_handle, &mut out_error) };
948        if status != ffi::status::OK {
949            // SAFETY: error_from_status takes ownership of the C string and frees it.
950            return Err(unsafe { error_from_status(status, out_error) });
951        }
952        if out_handle.is_null() {
953            return Err(WeatherKitError::bridge(
954                -1,
955                format!("missing handle for {context}"),
956            ));
957        }
958        Ok(out_handle)
959    }
960
961    fn fetch_location_handle(
962        &self,
963        location: &CLLocation,
964        call: LocationFetchFn,
965        context: &str,
966    ) -> Result<*mut c_void, WeatherKitError> {
967        location.validate()?;
968        let service = ServiceHandle::acquire(self.kind)?;
969        let mut out_handle = core::ptr::null_mut();
970        let mut out_error = core::ptr::null_mut();
971        // SAFETY: service.as_ptr() is a valid retained handle; out_handle and
972        // out_error are valid stack-allocated output pointers.
973        let status = unsafe {
974            call(
975                service.as_ptr(),
976                location.latitude,
977                location.longitude,
978                &mut out_handle,
979                &mut out_error,
980            )
981        };
982        if status != ffi::status::OK {
983            // SAFETY: error_from_status takes ownership of the C string and frees it.
984            return Err(unsafe { error_from_status(status, out_error) });
985        }
986        if out_handle.is_null() {
987            return Err(WeatherKitError::bridge(
988                -1,
989                format!("missing handle for {context}"),
990            ));
991        }
992        Ok(out_handle)
993    }
994
995    fn fetch_interval_handle(
996        &self,
997        location: &CLLocation,
998        interval: Option<&DateInterval>,
999        call: IntervalFetchFn,
1000        context: &str,
1001    ) -> Result<*mut c_void, WeatherKitError> {
1002        location.validate()?;
1003        let service = ServiceHandle::acquire(self.kind)?;
1004        let mut out_handle = core::ptr::null_mut();
1005        let mut out_error = core::ptr::null_mut();
1006        let (has_range, start_seconds, end_seconds) = if let Some(interval) = interval {
1007            (1, interval.start_seconds()?, interval.end_seconds()?)
1008        } else {
1009            (0, 0.0, 0.0)
1010        };
1011        // SAFETY: service.as_ptr() is a valid retained handle; out_handle and
1012        // out_error are valid stack-allocated output pointers.
1013        let status = unsafe {
1014            call(
1015                service.as_ptr(),
1016                location.latitude,
1017                location.longitude,
1018                has_range,
1019                start_seconds,
1020                end_seconds,
1021                &mut out_handle,
1022                &mut out_error,
1023            )
1024        };
1025        if status != ffi::status::OK {
1026            // SAFETY: error_from_status takes ownership of the C string and frees it.
1027            return Err(unsafe { error_from_status(status, out_error) });
1028        }
1029        if out_handle.is_null() {
1030            return Err(WeatherKitError::bridge(
1031                -1,
1032                format!("missing handle for {context}"),
1033            ));
1034        }
1035        Ok(out_handle)
1036    }
1037}
1038
1039fn validate_index_range(start: i64, end: i64, context: &str) -> Result<(), WeatherKitError> {
1040    if start > end {
1041        return Err(WeatherKitError::bridge(
1042            -1,
1043            format!("{context} start index must not be after end index"),
1044        ));
1045    }
1046    Ok(())
1047}
1048
1049fn unix_seconds(time: SystemTime) -> Result<f64, WeatherKitError> {
1050    let duration = time.duration_since(UNIX_EPOCH).map_err(|error| {
1051        WeatherKitError::bridge(-1, format!("time {time:?} is before UNIX_EPOCH: {error}"))
1052    })?;
1053    Ok(duration.as_secs_f64())
1054}