1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
//! Semi-internal functionality related to networking.
use std::fmt;

use futures::{Future, Poll, Stream};
use url::Url;

use crate::{EndpointType, Error, TokenStorage};

/// Struct mirroring `hyper`'s `FutureResponse`, but with parsing that happens after the request is finished.
#[must_use = "futures do nothing unless polled"]
pub struct FutureResponse<R: EndpointType>(Box<Future<Item = R, Error = Error>>);

impl<R: EndpointType> fmt::Debug for FutureResponse<R> {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        f.debug_struct("ScreepsFutureResponse")
            .field("inner", &"<boxed future>")
            .finish()
    }
}

impl<R> Future for FutureResponse<R>
where
    R: EndpointType,
{
    type Item = R;
    type Error = Error;

    fn poll(&mut self) -> Poll<R, Error> {
        self.0.poll()
    }
}

/// Interpret a hyper result as the result from a specific endpoint.
///
/// The returned future will:
///
/// - Wait for the hyper request to finish
/// - Wait for hyper request body, collecting it into a single chunk
/// - Parse JSON body as the given `EndpointType`, and return result/error.
///
/// All errors returned will have the given `Url` contained as part of the context.
///
/// # Parameters
///
/// - `url`: url that is being queried, used only for error and warning messages
/// - `tokens`: where to put any tokens that were returned, if any
/// - `response`: actual hyper response that we're interpreting
pub fn interpret<R>(
    tokens: TokenStorage,
    url: Url,
    response: hyper::client::ResponseFuture,
) -> FutureResponse<R>
where
    R: EndpointType,
{
    FutureResponse(Box::new(
        response
            .then(move |result| match result {
                Ok(v) => Ok((tokens, url, v)),
                Err(e) => Err(Error::with_url(e, Some(url))),
            })
            .and_then(|(tokens, url, response)| {
                if let Some(token) = response.headers().get("X-Token") {
                    debug!(
                        "replacing stored auth_token with token returned from API: {:?}",
                        token.to_str()
                    );
                    tokens.set(token.as_bytes().into());
                }
                Ok((url, response))
            })
            .and_then(|(url, response)| {
                let status = response.status();

                response
                    .into_body()
                    .concat2()
                    .then(move |result| match result {
                        Ok(v) => Ok((status, url, v)),
                        Err(e) => Err(Error::with_url(e, Some(url))),
                    })
            })
            .and_then(
                |(status, url, data): (hyper::StatusCode, _, hyper::Chunk)| {
                    let json_result = serde_json::from_slice(&data);

                    // insert this check here so we can include response body in status errors.
                    if !status.is_success() {
                        if let Ok(json) = json_result {
                            return Err(Error::with_json(status, Some(url), Some(json)));
                        } else {
                            return Err(Error::with_body(status, Some(url), Some(data)));
                        }
                    }

                    let json = match json_result {
                        Ok(v) => v,
                        Err(e) => return Err(Error::with_body(e, Some(url), Some(data))),
                    };
                    let parsed = match deserialize_with_warnings::<R>(&json, &url) {
                        Ok(v) => v,
                        Err(e) => return Err(Error::with_json(e, Some(url), Some(json))),
                    };

                    R::from_raw(parsed).map_err(|e| Error::with_json(e, Some(url), Some(json)))
                },
            ),
    ))
}

fn deserialize_with_warnings<T: EndpointType>(
    input: &serde_json::Value,
    url: &Url,
) -> Result<T::RequestResult, Error> {
    let mut unused = Vec::new();

    let res = match serde_ignored::deserialize::<_, _, T::RequestResult>(input, |path| {
        unused.push(path.to_string())
    }) {
        Ok(v) => Ok(v),
        Err(e1) => {
            unused.clear();
            match serde_ignored::deserialize::<_, _, T::ErrorResult>(input, |path| {
                unused.push(path.to_string())
            }) {
                Ok(v) => Err(Error::with_json(v, Some(url.clone()), Some(input.clone()))),
                // Favor the primary parsing error if one occurs parsing the error type as well.
                Err(_) => Err(Error::with_json(e1, Some(url.clone()), Some(input.clone()))),
            }
        }
    };

    if !unused.is_empty() {
        warn!(
            "screeps API lib didn't parse some data retrieved from: {}\n\
             full data: {}\n\
             unparsed fields: {:#?}",
            url,
            serde_json::to_string_pretty(input).unwrap(),
            unused
        );
    }

    res
}