Here comes the sun
Little darling, it’s been a long, cold, lonely winter.
—George Harrison, “Here Comes the Sun”
We’re better together, and that’s as true of software engineers as it was of the Beatles. None of us is as smart as all of us, and this is why in my book The Secrets of Rust: Tools, I encourage thinking about every program we write as a kind of global collaboration. By designing our Rust programs as modular, reusable components—crates—and publishing them to the universal library, we make it possible for others to connect our software with their own.
The results of these collaborative efforts are better than any of us could have achieved by ourselves, and the same principle applies to our programs, too: when they connect with each other, they can do great things. So one of the most important skills in software engineering is being able to write programs that talk to other programs, often over network connections and using protocols like HTTP and JSON.
The Weatherstack API
For example, let’s think about a Rust program that gets the current weather conditions for your location (or any location). There are several suitable public APIs for this, and we don’t need anything fancy, so we’ll start with weatherstack.com.
Most services like this require some kind of API key, or authentication token, either for billing or just to limit usage. Weatherstack is no exception, so in order to use it we’ll first need to sign up for at least a free account.
The free plan limits us to 100 requests a month, but that’s okay: we probably won’t be needing to check the weather all that often in practice. We’ll just have to be a little bit careful while developing the tool that we don’t accidentally use up all our quota, but I don’t think it’ll be a problem.
Once we’re signed up, we can get our access key, which will look something like this:
f4ac3e4c75d34cc2be60b0628e7b2ecc
This is essentially our password to the Weatherstack service, so we’ll want to keep it secret, and we also don’t want to embed it in our program. Instead, we’ll need to provide a way for users to supply their own API key when they run the program (for example, in an environment variable).
Making HTTP requests with
reqwest
Let’s start by writing the simplest imaginable weather tool, then: one that makes an HTTP request to the Weatherstack API, using a suitable key, and prints the results.
As usual, there’s a crate that makes this sort of thing pretty easy:
it’s called reqwest
(yes, with a “w”). Let’s start a new
project and add reqwest
as a dependency:
cargo add reqwest -F blocking
Don’t worry about what the blocking
feature does for the
moment. Let’s get a sketch version working first.
A first sketch
Here’s our first attempt:
use std::env;
fn main() {
let api_key = env::var("WEATHERSTACK_API_KEY").unwrap();
let resp = reqwest::blocking::Client::new()
.get("https://api.weatherstack.com/current")
.query(&[("query", "London,UK"), ("access_key", &api_key)])
.send()
.unwrap();
println!("{}", resp.text().unwrap());
}
We use env::var
to get the API key from an environment
variable (its name is up to us, but this seems like a reasonable
choice).
Next, we use Client::new()
to create a new
reqwest
client, which is the thing we’ll use to make the
HTTP request. To do that, we call the client’s get
method
with the Weatherstack URL. To this we add a couple of URL parameters:
query
(the location we want the weather for), and, of
course, access_key
, so that we can pass our API key.
Calling send()
actually makes the request, and, since
that could fail, we need to unwrap()
the result to get the
HTTP response, which we’ll call resp
. And we’ll just print
whatever the response body contains for now, using
resp.text()
.
This program sucks
You may be fuming with righteous anger by now, thinking “But this code is terrible! It’s not an importable crate, it doesn’t handle errors properly, and it doesn’t let users specify their location!”
And you fume some good points, dear reader. Do register your complaints on Hacker News by all means (“Worst. Rust. Ever.”), but let me say one thing. In The Secrets of Rust: Tools, one of the insider secrets I reveal is that it’s okay to write terrible code. At least, at first. In fact, it’s a great idea to deliberately write a sketch version of your program that you know you’re going to throw away and replace.
By doing this, you can try out the program to make sure it actually does what you want, which is kind of important. If so, the sketch version will start to point the way to what magic functions—what abstractions—you might include in a properly-structured version. And, since many of our initial decisions about this will be wrong, a preliminary sketch will help us avoid getting locked into them.
The API response
So let’s see if our sketch version of the weather client will do anything useful at all. Nope:
cargo run
...
called `Result::unwrap()` on an `Err` value: NotPresent
...
We’re calling env::var
to get the API key, and that’s
returning Err
. Well, of course: we haven’t set that
environment variable yet. Let’s do that (replace my dummy value with
your own real key):
export WEATHERSTACK_API_KEY=f4ac3e4c75d34cc2be60b0628e7b2ecc
And try again:
{
"request": {
"type": "City",
"query": "London, United Kingdom",
...
This looks promising. If you get something that looks like an error response instead, check that you used the right API key and that it’s activated (sometimes it takes a little while for a new key to start working).
Let’s copy this JSON output and save it in a file, because it’ll come in useful for testing later on. For now, though, we can just have a look at it and see if it contains the two bits of information we actually want: the temperature and the description.
Yes, it does:
"temperature": 11.2,
...
"weather_descriptions": ["Sunny"],
Looks like it’s currently 11.2ºC and sunny in London. For my American friends, that’s about fifty degrees Fahrenheit (and don’t worry, no disrespect is intended by giving the temperature only in Celsius: more on that later).
From sketch to crate
We have a working, minimal weather client. Yes, the presentation needs a bit of work, and it might be useful to get the weather for locations other than London, but that’s all detail. The point is that we have something that runs and prints the weather, sort of.
Now we can start to turn it into a proper, grown-up crate, with
tests, a nice user interface, and so on. Let’s start, as usual, by using
the “magic function” approach to design the API of the crate. What’s the
least we could write in main
? This time, you can try coming
up with the initial sketch.
GOAL: Write the main
function for the
real weather tool, using magic functions where necessary. Users should
be able to supply their chosen location as a command-line argument.
HINT: Well, you already know how to get the API key
from an environment variable, and how to get the program’s command-line
arguments. What we won’t want to do in main
is
actually make the HTTP request: that’s too fiddly and low-level. That’s
a good candidate for pushing down into a magic function. But what would
it need to take? What would it return? See what you can do.
SOLUTION: Here’s my version:
use anyhow::Result;
use std::env;
use weather::get_weather;
fn main() -> Result<()> {
let args: Vec<_> = env::args().skip(1).collect();
let location = args.join(" ");
let api_key = env::var("WEATHERSTACK_API_KEY")?;
let weather = get_weather(&location, &api_key)?;
println!("{weather}");
Ok(())
}
Just like that: a
magic get_weather
function
This isn’t the final version, of course, but it’ll do to get us started. We now know what the magic function needs to be:
let weather = get_weather(&location, &api_key)?;
What we have is the location and key; what we want is the weather, so we invent exactly the magic function that turns one into the other. Abracadabra!
A good abstraction hides the implementation details that the calling
function doesn’t need to know about. In this example, main
doesn’t have to know anything about HTTP, or even about
Weatherstack.
Indeed, it doesn’t even know what type of value
get_weather
returns. We can infer it’s a
Result
of something, but we don’t know what. And we don’t
need to know! We just print it, straightforwardly:
println!("{weather}");
We’re saying that whatever type this is, it has to implement
Display
, so that it knows how to turn itself into a string
representing the weather conditions.
Testing get_weather
Fine. Let’s see if we can build this magic get_weather
function, then. As usual, we’ll start with a test in
lib.rs
:
#[cfg(test)]
mod tests {
use std::env;
use super::*;
#[test]
fn get_weather_fn_returns_correct_weather_for_location() {
let api_key = env::var("WEATHERSTACK_API_KEY").unwrap();
let weather = get_weather("London,UK", &api_key).unwrap();
assert_eq!(
,
weather{
Weather : 11.2,
temperature: "Sunny".into(),
summary},
"wrong weather"
;
)}
}
This looks reasonable, and indeed it is. But it doesn’t
compile yet, because it refers to some magic struct Weather
that hasn’t yet been defined. That’s easily fixed, though: we’ll define
it.
#[derive(Debug, PartialEq)]
pub struct Weather {
: f64,
temperature: String,
summary}
We derived Debug
and PartialEq
so that we
can compare values using assert_eq!
, as we’d like to in the
test.
We’ll store the temperature as an f64
(Rust’s 64-bit
floating-point type, which can store fractional numbers). There’s more
we could do here, as we’ll see later, but an f64
will do
for now.
A failing implementation
And we’ll need a null implementation of get_weather
to
test against:
pub fn get_weather(location: &str, api_key: &str) -> Result<Weather> {
Ok(Weather{
: 11.2,
temperature: "Rainy".into(),
summary})
}
This is fine, but Rust complains that we haven’t yet implemented
Display
for Weather
, as we promised, so let’s
fake it for now:
impl Display for Weather {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{self:?}")
}
}
A bit cheeky, but we know this isn’t the final version, just
scaffolding. In effect, we’re implementing Display
by using
the Debug
implementation we already derived.
Now we can run the test, and, as expected, it fails:
assertion `left == right` failed: wrong weather
left: Weather { temperature: 11.2, summary: "Rainy" }
right: Weather { temperature: 11.2, summary: "Sunny" }
Great. That validates the test, so in the next post we’ll go ahead
and write the real version of get_weather
.
Or will we? Can you spot at least one potential problem with this test? Here it is again:
#[cfg(test)]
mod tests {
use std::env;
use super::*;
#[test]
fn get_weather_fn_returns_correct_weather_for_location() {
let api_key = env::var("WEATHERSTACK_API_KEY").unwrap();
let weather = get_weather("London,UK", &api_key).unwrap();
assert_eq!(
,
weather{
Weather : 11.2,
temperature: "Sunny".into(),
summary},
"wrong weather"
;
)}
}
Tune in next time to see if you were right!