aboutsummaryrefslogtreecommitdiff
path: root/src/client.rs
blob: 63acf39e5172f6ffb2ab51754c244a97a2f5f03b (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
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
146
use crate::call;
use crate::response::{self, ResponseData};
use crate::{Error, Result};

use std::net::SocketAddr;
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 Endpoint {
    pub fn domain(&self) -> &'static str {
        match self {
            Self::Production => "api.domrobot.com",
            Self::Sandbox => "api.ote.domrobot.com",
        }
    }
}

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

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)
    }

    /// Initialises a session using the provided `SocketAddr`
    /// and returns a `Client` if successful.
    /// This is useful if your system can't resolve DNS hostnames.
    pub fn login_addr(
        ep: Endpoint,
        addr: SocketAddr,
        user: String,
        pass: String,
    ) -> Result<Client> {
        let client = Client {
            inner: Arc::new(ClientRef {
                http: blocking::Client::builder()
                    .cookie_store(true)
                    .resolve(ep.domain(), addr)
                    .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 + call::Response<U>,
        U: response::Response + Clone + 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 resp: ResponseData<U> = serde_xmlrpc::response_from_str(&raw_response)?;

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

        Ok(U::unwrap(resp.params))
    }
}

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,
}