Skip to content

Commit ead89db

Browse files
committed
abstract the request making logic to start buildling client methods
1 parent 64f9409 commit ead89db

1 file changed

Lines changed: 120 additions & 45 deletions

File tree

src/main.rs

Lines changed: 120 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,39 @@
11
extern crate clap;
2-
extern crate hyper;
3-
extern crate url;
42
extern crate futures;
5-
extern crate tokio_core;
3+
extern crate hyper;
64
extern crate serde;
75
extern crate serde_json;
8-
96
#[macro_use]
107
extern crate serde_derive;
8+
extern crate tokio_core;
9+
extern crate url;
1110

1211
use clap::{Arg, ArgMatches, App};
12+
use futures::{Future, Stream};
1313
use hyper::{Client, Method, Request, Uri};
14-
use hyper::header::Authorization;
1514
use hyper::header;
15+
use hyper::header::Authorization;
16+
use std::option::Option::*;
1617
use std::str::FromStr;
17-
use url::{Url};
1818
use tokio_core::reactor::Core;
19-
use futures::{Future, Stream};
19+
use url::{Url};
2020

2121
fn main() {
2222
match ClientConfig::from_cli_args() {
2323
Ok(config) => {
2424
println!("arguments are good: {:?}", config);
25-
make_test_request(&config);
25+
let mut client = ApiClient::new(&config).unwrap();
26+
let request = client.get_project_list();
27+
match client.run(request) {
28+
Ok(projects) => {
29+
for project in projects {
30+
println!("{:?}", project);
31+
}
32+
},
33+
Err(e) => {
34+
println!("Error in request: {:?}", e);
35+
}
36+
}
2637
},
2738
Err(ConfigError::MissingAuth) => println!("Authorization info missing or incomplete. Either an API Key or a Username + Password must be provided"),
2839
Err(ConfigError::MissingUrl) => println!("Missing the Base URL"),
@@ -35,50 +46,114 @@ struct ApiProject {
3546
id: u32,
3647
name: String,
3748
#[serde(rename = "parentId")]
38-
parent_id: Option<u32>
49+
parent_id: Option<u32>,
3950
}
4051

41-
fn make_test_request(config: &ClientConfig) {
42-
let uri = config.api_uri(&["x", "projects"]);
52+
type BoxedClientFuture<T> = Box<ClientFuture<T>>;
53+
type ClientFuture<T> = Future<Item = T, Error = ClientError>;
54+
type ClientResult<T> = Result<T, ClientError>;
4355

44-
let mut req = Request::new(Method::Get, uri);
45-
config.auth_info.apply_to(&mut req);
46-
println!("Request:\n{:?}", req);
56+
struct ApiClient<'a> {
57+
config: &'a ClientConfig,
58+
core: Core,
59+
client: Client<hyper::client::HttpConnector, hyper::Body>
60+
}
4761

48-
let mut core = Core::new()
49-
.expect("Failed to create an event loop for making HTTP requests");
50-
let client: Client<hyper::client::HttpConnector, hyper::Body> = Client::new(&core.handle());
62+
impl <'a> ApiClient<'a> {
63+
fn new(config: &'a ClientConfig) -> std::io::Result<ApiClient> {
64+
Core::new().map(|core| {
65+
let client = Client::new(&core.handle());
66+
ApiClient { config: &config, core, client }
67+
})
68+
}
5169

52-
let work = client.request(req).and_then(|res| {
53-
println!("Response Status: {}", res.status());
54-
res.body()
55-
.fold(Vec::new(), |mut acc, chunk| {
56-
acc.extend_from_slice(&*chunk);
57-
futures::future::ok::<_, hyper::Error>(acc)
58-
})
59-
.map(|body| {
60-
let des: Vec<ApiProject> = serde_json::from_slice(&body).unwrap();
61-
for project in des {
62-
println!("{:?}", project);
63-
}
64-
})
70+
pub fn run<T>(&mut self, f: Box<ClientFuture<T>>) -> ClientResult<T> {
71+
self.core.run(f)
72+
}
73+
74+
fn get_project_list(&self) -> BoxedClientFuture<Vec<ApiProject>> {
75+
self.request_json(Method::Get, &["x", "projects"])
76+
}
77+
78+
/// Underlying method for creating a request to be sent to Code Dx.
79+
///
80+
/// Accepts a `method` (GET, POST, etc)
81+
/// and `path_segments` which form the API path, e.g. `&["x", "projects"]`.
82+
///
83+
/// A type parameter `T` is given to indicate the concrete type the response
84+
/// should be deserialized to once the body is parsed as JSON.
85+
///
86+
/// Returns a Boxed future that resolves with either the deserialized `T`
87+
/// or a `ClientError`.
88+
fn request_json<T>(&self, method: Method, path_segments: &[&str]) -> BoxedClientFuture<T>
89+
where T: serde::de::DeserializeOwned + std::fmt::Debug + 'static
90+
{
91+
self.request_json_with_body::<T, String>(method, path_segments, None)
92+
}
93+
94+
/// Underlying method for creating a request to be sent to Code Dx.
95+
///
96+
/// Accepts a `method` (GET, POST, etc),
97+
/// `path_segments` which form the API path, e.g. `&["x", "projects"]`
98+
/// and an optional `body` which will be attached to the request.
99+
///
100+
/// A type parameter `T` is given to indicate the concrete type the response
101+
/// should be deserialized to once the body is parsed as JSON.
102+
///
103+
/// Returns a Boxed future that resolves with either the deserialized `T`
104+
/// or a `ClientError`.
105+
fn request_json_with_body<T, B: Into<hyper::Body>>(&self, method: Method, path_segments: &[&str], body: Option<B>) -> BoxedClientFuture<T>
106+
where T: serde::de::DeserializeOwned + std::fmt::Debug + 'static
107+
{
108+
let mut req = Request::new(method, self.config.api_uri(path_segments));
109+
self.config.auth_info.apply_to(&mut req);
110+
for b in body {
111+
req.set_body(b);
112+
}
113+
114+
println!("Request:\n{:?}", req);
115+
116+
Box::new(
117+
self.client.request(req)
118+
.map_err(|err| ClientError::Protocol(err))
119+
.and_then(|res| {
120+
match res.status() {
121+
code if code.is_success() => Ok(res),
122+
code => Err(ClientError::NonSuccess(code))
123+
}
124+
})
125+
.and_then(|res| {
126+
res.body()
127+
.map_err(|err| ClientError::Protocol(err))
128+
.fold(Vec::new(), |mut acc, chunk| {
129+
acc.extend_from_slice(&*chunk);
130+
futures::future::ok::<_, ClientError>(acc)
131+
})
132+
.and_then(|complete_body| {
133+
serde_json::from_slice::<T>(&complete_body)
134+
.map_err(|err| ClientError::Json(err))
135+
})
136+
})
137+
)
138+
}
139+
}
140+
141+
142+
143+
/// Things that might go wrong when making a request with the client.
144+
#[derive(Debug)]
145+
enum ClientError {
146+
/// Wrapper for errors in the underlying request, like invalid URI,
147+
/// request format, or IO issues while executing the request.
148+
Protocol(hyper::Error),
65149

150+
/// Indicates that the request reached the server but the server
151+
/// responded with a non-success (non 2xx) response code.
152+
NonSuccess(hyper::StatusCode),
66153

67-
// let foo = res.body().concat().and_then(|body| {
68-
//
69-
// });
70-
// let x = serde_json::from_reader(res.body());
71-
// println!("JSON: {:?}", x);
72-
// res.body().for_each(|chunk| {
73-
// io::stdout()
74-
// .write_all(&chunk)
75-
// .map(|_| ())
76-
// .map_err(From::from)
77-
// })
78-
});
79-
80-
core.run(work)
81-
.expect("Failed to make request");
154+
/// Indicates that the response body was received but could not be
155+
/// parsed as JSON in the expected format.
156+
Json(serde_json::Error),
82157
}
83158

84159
/// Connection information for Code Dx.

0 commit comments

Comments
 (0)