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