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        let ptr = unsafe {
242            match kind {
243                ServiceKind::Shared => ffi::service::wk_weather_service_shared(),
244                ServiceKind::Owned => ffi::service::wk_weather_service_new(),
245            }
246        };
247        if ptr.is_null() {
248            Err(WeatherKitError::bridge(
249                -1,
250                "failed to acquire WeatherService handle",
251            ))
252        } else {
253            Ok(Self { ptr })
254        }
255    }
256
257    fn as_ptr(&self) -> *mut c_void {
258        self.ptr
259    }
260}
261
262impl Drop for ServiceHandle {
263    fn drop(&mut self) {
264        if !self.ptr.is_null() {
265            unsafe {
266                ffi::service::wk_weather_service_release(self.ptr);
267            }
268            self.ptr = core::ptr::null_mut();
269        }
270    }
271}
272
273impl WeatherService {
274    pub const fn shared() -> Self {
275        Self {
276            kind: ServiceKind::Shared,
277        }
278    }
279
280    pub const fn new() -> Self {
281        Self {
282            kind: ServiceKind::Owned,
283        }
284    }
285
286    pub fn attribution(&self) -> Result<WeatherAttribution, WeatherKitError> {
287        let ptr = self.fetch_service_handle(
288            ffi::service::wk_weather_service_attribution,
289            "WeatherService.attribution",
290        )?;
291        WeatherAttribution::from_owned_ptr(ptr)
292    }
293
294    pub fn weather(&self, location: &CLLocation) -> Result<Weather, WeatherKitError> {
295        let ptr = self.fetch_location_handle(
296            location,
297            ffi::service::wk_weather_service_weather,
298            "WeatherService.weather(for:)",
299        )?;
300        parse_json_from_handle(
301            ptr,
302            ffi::service::wk_weather_release,
303            ffi::service::wk_weather_copy_json,
304            "weather",
305        )
306    }
307
308    pub fn current_weather(
309        &self,
310        location: &CLLocation,
311    ) -> Result<CurrentWeather, WeatherKitError> {
312        let ptr = self.fetch_location_handle(
313            location,
314            ffi::service::wk_weather_service_current_weather,
315            "WeatherService.weather(for: including: .current)",
316        )?;
317        CurrentWeather::from_owned_ptr(ptr)
318    }
319
320    pub fn hourly_forecast(
321        &self,
322        location: &CLLocation,
323    ) -> Result<HourlyForecast, WeatherKitError> {
324        let ptr = self.fetch_interval_handle(
325            location,
326            None,
327            ffi::service::wk_weather_service_hourly_forecast,
328            "WeatherService.weather(for: including: .hourly)",
329        )?;
330        HourlyForecast::from_owned_ptr(ptr)
331    }
332
333    pub fn hourly_forecast_in(
334        &self,
335        location: &CLLocation,
336        interval: DateInterval,
337    ) -> Result<HourlyForecast, WeatherKitError> {
338        let ptr = self.fetch_interval_handle(
339            location,
340            Some(&interval),
341            ffi::service::wk_weather_service_hourly_forecast,
342            "WeatherService.weather(for: including: .hourly(startDate:endDate))",
343        )?;
344        HourlyForecast::from_owned_ptr(ptr)
345    }
346
347    pub fn daily_forecast(&self, location: &CLLocation) -> Result<DailyForecast, WeatherKitError> {
348        let ptr = self.fetch_interval_handle(
349            location,
350            None,
351            ffi::service::wk_weather_service_daily_forecast,
352            "WeatherService.weather(for: including: .daily)",
353        )?;
354        DailyForecast::from_owned_ptr(ptr)
355    }
356
357    pub fn daily_forecast_in(
358        &self,
359        location: &CLLocation,
360        interval: DateInterval,
361    ) -> Result<DailyForecast, WeatherKitError> {
362        let ptr = self.fetch_interval_handle(
363            location,
364            Some(&interval),
365            ffi::service::wk_weather_service_daily_forecast,
366            "WeatherService.weather(for: including: .daily(startDate:endDate))",
367        )?;
368        DailyForecast::from_owned_ptr(ptr)
369    }
370
371    pub fn minute_forecast(
372        &self,
373        location: &CLLocation,
374    ) -> Result<Option<MinuteForecastCollection>, WeatherKitError> {
375        let ptr = self.fetch_location_handle(
376            location,
377            ffi::service::wk_weather_service_minute_forecast,
378            "WeatherService.weather(for: including: .minute)",
379        )?;
380        MinuteForecastCollection::option_from_owned_ptr(ptr)
381    }
382
383    pub fn weather_alerts(
384        &self,
385        location: &CLLocation,
386    ) -> Result<Vec<WeatherAlert>, WeatherKitError> {
387        let ptr = self.fetch_location_handle(
388            location,
389            ffi::service::wk_weather_service_weather_alerts,
390            "WeatherService.weather(for: including: .alerts)",
391        )?;
392        alerts_from_owned_ptr(ptr)
393    }
394
395    pub fn availability(
396        &self,
397        location: &CLLocation,
398    ) -> Result<WeatherAvailability, WeatherKitError> {
399        let ptr = self.fetch_location_handle(
400            location,
401            ffi::service::wk_weather_service_availability,
402            "WeatherService.weather(for: including: .availability)",
403        )?;
404        WeatherAvailability::from_owned_ptr(ptr)
405    }
406
407    pub fn weather_including(
408        &self,
409        location: &CLLocation,
410        query: WeatherQuery,
411    ) -> Result<WeatherQueryResult, WeatherKitError> {
412        query.fetch(*self, location)
413    }
414
415    pub fn weather_including2(
416        &self,
417        location: &CLLocation,
418        query1: WeatherQuery,
419        query2: WeatherQuery,
420    ) -> Result<(WeatherQueryResult, WeatherQueryResult), WeatherKitError> {
421        Ok((query1.fetch(*self, location)?, query2.fetch(*self, location)?))
422    }
423
424    pub fn weather_including3(
425        &self,
426        location: &CLLocation,
427        query1: WeatherQuery,
428        query2: WeatherQuery,
429        query3: WeatherQuery,
430    ) -> Result<(WeatherQueryResult, WeatherQueryResult, WeatherQueryResult), WeatherKitError> {
431        Ok((
432            query1.fetch(*self, location)?,
433            query2.fetch(*self, location)?,
434            query3.fetch(*self, location)?,
435        ))
436    }
437
438    pub fn weather_including4(
439        &self,
440        location: &CLLocation,
441        query1: WeatherQuery,
442        query2: WeatherQuery,
443        query3: WeatherQuery,
444        query4: WeatherQuery,
445    ) -> Result<
446        (
447            WeatherQueryResult,
448            WeatherQueryResult,
449            WeatherQueryResult,
450            WeatherQueryResult,
451        ),
452        WeatherKitError,
453    > {
454        Ok((
455            query1.fetch(*self, location)?,
456            query2.fetch(*self, location)?,
457            query3.fetch(*self, location)?,
458            query4.fetch(*self, location)?,
459        ))
460    }
461
462    pub fn weather_including5(
463        &self,
464        location: &CLLocation,
465        query1: WeatherQuery,
466        query2: WeatherQuery,
467        query3: WeatherQuery,
468        query4: WeatherQuery,
469        query5: WeatherQuery,
470    ) -> Result<
471        (
472            WeatherQueryResult,
473            WeatherQueryResult,
474            WeatherQueryResult,
475            WeatherQueryResult,
476            WeatherQueryResult,
477        ),
478        WeatherKitError,
479    > {
480        Ok((
481            query1.fetch(*self, location)?,
482            query2.fetch(*self, location)?,
483            query3.fetch(*self, location)?,
484            query4.fetch(*self, location)?,
485            query5.fetch(*self, location)?,
486        ))
487    }
488
489    #[allow(clippy::too_many_arguments)]
490    pub fn weather_including6(
491        &self,
492        location: &CLLocation,
493        query1: WeatherQuery,
494        query2: WeatherQuery,
495        query3: WeatherQuery,
496        query4: WeatherQuery,
497        query5: WeatherQuery,
498        query6: WeatherQuery,
499    ) -> Result<
500        (
501            WeatherQueryResult,
502            WeatherQueryResult,
503            WeatherQueryResult,
504            WeatherQueryResult,
505            WeatherQueryResult,
506            WeatherQueryResult,
507        ),
508        WeatherKitError,
509    > {
510        Ok((
511            query1.fetch(*self, location)?,
512            query2.fetch(*self, location)?,
513            query3.fetch(*self, location)?,
514            query4.fetch(*self, location)?,
515            query5.fetch(*self, location)?,
516            query6.fetch(*self, location)?,
517        ))
518    }
519
520    pub fn weather_including_many<I>(
521        &self,
522        location: &CLLocation,
523        queries: I,
524    ) -> Result<Vec<WeatherQueryResult>, WeatherKitError>
525    where
526        I: IntoIterator<Item = WeatherQuery>,
527    {
528        queries
529            .into_iter()
530            .map(|query| query.fetch(*self, location))
531            .collect()
532    }
533
534    pub fn weather_changes(
535        &self,
536        location: &CLLocation,
537    ) -> Result<Option<WeatherChanges>, WeatherKitError> {
538        let ptr = self.fetch_location_handle(
539            location,
540            ffi::changes::wk_weather_service_weather_changes,
541            "WeatherService.weather(for: including: .changes)",
542        )?;
543        WeatherChanges::option_from_owned_ptr(ptr)
544    }
545
546    pub fn historical_comparisons(
547        &self,
548        location: &CLLocation,
549    ) -> Result<Option<HistoricalComparisons>, WeatherKitError> {
550        let ptr = self.fetch_location_handle(
551            location,
552            ffi::changes::wk_weather_service_historical_comparisons,
553            "WeatherService.weather(for: including: .historicalComparisons)",
554        )?;
555        HistoricalComparisons::option_from_owned_ptr(ptr)
556    }
557
558    pub fn daily_statistics(
559        &self,
560        location: &CLLocation,
561        query: DailyWeatherStatisticsQuery,
562    ) -> Result<DailyWeatherStatisticsResult, WeatherKitError> {
563        self.daily_statistics_with_scope(location, query, QueryScope::None)
564    }
565
566    pub fn daily_statistics_in(
567        &self,
568        location: &CLLocation,
569        interval: DateInterval,
570        query: DailyWeatherStatisticsQuery,
571    ) -> Result<DailyWeatherStatisticsResult, WeatherKitError> {
572        self.daily_statistics_with_scope(location, query, QueryScope::Interval(&interval))
573    }
574
575    pub fn daily_statistics_between_days(
576        &self,
577        location: &CLLocation,
578        start_day: i64,
579        end_day: i64,
580        query: DailyWeatherStatisticsQuery,
581    ) -> Result<DailyWeatherStatisticsResult, WeatherKitError> {
582        self.daily_statistics_with_scope(
583            location,
584            query,
585            QueryScope::Index {
586                start: start_day,
587                end: end_day,
588            },
589        )
590    }
591
592    pub fn daily_summary(
593        &self,
594        location: &CLLocation,
595        query: DailyWeatherSummaryQuery,
596    ) -> Result<DailyWeatherSummaryResult, WeatherKitError> {
597        self.daily_summary_with_scope(location, query, QueryScope::None)
598    }
599
600    pub fn daily_summary_in(
601        &self,
602        location: &CLLocation,
603        interval: DateInterval,
604        query: DailyWeatherSummaryQuery,
605    ) -> Result<DailyWeatherSummaryResult, WeatherKitError> {
606        self.daily_summary_with_scope(location, query, QueryScope::Interval(&interval))
607    }
608
609    pub fn hourly_statistics(
610        &self,
611        location: &CLLocation,
612        query: HourlyWeatherStatisticsQuery,
613    ) -> Result<HourlyWeatherStatistics<HourTemperatureStatistics>, WeatherKitError> {
614        self.hourly_statistics_with_scope(location, query, QueryScope::None)
615    }
616
617    pub fn hourly_statistics_in(
618        &self,
619        location: &CLLocation,
620        interval: DateInterval,
621        query: HourlyWeatherStatisticsQuery,
622    ) -> Result<HourlyWeatherStatistics<HourTemperatureStatistics>, WeatherKitError> {
623        self.hourly_statistics_with_scope(location, query, QueryScope::Interval(&interval))
624    }
625
626    pub fn hourly_statistics_between_hours(
627        &self,
628        location: &CLLocation,
629        start_hour: i64,
630        end_hour: i64,
631        query: HourlyWeatherStatisticsQuery,
632    ) -> Result<HourlyWeatherStatistics<HourTemperatureStatistics>, WeatherKitError> {
633        self.hourly_statistics_with_scope(
634            location,
635            query,
636            QueryScope::Index {
637                start: start_hour,
638                end: end_hour,
639            },
640        )
641    }
642
643    pub fn monthly_statistics(
644        &self,
645        location: &CLLocation,
646        query: MonthlyWeatherStatisticsQuery,
647    ) -> Result<MonthlyWeatherStatisticsResult, WeatherKitError> {
648        self.monthly_statistics_with_scope(location, query, QueryScope::None)
649    }
650
651    pub fn monthly_statistics_in(
652        &self,
653        location: &CLLocation,
654        interval: DateInterval,
655        query: MonthlyWeatherStatisticsQuery,
656    ) -> Result<MonthlyWeatherStatisticsResult, WeatherKitError> {
657        self.monthly_statistics_with_scope(location, query, QueryScope::Interval(&interval))
658    }
659
660    pub fn monthly_statistics_between_months(
661        &self,
662        location: &CLLocation,
663        start_month: i64,
664        end_month: i64,
665        query: MonthlyWeatherStatisticsQuery,
666    ) -> Result<MonthlyWeatherStatisticsResult, WeatherKitError> {
667        self.monthly_statistics_with_scope(
668            location,
669            query,
670            QueryScope::Index {
671                start: start_month,
672                end: end_month,
673            },
674        )
675    }
676
677    pub fn sun_events(&self, location: &CLLocation) -> Result<SunEvents, WeatherKitError> {
678        let forecast = self.daily_forecast(location)?;
679        forecast
680            .forecast
681            .first()
682            .map(|day| day.sun.clone())
683            .ok_or_else(|| WeatherKitError::bridge(-1, "daily forecast returned no days"))
684    }
685
686    pub fn moon_events(&self, location: &CLLocation) -> Result<MoonEvents, WeatherKitError> {
687        let forecast = self.daily_forecast(location)?;
688        forecast
689            .forecast
690            .first()
691            .map(|day| day.moon.clone())
692            .ok_or_else(|| WeatherKitError::bridge(-1, "daily forecast returned no days"))
693    }
694
695    pub fn pressure(&self, location: &CLLocation) -> Result<Pressure, WeatherKitError> {
696        Ok(self.current_weather(location)?.pressure_reading())
697    }
698
699    fn daily_statistics_with_scope(
700        &self,
701        location: &CLLocation,
702        query: DailyWeatherStatisticsQuery,
703        scope: QueryScope<'_>,
704    ) -> Result<DailyWeatherStatisticsResult, WeatherKitError> {
705        let ptr = self.fetch_scoped_query_handle(
706            location,
707            query.query_kind(),
708            scope,
709            ffi::statistics::wk_weather_service_daily_statistics,
710            "WeatherService.dailyStatistics",
711        )?;
712        match query {
713            DailyWeatherStatisticsQuery::Temperature => Ok(DailyWeatherStatisticsResult::Temperature(
714                DailyWeatherStatistics::<DayTemperatureStatistics>::from_owned_ptr(ptr)?,
715            )),
716            DailyWeatherStatisticsQuery::Precipitation => Ok(
717                DailyWeatherStatisticsResult::Precipitation(
718                    DailyWeatherStatistics::<DayPrecipitationStatistics>::from_owned_ptr(ptr)?,
719                ),
720            ),
721        }
722    }
723
724    fn daily_summary_with_scope(
725        &self,
726        location: &CLLocation,
727        query: DailyWeatherSummaryQuery,
728        scope: QueryScope<'_>,
729    ) -> Result<DailyWeatherSummaryResult, WeatherKitError> {
730        let ptr = self.fetch_scoped_query_handle(
731            location,
732            query.query_kind(),
733            scope,
734            ffi::statistics::wk_weather_service_daily_summary,
735            "WeatherService.dailySummary",
736        )?;
737        match query {
738            DailyWeatherSummaryQuery::Temperature => Ok(DailyWeatherSummaryResult::Temperature(
739                DailyWeatherSummary::<DayTemperatureSummary>::from_owned_ptr(ptr)?,
740            )),
741            DailyWeatherSummaryQuery::Precipitation => Ok(
742                DailyWeatherSummaryResult::Precipitation(
743                    DailyWeatherSummary::<DayPrecipitationSummary>::from_owned_ptr(ptr)?,
744                ),
745            ),
746        }
747    }
748
749    fn hourly_statistics_with_scope(
750        &self,
751        location: &CLLocation,
752        query: HourlyWeatherStatisticsQuery,
753        scope: QueryScope<'_>,
754    ) -> Result<HourlyWeatherStatistics<HourTemperatureStatistics>, WeatherKitError> {
755        let ptr = self.fetch_scoped_query_handle(
756            location,
757            query.query_kind(),
758            scope,
759            ffi::statistics::wk_weather_service_hourly_statistics,
760            "WeatherService.hourlyStatistics",
761        )?;
762        HourlyWeatherStatistics::<HourTemperatureStatistics>::from_owned_ptr(ptr)
763    }
764
765    fn monthly_statistics_with_scope(
766        &self,
767        location: &CLLocation,
768        query: MonthlyWeatherStatisticsQuery,
769        scope: QueryScope<'_>,
770    ) -> Result<MonthlyWeatherStatisticsResult, WeatherKitError> {
771        let ptr = self.fetch_scoped_query_handle(
772            location,
773            query.query_kind(),
774            scope,
775            ffi::statistics::wk_weather_service_monthly_statistics,
776            "WeatherService.monthlyStatistics",
777        )?;
778        match query {
779            MonthlyWeatherStatisticsQuery::Temperature => Ok(
780                MonthlyWeatherStatisticsResult::Temperature(
781                    MonthlyWeatherStatistics::<MonthTemperatureStatistics>::from_owned_ptr(ptr)?,
782                ),
783            ),
784            MonthlyWeatherStatisticsQuery::Precipitation => Ok(
785                MonthlyWeatherStatisticsResult::Precipitation(
786                    MonthlyWeatherStatistics::<MonthPrecipitationStatistics>::from_owned_ptr(ptr)?,
787                ),
788            ),
789        }
790    }
791
792    fn fetch_scoped_query_handle(
793        &self,
794        location: &CLLocation,
795        query_kind: i32,
796        scope: QueryScope<'_>,
797        call: ScopedQueryFetchFn,
798        context: &str,
799    ) -> Result<*mut c_void, WeatherKitError> {
800        location.validate()?;
801        let service = ServiceHandle::acquire(self.kind)?;
802        let mut out_handle = core::ptr::null_mut();
803        let mut out_error = core::ptr::null_mut();
804        let (scope_kind, start_seconds, end_seconds, start_index, end_index) = match scope {
805            QueryScope::None => (0, 0.0, 0.0, 0, 0),
806            QueryScope::Interval(interval) => (1, interval.start_seconds()?, interval.end_seconds()?, 0, 0),
807            QueryScope::Index { start, end } => {
808                validate_index_range(start, end, context)?;
809                (2, 0.0, 0.0, start, end)
810            }
811        };
812        let status = unsafe {
813            call(
814                service.as_ptr(),
815                location.latitude,
816                location.longitude,
817                query_kind,
818                scope_kind,
819                start_seconds,
820                end_seconds,
821                start_index,
822                end_index,
823                &mut out_handle,
824                &mut out_error,
825            )
826        };
827        if status != ffi::status::OK {
828            return Err(unsafe { error_from_status(status, out_error) });
829        }
830        if out_handle.is_null() {
831            return Err(WeatherKitError::bridge(
832                -1,
833                format!("missing handle for {context}"),
834            ));
835        }
836        Ok(out_handle)
837    }
838
839    fn fetch_service_handle(
840        &self,
841        call: ServiceFetchFn,
842        context: &str,
843    ) -> Result<*mut c_void, WeatherKitError> {
844        let service = ServiceHandle::acquire(self.kind)?;
845        let mut out_handle = core::ptr::null_mut();
846        let mut out_error = core::ptr::null_mut();
847        let status = unsafe { call(service.as_ptr(), &mut out_handle, &mut out_error) };
848        if status != ffi::status::OK {
849            return Err(unsafe { error_from_status(status, out_error) });
850        }
851        if out_handle.is_null() {
852            return Err(WeatherKitError::bridge(
853                -1,
854                format!("missing handle for {context}"),
855            ));
856        }
857        Ok(out_handle)
858    }
859
860    fn fetch_location_handle(
861        &self,
862        location: &CLLocation,
863        call: LocationFetchFn,
864        context: &str,
865    ) -> Result<*mut c_void, WeatherKitError> {
866        location.validate()?;
867        let service = ServiceHandle::acquire(self.kind)?;
868        let mut out_handle = core::ptr::null_mut();
869        let mut out_error = core::ptr::null_mut();
870        let status = unsafe {
871            call(
872                service.as_ptr(),
873                location.latitude,
874                location.longitude,
875                &mut out_handle,
876                &mut out_error,
877            )
878        };
879        if status != ffi::status::OK {
880            return Err(unsafe { error_from_status(status, out_error) });
881        }
882        if out_handle.is_null() {
883            return Err(WeatherKitError::bridge(
884                -1,
885                format!("missing handle for {context}"),
886            ));
887        }
888        Ok(out_handle)
889    }
890
891    fn fetch_interval_handle(
892        &self,
893        location: &CLLocation,
894        interval: Option<&DateInterval>,
895        call: IntervalFetchFn,
896        context: &str,
897    ) -> Result<*mut c_void, WeatherKitError> {
898        location.validate()?;
899        let service = ServiceHandle::acquire(self.kind)?;
900        let mut out_handle = core::ptr::null_mut();
901        let mut out_error = core::ptr::null_mut();
902        let (has_range, start_seconds, end_seconds) = if let Some(interval) = interval {
903            (1, interval.start_seconds()?, interval.end_seconds()?)
904        } else {
905            (0, 0.0, 0.0)
906        };
907        let status = unsafe {
908            call(
909                service.as_ptr(),
910                location.latitude,
911                location.longitude,
912                has_range,
913                start_seconds,
914                end_seconds,
915                &mut out_handle,
916                &mut out_error,
917            )
918        };
919        if status != ffi::status::OK {
920            return Err(unsafe { error_from_status(status, out_error) });
921        }
922        if out_handle.is_null() {
923            return Err(WeatherKitError::bridge(
924                -1,
925                format!("missing handle for {context}"),
926            ));
927        }
928        Ok(out_handle)
929    }
930}
931
932fn validate_index_range(start: i64, end: i64, context: &str) -> Result<(), WeatherKitError> {
933    if start > end {
934        return Err(WeatherKitError::bridge(
935            -1,
936            format!("{context} start index must not be after end index"),
937        ));
938    }
939    Ok(())
940}
941
942fn unix_seconds(time: SystemTime) -> Result<f64, WeatherKitError> {
943    let duration = time.duration_since(UNIX_EPOCH).map_err(|error| {
944        WeatherKitError::bridge(-1, format!("time {time:?} is before UNIX_EPOCH: {error}"))
945    })?;
946    Ok(duration.as_secs_f64())
947}