Last active
April 27, 2017 19:13
-
-
Save chaoticsmol/6b62ff1212944bbdc8656f550c263e8a to your computer and use it in GitHub Desktop.
Rust JSON API Client With Error Handling
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
[package] | |
name = "jsontest" | |
version = "0.1.0" | |
authors = ["Zack Mullaly <[email protected]>"] | |
[dependencies] | |
serde = "^1.0" | |
serde_json = "^1.0" | |
serde_derive = "^1.0" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package main | |
import ( | |
"encoding/json" | |
"fmt" | |
"os" | |
) | |
// HasError helps us get at the error expected to be in a response. | |
type HasError interface { | |
Err() *APIError | |
} | |
// APIError contains information we expect to get in an error if one is present. | |
type APIError struct { | |
Code uint32 `json:"code"` | |
Kind string `json:"kind"` | |
Message string `json:"message"` | |
} | |
// TimePeriod is the data we hope to get back from an API endpoint. | |
// The `Finished` attribute is a pointer because it may be null. | |
type TimePeriod struct { | |
Started string | |
Finished *string | |
} | |
// APIExample is what we would deserialize a response to. | |
type APIExample struct { | |
Error *APIError `json:"error"` | |
Started *string `json:"started"` | |
Finished *string `json:"finished"` | |
} | |
// Err is implemented for APIExample so we can get at an error if one occurs. | |
func (a APIExample) Err() *APIError { | |
return a.Error | |
} | |
// IsNull is used to determine if an error is essentially "nil". | |
func (e APIError) IsNull() bool { | |
return e == APIError{} | |
} | |
func decodeError() APIError { | |
return APIError{ | |
0, | |
"decode_err", | |
"Failed to decode input.", | |
} | |
} | |
// This is the kind of code that every API endpoint client function would have to contain. | |
// Beware of nil dereferences. | |
func exampleEndpt() (TimePeriod, APIError) { | |
input := os.Stdin | |
decoded := APIExample{} | |
decoder := json.NewDecoder(input) | |
err := decoder.Decode(&decoded) | |
if err != nil { | |
return TimePeriod{}, decodeError() | |
} | |
if decoded.Error != nil && !decoded.Error.IsNull() { | |
return TimePeriod{}, *decoded.Error | |
} | |
out := TimePeriod{} | |
if decoded.Started != nil { | |
out.Started = *decoded.Started | |
} else { | |
return TimePeriod{}, decodeError() | |
} | |
out.Finished = decoded.Finished | |
return out, APIError{} | |
} | |
// Demonstrates how a user of our Golang API library would call an endpoint function | |
func main() { | |
period, err := exampleEndpt() | |
if !err.IsNull() { | |
fmt.Printf("ERROR - %v\n", err) | |
} else { | |
fmt.Printf( | |
"SUCCESS - Period started at %s and finished at %v\n", | |
period.Started, | |
period.Finished) | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
extern crate serde; | |
extern crate serde_json; | |
#[macro_use] extern crate serde_derive; | |
use std::io::{Read, stdin}; | |
// This trait helps us get at the error expected to be in a response. | |
trait HasError { | |
fn error(&self) -> &Option<ApiError>; | |
} | |
/// Contains information we would expect to be in an en error if one's present. | |
#[derive(Debug, Clone, Deserialize)] | |
struct ApiError { | |
pub code: u32, | |
pub kind: String, | |
pub message: String, | |
} | |
/// The data we hope to get back from an API endpoint. | |
/// The `finished` attribute is `Option`al because it may be null. | |
struct TimePeriod { | |
pub started: String, | |
pub finished: Option<String>, | |
} | |
/// This is what our client function would return. | |
/// Each field is optional because they may either not be present or be null. | |
#[derive(Deserialize)] | |
struct ApiExample { | |
pub error: Option<ApiError>, | |
pub started: Option<String>, | |
pub finished: Option<String>, | |
} | |
impl HasError for ApiExample { | |
fn error(&self) -> &Option<ApiError> { | |
&self.error | |
} | |
} | |
fn decode_error() -> ApiError { | |
ApiError { | |
code: 0, | |
kind: "decode_err".to_owned(), | |
message: "Failed to decode input.".to_owned(), | |
} | |
} | |
/// A helper function that converts responses into more Rustic `Result`s. | |
fn decode_input<R, O>(input: R) -> Result<O, ApiError> | |
where R: Read, | |
O: HasError + serde::de::DeserializeOwned | |
{ | |
let decoded: O = serde_json::from_reader(input).map_err(|_| decode_error())?; | |
if let &Some(ref error) = decoded.error() { | |
return Err(error.clone()) | |
} | |
Ok(decoded) | |
} | |
// This is the kind of code we'd write in a client function. | |
fn example_endpt() -> Result<TimePeriod, ApiError> { | |
let input = stdin(); | |
let decoded: Result<ApiExample, ApiError> = decode_input(input); | |
decoded.and_then(|period| match period { | |
ApiExample {started: Some(time), finished, .. } => Ok(TimePeriod { | |
started: time, | |
finished: finished, | |
}), | |
_ => Err(decode_error()), | |
}) | |
} | |
fn main() { | |
let period = example_endpt(); | |
match period { | |
Ok(period) => println!("SUCCESS - Period started at {} and finished at {:?}", period.started, period.finished), | |
Err(err) => println!("ERROR - {:?}", err), | |
}; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment