Introduction

To see why I am even writing this, please see my previous blog post

Today I have written my first working snippet of code which is by no means :-

  1. Finished
  2. Of any level of quality
  3. Covered by tests
  4. Any good !

It is the start of my Energenie API crate which is used to control and monitor the Energenie MiHome products

This is what my files look like so far – as you can see, its not very big.

I am writing this primarily as a library for use by my main project (and of course, if anyone else uses energenie products – they are welcome to try it) – but it will also have a command line tool which at the moment, as I am not doing TDD yet, is the only way of running my library.

Where I Started From

I chose to start documenting this after I started, so I have taken a few small steps in advance. I will try to remember where I came from here

Starting A New Crate

cargo new –lib energenie_api

Note the ‘–lib’ – which will create a lib.rs and a main.js which is typical for a library.

I wanted to define how my library was going to be called before writing it, so I started with the main.js and it ended up like this

main.rs

1 use energenie_api;
2 fn main() {
3
4 let session = energenie_api::new_session();
5 match session {
6 Ok(session) => println!("It connected, the session looks like this {:?}", session),
7 Err(err) => println!("It didnt connect")
8 }

So, after reading the rust book I decided that the correct way of returning something from a function that may well go wrong is to return the std::Result type, containing a ‘session’ if all went well or a string if something went wrong (not sure a string is the best thing, but it will do for now)

So, the ‘match session’ on line 5 is what we use to deal with a response. A match has different ‘arms’ – 2 in this case – one for the ‘Ok’ result and another for the ‘Err’ result.

No rocket science going on here – if the session starts, it uses the println! macro to print “It connected…” and output the session variable (that awful {:?} means print a more complex object for debugging). If it fails to connect, it says “It didnt connect” – really useful stuff going on here.

lib.rs

1. #[macro_use]
2. extern crate serde_derive;
3. extern crate serde_json;
4.
5. pub mod api;
6. /// Creates a new session
7. pub fn new_session() -> Result<api::Session, &'static str> {
8. api::Session::start()
9. }
10. #[cfg(test)]
11. mod tests {
12. #[test]
13. fn it_works() {
14. assert_eq!(2 + 2, 4);
15. }
16. }

Both lib.rs and main.rs are in the ‘crate root’ – so I wanted to keep the interface in here nice and tidy – I don’t want users of this library to be put off by loads of public functions in here.

So, we have new_session and thats it up to now. You will notice that this function calls api::Session::start – this is where the actual work is done.

See the line ‘pub mod api;’ on line 5 – as it has no definition – it means ‘api is defined outside of this file’. So, where is it ? in this next file api.rs

api.rs

//! The api stuff
extern crate reqwest;
extern crate serde_derive;
extern crate serde_json;
extern crate serde;

use reqwest::Client;
use std::env;

// A Session is where everything begins. My plan (for now) is that we go via the session
// for everything, but we will see how the API develops.
#[derive(Debug)]
pub struct Session {
pub config: Config,
pub api_key: String
}
impl Session {
/// Starts a new session
pub fn start() -> Result<Session, &'static str> {
let config = Config::new();
let request_url = format!("{api_base}/users/profile", api_base = &config.api_url);
let mut resp = Client::new()
.get(&request_url)
.basic_auth(&config.api_username, Some(&config.api_password))
.header("Accept", "application/json")
.send().unwrap();
if resp.status().is_success() {
let packet: EnergenieResponse<EnergenieUserProfile> = resp.json().unwrap();
Ok(Session {
config: config,
api_key: packet.data.api_key
})
} else {
Err("Boom")
}

}
}

// The Config struct is used to gather all of the configuration into one place.
// At the moment, it is a combination of hard coding and environment variables.
// I have not really planned ahead to thing where it is going to get the config from long term
// but, lets learn to walk before we run ?
#[derive(Debug)]
pub struct Config {
api_url: String,
api_password: String,
api_username: String
}
impl Config {
pub fn new() -> Config {
Config {
api_url: String::from("https://mihome4u.co.uk/api/v1"),
api_username: env::var("MIHOME_USERNAME").expect("MIHOME_USERNAME must be set"),
api_password: env::var("MIHOME_PASSWORD").expect("MIHOME_PASSWORD must be set")
}
}
}

// Below are the data structures returned by the API - I am hoping to move these into a different file

#[derive(Deserialize, Debug)]
struct EnergenieUserProfile {
id: i64,
email_address: String,
api_key: String
}

#[derive(Deserialize, Debug)]
struct EnergenieResponse<T> {
status: String,
time: f64,
data: T
}

Sorry, as I mentioned – I decided to document this a bit late – but I will try and show you how I came to the decisions that I did.

I started off with the Session and Config structs and their basic implementations. At the beginning I wasn’t making any rest calls yet – so none of the external crates were present. I just really implemented the new function to return the correct thing so it would compile and run.

Then, I went to crates.io and looked for the most popular http client which seemed to be ‘reqwest‘. After a bit of messing around and not understanding the concept of ‘borrowing’ and ‘lifetimes’ – I managed to write some quite simple code without any ‘lifetime’ stuff (which I am pleased about because I don’t quite follow them at the moment – but I will soon – I just need a practical problem to learn from).

reqwest can also use ‘serde‘ crate to parse the json response from the API call into – very handy.

Rust’s ‘generics’ came into play here as well as I was able to define a generic ‘EnergenieResponse’ struct which can contain any other type (within reason) in it’s ‘data’. For now we are only using ‘EnergenieUserProfile’ struct as the data as that is the only API call we are making at present.

Running The App

So, now I am going to run it

Garys-MacBook-Pro:energenie-api garyhome$ cargo run
Compiling energenie-api v0.1.0 (/Users/garyhome/Sync/rust/smarthome/energenie-api)
warning: unused variable: err
--> energenie-api/src/main.rs:7:13
|
7 | Err(err) => println!("It didnt connect")
| ^^^ help: consider using _err instead
|
= note: #[warn(unused_variables)] on by default

Finished dev [unoptimized + debuginfo] target(s) in 6.82s Running `/Users/garyhome/Sync/rust/smarthome/target/debug/energenie-api`

It connected, the session looks like this Session { config: Config { api_url: "https://mihome4u.co.uk/api/v1", api_password: "*********", api_username: "**********" }, api_key: "*************************" }
Garys-MacBook-Pro:energenie-api garyhome

As you can see, a warning in there – but as this is my first time running rust stuff I don’t care – I am celebrating because I can see my session at the bottom meaning it connected.

Review

Im no rust expert, but the things that I don’t like up to now are

  1. api.rs is already getting big – imagine when the remainder of the energenie api is implemented
  2. We are using unwrap in places – what this does is basically returns the value of a ‘Response’ when it doesnt have an error OR just panic’s if it does – not nice
  3. Really poor error handling in general
  4. Use of environment variables for credentials etc..
  5. Username and password logged in the output

So, in my next session – I will start to sort this out, but this time – I will document as I go along.


0 Comments

Leave a Reply

Avatar placeholder

Your email address will not be published. Required fields are marked *