rjw_metoffice/lib.rs
1//! `rjw-metoffice` helps construct URLs for and parse the response from the Met Office Global Spot
2//! site-specific weather forecast API. Global Spot provides deterministic forecasts for 20,000
3//! locations worldwide, giving the "most likely" prediction for a given place.
4//!
5//! You will need a [Weather DataHub] API key to use the service, which can be obtained
6//! for free from the [website][Weather DataHub], and permits 360 API requests per day.
7//!
8//! [Weather DataHub]: https://datahub.metoffice.gov.uk/
9//!
10//! ## Quickstart
11//!
12//! Here's a simple example using the [`ureq`] blocking HTTP client to look up hourly forecasts for
13//! [Sana’a], Yemen.
14//!
15//! [`ureq`]: https://crates.io/crates/ureq
16//! [Sana’a]: https://whc.unesco.org/en/list/385
17//!
18//! ```no_run
19//! # use rjw_metoffice::{Forecast, Hourly};
20//! # fn main() -> anyhow::Result<()> {
21//! # let API_KEY: &str = "";
22//! let [lat, lon] = [15.348333, 44.206389];
23//! let url = Forecast::<Hourly>::url_for_location(lat.try_into()?, lon.try_into()?);
24//! let response = ureq::get(url.to_string())
25//! .header("apikey", API_KEY)
26//! .call()?;
27//! let json_bytes = response.into_body().read_to_vec()?;
28//! let forecast: Forecast<Hourly> = json_bytes.as_slice().try_into()?;
29//! for hour in forecast.predictions {
30//! println!("{} {}", hour.time.in_tz("Asia/Riyadh")?, hour.temperature)
31//! }
32//! # Ok(())
33//! # }
34//! ````
35//!
36//! ## HTTP client agnostic
37//!
38//! The library doesn't perform any IO itself, so you can use whichever HTTP client you like, and
39//! use it in both synchronous or asynchronous contexts. In effect, you "bring your own HTTP
40//! client": use the library to construct a location-specific URL, use your HTTP client to make the
41//! request, and pass the received bytes to the library for parsing.
42//!
43//! ## API key HTTP header
44//!
45//! You **must** add a `apikey` header to the HTTP request containing your Met Office Weather
46//! DataHub API key. This library cannot do this for you, so please consult the documentation for
47//! the HTTP client you are using.
48//!
49//! ## Forecast time periods and the `Forecast` struct
50//!
51//! The Global Spot API offers forecasts at three levels of granularity: hourly (for 48
52//! hours), three-hourly (168 hours, ie 7 days), and daily (7 days).
53//!
54//! These time periods are used as a generic parameter to the [`Forecast`] struct, which which
55//! contains general information and a vector of time-period-specific structs that hold the
56//! prediction data. This generic parameter is also needed when constructing a URL, for instance
57//! here we construct URLs for all three time periods for "[null island]".
58//!
59//! [null island]: https://en.wikipedia.org/wiki/Null_Island
60//!
61//! ```
62//! # use rjw_metoffice::{Forecast, Hourly, ThreeHourly, Daily, Latitude, Longitude};
63//! let lat = Latitude::new(0.0).unwrap();
64//! let lon = Longitude::new(0.0).unwrap();
65//! let hourly_url = Forecast::<Hourly>::url_for_location(lat, lon);
66//! let three_hourly_url = Forecast::<ThreeHourly>::url_for_location(lat, lon);
67//! let daily_url = Forecast::<Daily>::url_for_location(lat, lon);
68//! ````
69//!
70//! Similarly, the generic is used to determine parsing behaviour via string or byte slices
71//! (`FromStr` or `TryFrom<&[u8]>`).
72//!
73//! ## Prediction field names
74//!
75//! Generally, the Met Office field names are not used. This is to make field names consistent
76//! across time point structs (versus, for example, `feelsLikeTemperature` in hourly forecasts and
77//! `feelsLikeTemp` in three-hourly forecasts), as well as to provide arguably more intuitive and
78//! discoverable names (for example, `pressure` instead of `mslp`).
79//!
80//! Please see the documentation for each time point for a table mapping field names back to the
81//! originals that are present in the Met Office JSON and official API documentation.
82//!
83//! If you feel particularly strongly that anything is misnamed, please open an issue.
84//!
85//! ## Missing data
86//!
87//! **Hourly** forecasts contain 49 separate sets of predictions (start + 48 hours), but I have
88//! observed that the final 3 hours are missing five data points (of 19):
89//!
90//! - Maximum temperature
91//! - Minimum temperature
92//! - Total precipitation
93//! - Total snowfall
94//! - Maximum gust speed over the previous hour
95//!
96//! As well, these seem to be missing from some forecast locations entirely. These are represented
97//! as `Option`s in the [`Hourly`] struct. If you are interested only in particular locations, I
98//! recommend checking which data you receive at which time points, after which you should be OK to
99//! `unwrap` the reliable ones.
100//!
101//! [`Hourly`]: crate::Hourly
102//!
103//! Meanwhile, **[daily]** forecasts have an 8-element time series (previous day + 7 days). The
104//! first of these is missing 10 daytime data points (of 21; there are 21 predictions each for day
105//! and night). Because this is such a big difference, the [`Day`] predictions are an enum of
106//! either the [past] day (missing predictions) or [future] days (with full predictions).
107//!
108//! [daily]: crate::daily::Daily
109//! [`Day`]: crate::daily::Day
110//! [past]: crate::daily::Day::Past
111//! [future]: crate::daily::Day::Future
112//!
113//! ## Memory usage
114//!
115//! While this crate is `no_std`, it still requires a memory allocator (ie, uses `alloc`).
116//! The JSON returned from the Met Office spot APIs is roughly 24 KiB for the hourly
117//! forecasts, 28 KiB for three-hourly, and 12 KiB for daily. The `Forecast` struct takes
118//! a bit under 10 KiB for hourly and three-hourly forecasts, and 2 KiB for daily.
119//! The JSON parsing does allocate, so you'll want to budget JSON + `Forecast`.
120
121#![no_std]
122
123extern crate alloc;
124
125pub mod daily;
126mod error;
127mod forecast;
128mod hourly;
129mod parse;
130mod sealed;
131mod three_hourly;
132pub mod units;
133
134pub use daily::Daily;
135pub use error::Error;
136pub use forecast::Forecast;
137pub use hourly::Hourly;
138pub use sealed::TimePeriod;
139pub use three_hourly::ThreeHourly;
140pub use units::{Latitude, Longitude};