aboutsummaryrefslogtreecommitdiff
path: root/src/client.rs
blob: ebe300a36286ea71ac7acf1dde05747bdb07ded7 (plain) (blame)
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
use crate::call::{self, Response};
use crate::{Error, Result};

use std::sync::Arc;

use reqwest::{blocking, Url};

/// The INWX environment to use. The Sandbox is good for testing
/// or debugging purposes.
#[derive(Clone, Copy, Debug)]
pub enum Endpoint {
    Production,
    Sandbox,
}

impl From<Endpoint> for String {
    fn from(endpoint: Endpoint) -> String {
        match endpoint {
            Endpoint::Production => String::from("https://api.domrobot.com/xmlrpc/"),
            Endpoint::Sandbox => String::from("https://api.ote.domrobot.com/xmlrpc/"),
        }
    }
}

impl From<Endpoint> for Url {
    fn from(endpoint: Endpoint) -> Self {
        String::from(endpoint).parse().unwrap()
    }
}

/// A synchronous client to make API calls with.
/// You do **not** need to wrap it in an `Arc` or `Rc`
/// because it already uses an `Arc` internally.
/// [`Rc`]: std::rc::Rc
pub struct Client {
    inner: Arc<ClientRef>,
}

impl Client {
    /// Initialises a session and returns a `Client` if successful.
    pub fn login(ep: Endpoint, user: String, pass: String) -> Result<Client> {
        let client = Client {
            inner: Arc::new(ClientRef {
                http: blocking::Client::builder().cookie_store(true).build()?,
                endpoint: ep,
            }),
        };

        client.call(call::account::Login {
            user,
            pass,
            case_insensitive: false,
        })?;

        Ok(client)
    }

    /// Issues a `Call` and returns a `Response`
    /// if successful and if the status code
    /// matches one of the expected status codes.
    pub fn call<T, U>(&self, call: T) -> Result<U>
    where
        T: call::Call + Response<U>,
        U: serde::de::DeserializeOwned,
    {
        let expected = call.expected();
        let xml = serde_xmlrpc::request_to_str(&call.method_name(), vec![call])?;

        let raw_response = self
            .inner
            .http
            .post::<Url>(self.inner.endpoint.into())
            .body(xml)
            .send()?
            .text()?;

        let map = serde_xmlrpc::value_from_str(&raw_response)?;

        let resp = map
            .as_struct()
            .ok_or_else(|| Error::MalformedResponse(map.clone()))?;

        let code = resp
            .get("code")
            .ok_or_else(|| Error::MalformedResponse(map.clone()))?
            .as_i32()
            .ok_or_else(|| Error::MalformedResponse(map.clone()))?;

        if !expected.contains(&code) {
            return Err(Error::BadStatus(expected, code));
        }

        let data = resp
            .get("resData")
            .ok_or_else(|| Error::MalformedResponse(map.clone()))?;

        let res_data = serde_xmlrpc::value_to_string(data.clone())?;

        Ok(serde_xmlrpc::response_from_str(&res_data)?)
    }
}

impl Drop for Client {
    fn drop(&mut self) {
        // Ignore the result. Failed logout doesn't really matter.
        self.call(call::account::Logout).ok();
    }
}

// The underlying data of a `Client`.
struct ClientRef {
    http: blocking::Client,
    endpoint: Endpoint,
}