In part 1 – we identified a few problems with my initial code.

In part 2 – we addressed one of them – the removal of the unwraps.

In this episode, we will sort out those pesky errors. Returning just strings seems so wrong.

So, first I had to go back and re read the section on error handling in the rust book https://doc.rust-lang.org/1.8.0/book/error-handling.html . One option we have is to use the Option type to return the session if it managed to start or nothing if not. That is an option, but I will continue to read

The drawback of the Option type is an error condition is not communicated to the caller. I do not want to output straight to the user from my low level code, so passing the error up seems like a good idea, just in case someone is interested.

So, this looks like we want the Result type which we are already using.

The Result type has an interesting map method which would pass the result from one function onto another if the result was successful, whilst passing on the error to the caller if not.
However, call me a perfectionist, but if my code is using a library under the hood – in this case ‘reqwest’ – then why should i be passing reqwest errors back to the caller ? the caller should not even know I use ‘reqwest’ – after all, what would happen if I changed to another library later on and lots of people had written code to work with the reqwest error type. This sort of implementation would lead to a resistance to change which I don’t want.

A representation of an error seems like it is normally done using a struct. To group errors you might use an enum with several types defined for different errors, but the underlying error is still a struct.
It seems this struct should implement various traits such as Display, Clone, Eq etc… but this seems like a lot of work. So, reading on..

Ok, found some advice from the rust book – ‘advice for library writers’ – as a minimum, implement the Error trait.

But, it does seem like there is a bit of work to do custom error structs – I have tried to determine if there is a common pattern from looking at other rust libraries etc.. One pattern I am coming across is having en error struct with an inner ‘kind’ struct.

I have also seen this :-

use failure::*;       



#[derive(Fail, Debug)]

pub enum FunctionError {

#[fail(display = "Invalid command line value")]

CommandlineParse,

#[fail(display = "Invalid path value")]

ParentPathParse,

#[fail(display = "Cannot parse filename")]

FileNameParseError,

#[fail(display = "Cannot convert path to string")]

PathConverError,

#[fail(display = "Cannot fetch body from s3 response")]

S3FetchBodyError,

#[fail(display = "File is already present")]

PresentFileError,

#[fail(display = "S3 Object is not complete")]

ObjectFieldError,
}

But, it requires an external dependency called ‘failure’. Not sure I want an external dependency just to define errors.

I found this in the ‘rust-copperline‘ github repo

use std::error;       

use std::fmt;

use nix;



#[derive(Debug, PartialEq)]

pub enum Error {

ErrNo(nix::Error),

Cancel,

EndOfFile,

UnsupportedTerm,

ParseError

}



impl fmt::Display for Error {

fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {

match *self {

Error::ErrNo(ref err) => write!(f, "ERRNO: {}", err.errno().desc()),

Error::Cancel => write!(f, "Cancelled"),

Error::EndOfFile => write!(f, "End of file"),

Error::UnsupportedTerm => write!(f, "Unsupported terminal type"),

Error::ParseError => write!(f, "Encountered unknown sequence")

}

}

}



impl error::Error for Error {

fn description(&self) -> &str {

match *self {

Error::ErrNo(ref err) => err.errno().desc(),

Error::Cancel => "cancelled",

Error::EndOfFile => "end of file",

Error::UnsupportedTerm => "unsupported terminal type",

Error::ParseError => "unknown sequence"

}

}

}



impl From<nix::Error> for Error {

fn from(err: nix::Error) -> Error {

Error::ErrNo(err)

}
}

I quite like this – until I find a reason to do anything more complex. .

So, I now want to define all API errors under one Enum – so that it is simple for my users to deal with all api errors as they wish.

But, if I were to add all of these errors into my api module (currently in api.rs), the file is going to get messy. So, I remembered that a module can also be organised in a folder and use a mod.rs as the base for that module.

So, to start off with, I created a folder called ‘api’ – moved api.rs into it and renamed it ‘mod.rs’. So, my new structure looked like this

I then ran the code and it still worked 🙂

Next, I want to define and use 2 errors – one for if there is a problem logging in and another (hopefully very rare occurence) when a response comes back from the server which is invalid and cannot be parsed.

So, lets create a file called errors.rs and define the errors as follows

pub enum ApiError {
LoginFailure,
InvalidLoginResponse
}

I just wanted to keep this nice and simple – don’t add functionality until its needed.

Next, lets use these errors

Lets focus on the api/mod.rs file to start with. We need to ‘re export’ the errors module so that it is available as energenie_api::api::errors. To do this, we add

pub mod errors;

near the top of the file. Then, we tell our Session’s ‘start’ function that it is to return an api error. So, the signature changes to this

pub fn start() -> Result<Session, errors::ApiError> {

If we were to try and compile now, it would fail as we are not actually returning an error in the code – just a string. So, lets sort that. There are 2 places where we want to return an error – all in the same block. So, the block will change to this

match result {
Ok(mut resp) => {
match resp.json() {
Ok(json) => Ok(Session::parse_new_session_response(config, json)),
Err(_err) => Err(errors::ApiError::InvalidLoginResponse)
}

},
Err(err) => Err(errors::ApiError::LoginFailure)
}

As you can see, we are now using errors::ApiError::<SomeError> – perfect

If we tried to compile now, we still have a problem as our ‘new_session’ function signature in lib.rs looks like this

pub fn new_session() -> Result<api::Session, &'static str> {

i.e. it is expecting a string error – so we need to change that expectation to look like this

pub fn new_session() -> Result<api::Session, errors::ApiError> {

which will also need this adding near the top

use crate::api::errors;

So, the 2 files we have changed will now look like this

lib.rs


#[macro_use]
extern crate serde_derive;
extern crate serde_json;

use crate::api::errors;

pub mod api;
/// Creates a new session
pub fn new_session() -> Result<api::Session, errors::ApiError> {
api::Session::start()
}
#[cfg(test)]
mod tests {
#[test]
fn it_works() {
assert_eq!(2 + 2, 4);
}
}

and api/mod.rs

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


use reqwest::Client;
use std::env;
pub mod errors;

// 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, errors::ApiError> {
let config = Config::new();
let request_url = format!("{api_base}/users/profile", api_base = &config.api_url);
let result = Client::new()
.get(&request_url)
.basic_auth(&config.api_username, Some(&config.api_password))
.header("Accept", "application/json")
.send();
match result {
Ok(mut resp) => {
match resp.json() {
Ok(json) => Ok(Session::parse_new_session_response(config, json)),
Err(_err) => Err(errors::ApiError::InvalidLoginResponse)
}

},
Err(err) => Err(errors::ApiError::LoginFailure)
}
}

fn parse_new_session_response(config: Config, json: EnergenieResponse<EnergenieUserProfile>) -> Session {
Session {
config: config,
api_key: json.data.api_key
}
}
}

// 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
}

Lets now run it – all looks good 🙂

Whilst we aren’t communicating the reason why the error happened, it will do for now. We have successfully defined a mechanism for returning errors to the caller which is not just a string – so a tick in the box for that one.

Categories: rust

0 Comments

Leave a Reply

Avatar placeholder

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