Skip to content

Commit

Permalink
Merge pull request #95 from gabisurita/typed-fetch
Browse files Browse the repository at this point in the history
Use typed Request/Response objects in Fetch Service
  • Loading branch information
therustmonk committed Jan 11, 2018
2 parents 83f16d9 + 4e4168b commit 5071dd0
Show file tree
Hide file tree
Showing 5 changed files with 152 additions and 53 deletions.
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ categories = ["gui", "web-programming"]
description = "A framework for making client-side single-page apps"

[dependencies]
http = "0.1"
serde = "1"
serde_json = "1"
stdweb = "0.3"
Expand Down
14 changes: 12 additions & 2 deletions examples/dashboard/client/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ extern crate yew;
use yew::html::*;
use yew::format::{Nothing, Json};
use yew::services::Task;
use yew::services::fetch::{FetchService, Method};
use yew::services::fetch::{FetchService, Request};
use yew::services::websocket::{WebSocketService, WebSocketHandle, WebSocketStatus};

struct Context {
Expand Down Expand Up @@ -71,7 +71,17 @@ fn update(context: &mut Context, model: &mut Model, msg: Msg) {
match msg {
Msg::FetchData => {
model.fetching = true;
context.web.fetch(Method::Get, "./data.json", Nothing, |Json(data)| Msg::FetchReady(data));
context.web.fetch(
Request::get("/data.json").body(Nothing).unwrap(),
|response| {
let (meta, Json(data)) = response.into_parts();
if meta.status.is_success() {
Msg::FetchReady(data)
} else {
Msg::Ignore // FIXME: Handle this error accordingly.
}
}
);
}
Msg::WsAction(action) => {
match action {
Expand Down
13 changes: 9 additions & 4 deletions examples/npm_and_rest/src/gravatar.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use yew::format::{Nothing, Json};
use yew::services::fetch::{FetchService, FetchHandle, Method};
use yew::services::fetch::{FetchService, FetchHandle, Request};
use yew::html::AppSender;

#[derive(Deserialize, Debug)]
Expand Down Expand Up @@ -32,8 +32,13 @@ impl<MSG: 'static> GravatarService<MSG> {
where
F: Fn(Result<Profile, ()>) -> MSG + 'static
{
let url = format!("https://www.gravatar.com/{}.json", hash);
let handler = move |Json(data)| { listener(data) };
self.web.fetch(Method::Get, &url, Nothing, handler)
let url = format!("https://gravatar.com/{}", hash);
self.web.fetch(
Request::get(url.as_str()).body(Nothing)
.unwrap(),
move |response| {
let (_, Json(data)) = response.into_parts();
listener(data)
})
}
}
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
)]
#![recursion_limit="256"]

extern crate http;
extern crate serde;
extern crate serde_json;
#[macro_use]
Expand Down
176 changes: 129 additions & 47 deletions src/services/fetch.rs
Original file line number Diff line number Diff line change
@@ -1,87 +1,169 @@
//! Service to send HTTP-request to a server.

use std::collections::HashMap;

use stdweb::Value;
use stdweb::unstable::TryFrom;

use html::AppSender;
use format::{Storable, Restorable};
use super::Task;

/// A handle to control sent request. Could be canceled by `Task::cancel` call.
pub struct FetchHandle(Option<Value>);
pub use http::{
HeaderMap,
Method,
Request,
Response,
StatusCode,
Uri
};

/// A method of HTTP-request of [HTTP protocol](https://tools.ietf.org/html/rfc7231).
pub enum Method {
/// `GET` method of a request.
Get,
/// `POST` method of a request.
Post,
}

impl Method {
/// Converts a method to `fetch` input argument.
fn to_argument(&self) -> &'static str {
match self {
&Method::Get => "GET",
&Method::Post => "POST",
}
}
}
/// A handle to control sent requests. Can be canceled with a `Task::cancel` call.
pub struct FetchHandle(Option<Value>);


/// A service to fetch resources.
pub struct FetchService<MSG> {
sender: AppSender<MSG>,
}

impl<MSG: 'static> FetchService<MSG> {
/// Creates a new service instance connected to `App` by provided `sender`.

/// Creates a new service instance connected to an `App` by the provided `sender`.
pub fn new(sender: AppSender<MSG>) -> Self {
Self { sender }
}

/// Sends request to a server. Could contains input data and
/// needs a fuction to convert returned data to a loop's message.
pub fn fetch<F, IN, OUT>(&mut self, method: Method, url: &str, data: IN, converter: F) -> FetchHandle
/// Sends a request to a remote server given a Request object and a callback
/// fuction to convert a Response object into a loop's message.
///
/// You may use a Request builder to build your request declaratively as on the
/// following examples:
///
/// ```rust
/// let post_request = Request::post("https://my.api/v1/resource")
/// .header("Content-Type", "application/json")
/// .body(Json(&json!({"foo": "bar"})))
/// .expect("Failed to build request.");
///
/// let get_request = Request::get("https://my.api/v1/resource")
/// .body(Nothing)
/// .expect("Failed to build request.");
/// ```
///
/// The callback function can build a loop message by passing or analizing the
/// response body and metadata.
///
/// ```rust
/// context.web.fetch(
/// post_request,
/// |response| {
/// if response.status().is_success() {
/// Msg::Noop
/// } else {
/// Msg::Error
/// }
/// }
/// ```
///
/// One can also simply consume and pass the response or body object into
/// the message.
///
/// ```rust
/// context.web.fetch(
/// get_request,
/// |response| {
/// let (meta, Json(body)) = response.into_parts();
/// if meta.status.is_success() {
/// Msg::FetchResourceComplete(body)
/// } else {
/// Msg::FetchResourceFailed
/// }
/// }
/// ```
///
pub fn fetch<'a, F, IN, OUT>(&mut self, request: Request<IN>, converter: F) -> FetchHandle
where
IN: Into<Storable>,
OUT: From<Restorable>,
F: Fn(OUT) -> MSG + 'static
F: Fn(Response<OUT>) -> MSG + 'static
{
// Consume request as parts and body.
let (parts, body) = request.into_parts();

// Map headers into a Js serializable HashMap.
let header_map: HashMap<&str, &str> = parts.headers.iter().map(
|(k, v)| (k.as_str(), v.to_str().expect(
format!("Unparsable request header {}: {:?}", k.as_str(), v).as_str()
))
).collect();

// Formats URI.
let uri = format!("{}", parts.uri);

// Prepare the response callback.
// Notice that the callback signature must match the call from the javascript
// side. There is no static check at this point.
let mut tx = self.sender.clone();
let callback = move |success: bool, s: String| {
let data = if success { Ok(s) } else { Err(s) };
let callback = move |success: bool, response: Value, body: String| {
let mut response_builder = Response::builder();

// Deserialize response status.
let status = u16::try_from(js!{
return @{&response}.status;
});

if let Ok(code) = status {
response_builder.status(code);
}

// Deserialize response headers.
let headers: HashMap<String, String> = HashMap::try_from(js!{
var map = {};
@{&response}.headers.forEach(function(value, key) {
map[key] = value;
});
return map;
}).unwrap_or(HashMap::new());

for (key, values) in &headers {
response_builder.header(key.as_str(), values.as_str());
}

// Deserialize and wrap response body into a Restorable object.
let data = if success { Ok(body) } else { Err(body) };
let out = OUT::from(data);
let msg = converter(out);
let response = response_builder.body(out).unwrap();

let msg = converter(response);
tx.send(msg);
};
let method = method.to_argument();
let body = data.into();

let handle = js! {
var data = {
method: @{method},
body: @{body},
method: @{parts.method.as_str()},
body: @{body.into()},
headers: @{header_map},
};
var request = new Request(@{url}, data);
var request = new Request(@{uri}, data);
var callback = @{callback};
var handle = {
interrupt: false,
callback,
};
fetch(request).then(function(response) {
if (response.ok) {
// Do we need to use blob here?
return response.text();
} else {
throw new Error("Network response was not ok.");
}
}).then(function(data) {
if (handle.interrupted != true) {
callback(true, data);
callback.drop();
}
}).catch(function(err) {
if (handle.interrupted != true) {
callback(false, data);
callback.drop();
}
response.text().then(function(data) {
if (handle.interrupted != true) {
callback(true, response, data);
callback.drop();
}
}).catch(function(err) {
if (handle.interrupted != true) {
callback(false, response, data);
callback.drop();
}
});
});
return handle;
};
Expand Down

0 comments on commit 5071dd0

Please sign in to comment.