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