mm_client/
client.rs

1extern crate reqwest;
2extern crate serde;
3
4#[cfg(test)]
5use mockito;
6
7use reqwest::blocking::{Client as NetworkClient, RequestBuilder, Response};
8use reqwest::header::CONNECTION;
9use reqwest::{Method, StatusCode};
10use serde::Serialize;
11
12use std::fmt;
13use std::io::Read;
14use std::str;
15
16use crate::error::MMCError;
17use crate::error::MMCResult;
18
19#[cfg(not(test))]
20const LIVE_URL: &'static str = "https://media.services.pbs.org/api/v1";
21#[cfg(not(test))]
22const STAGING_URL: &'static str = "https://media-staging.services.pbs.org/api/v1";
23
24#[cfg(test)]
25const LIVE_URL: &'static str = mockito::SERVER_URL;
26#[cfg(test)]
27const STAGING_URL: &'static str = mockito::SERVER_URL;
28
29/// A client for communicating with the Media Manager API
30#[derive(Debug)]
31pub struct Client {
32    key: String,
33    secret: String,
34    base: String,
35    client: NetworkClient,
36}
37
38pub type Params<'a> = Vec<(&'a str, &'a str)>;
39
40type ParentEndpoint<'a> = (Endpoints, &'a str);
41
42#[derive(Serialize)]
43struct MoveTarget {
44    #[serde(skip_serializing_if = "Option::is_none")]
45    show: Option<String>,
46    #[serde(skip_serializing_if = "Option::is_none")]
47    season: Option<String>,
48}
49
50impl MoveTarget {
51    pub fn for_endpoint(endpoint: &Endpoints, id: &str) -> MMCResult<MoveTarget> {
52        match *endpoint {
53            Endpoints::Season => Ok(MoveTarget {
54                show: None,
55                season: Some(id.to_string()),
56            }),
57            Endpoints::Show => Ok(MoveTarget {
58                show: Some(id.to_string()),
59                season: None,
60            }),
61            _ => Err(MMCError::UnsupportedMoveParent(endpoint.to_string())),
62        }
63    }
64}
65
66#[derive(Serialize)]
67struct Move {
68    #[serde(rename = "type")]
69    _type: String,
70    id: String,
71    attributes: MoveTarget,
72}
73
74#[derive(Serialize)]
75struct MoveRequest {
76    data: Move,
77}
78
79/// The Media Manager endpoints that are supported by [Client](struct.Client.html)
80#[derive(Clone, Debug)]
81pub enum Endpoints {
82    /// Represents the assets endpoint
83    Asset,
84
85    /// Represents the changelog endpoint
86    Changelog,
87
88    /// Represents the collections endpoint
89    Collection,
90
91    /// Represents the episodes endpoint
92    Episode,
93
94    /// Represents the franchises endpoint
95    Franchise,
96
97    /// Represents the seasons endpoint
98    Season,
99
100    /// Represents the shows endpoint
101    Show,
102
103    /// Represents the specials endpoint
104    Special,
105}
106
107impl Endpoints {
108    fn singular(&self) -> String {
109        match *self {
110            Endpoints::Asset => "asset",
111            Endpoints::Changelog => "changelog",
112            Endpoints::Collection => "collection",
113            Endpoints::Episode => "episode",
114            Endpoints::Franchise => "franchise",
115            Endpoints::Season => "season",
116            Endpoints::Show => "show",
117            Endpoints::Special => "special",
118        }
119        .to_string()
120    }
121}
122
123impl fmt::Display for Endpoints {
124    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
125        let string_form = match *self {
126            Endpoints::Asset => "assets",
127            Endpoints::Changelog => "changelog",
128            Endpoints::Collection => "collections",
129            Endpoints::Episode => "episodes",
130            Endpoints::Franchise => "franchises",
131            Endpoints::Season => "seasons",
132            Endpoints::Show => "shows",
133            Endpoints::Special => "specials",
134        };
135
136        write!(f, "{}", string_form)
137    }
138}
139
140impl str::FromStr for Endpoints {
141    type Err = MMCError;
142
143    fn from_str(s: &str) -> Result<Self, Self::Err> {
144        match s {
145            "asset" | "assets" => Ok(Endpoints::Asset),
146            "changelog" => Ok(Endpoints::Changelog),
147            "collection" | "collections" => Ok(Endpoints::Collection),
148            "episode" | "episodes" => Ok(Endpoints::Episode),
149            "franchise" | "franchises" => Ok(Endpoints::Franchise),
150            "season" | "seasons" => Ok(Endpoints::Season),
151            "show" | "shows" => Ok(Endpoints::Show),
152            "special" | "specials" => Ok(Endpoints::Special),
153            x => Err(MMCError::UnknownEndpoint(x.to_string())),
154        }
155    }
156}
157
158impl Client {
159    /// Generates a new client for the production Media Manager API
160    pub fn new(key: &str, secret: &str) -> MMCResult<Client> {
161        Client::client_builder(key, secret, LIVE_URL)
162    }
163
164    /// Generates a new client for the staging Media Manager API
165    pub fn staging(key: &str, secret: &str) -> MMCResult<Client> {
166        Client::client_builder(key, secret, STAGING_URL)
167    }
168
169    fn client_builder(key: &str, secret: &str, base: &str) -> MMCResult<Client> {
170        NetworkClient::builder()
171            .build()
172            .map_err(MMCError::Network)
173            .and_then(|net_client| {
174                Ok(Client {
175                    key: String::from(key),
176                    secret: String::from(secret),
177                    base: String::from(base),
178                    client: net_client,
179                })
180            })
181    }
182
183    /// Attempts to fetch a single object with the requested id from the requested
184    /// Media Manager API endpoint
185    pub fn get(&self, endpoint: Endpoints, id: &str, params: Option<Params>) -> MMCResult<String> {
186        self.rq_get(
187            Client::build_url(
188                self.base.as_str(),
189                None,
190                endpoint,
191                Some(id),
192                params.unwrap_or(vec![]),
193            )
194            .as_str(),
195        )
196    }
197
198    /// Attempts to fetch a list of objects from the requested Media Manager API endpoint augmented
199    /// by the requested parameters
200    pub fn list(&self, endpoint: Endpoints, params: Params) -> MMCResult<String> {
201        self.rq_get(Client::build_url(self.base.as_str(), None, endpoint, None, params).as_str())
202    }
203
204    /// Attempts to fetch a list of child objects of the requested Media Manager API type belonging
205    /// to the requested parent object augmeted by the requested parameters
206    pub fn child_list(
207        &self,
208        endpoint: Endpoints,
209        parent_id: &str,
210        parent_endpoint: Endpoints,
211        params: Option<Params>,
212    ) -> MMCResult<String> {
213        self.rq_get(
214            Client::build_url(
215                self.base.as_str(),
216                Some((parent_endpoint, parent_id)),
217                endpoint,
218                None,
219                params.unwrap_or(Vec::new()),
220            )
221            .as_str(),
222        )
223    }
224
225    /// Attempts to create a new object of the provided [Endpoints](enum.Endpoints.html) for the
226    /// provided parent [Endpoints](enum.Endpoints.html)
227    pub fn create<T: Serialize>(
228        &self,
229        parent: Endpoints,
230        id: &str,
231        endpoint: Endpoints,
232        body: &T,
233    ) -> MMCResult<String> {
234        self.rq_post(
235            Client::build_url(
236                self.base.as_str(),
237                Some((parent, id)),
238                endpoint,
239                None,
240                vec![],
241            )
242            .as_str(),
243            body,
244        )
245    }
246
247    /// Attempts to fetch the edit object specified by the [Endpoints](enum.Endpoints.html) and id
248    pub fn edit(&self, endpoint: Endpoints, id: &str) -> MMCResult<String> {
249        self.rq_get(
250            Client::build_edit_url(self.base.as_str(), None, endpoint, Some(id), vec![]).as_str(),
251        )
252    }
253
254    /// Attempts to update the object specified by the [Endpoints](enum.Endpoints.html) and id
255    pub fn update<T: Serialize>(
256        &self,
257        endpoint: Endpoints,
258        id: &str,
259        body: &T,
260    ) -> MMCResult<String> {
261        self.rq_patch(
262            Client::build_edit_url(self.base.as_str(), None, endpoint, Some(id), vec![]).as_str(),
263            body,
264        )
265    }
266
267    /// Attempts to delete the object specified by the [Endpoints](enum.Endpoints.html) and id
268    pub fn delete(&self, endpoint: Endpoints, id: &str) -> MMCResult<String> {
269        self.rq_delete(
270            Client::build_edit_url(self.base.as_str(), None, endpoint, Some(id), vec![]).as_str(),
271        )
272    }
273
274    /// Attempts to change the parent of an object
275    pub fn change_parent(
276        &self,
277        parent_endpoint: Endpoints,
278        parent_id: &str,
279        child_endpoint: Endpoints,
280        child_id: &str,
281    ) -> MMCResult<String> {
282        let move_request = MoveRequest {
283            data: Move {
284                _type: child_endpoint.singular(),
285                id: child_id.to_string(),
286                attributes: MoveTarget::for_endpoint(&parent_endpoint, parent_id)?,
287            },
288        };
289
290        self.rq_patch(
291            Client::build_url(
292                self.base.as_str(),
293                None,
294                child_endpoint,
295                Some(child_id),
296                vec![],
297            )
298            .as_str(),
299            &move_request,
300        )
301    }
302
303    /// Allows for calling any arbitrary url from the Media Manager API
304    pub fn url(&self, url: &str) -> MMCResult<String> {
305        self.rq_get(url)
306    }
307
308    /// Shorthand for accessing a single asset
309    pub fn asset(&self, id: &str, params: Option<Params>) -> MMCResult<String> {
310        self.get(Endpoints::Asset, id, params)
311    }
312
313    /// Shorthand for accessing a list of assets
314    pub fn assets(
315        &self,
316        parent_id: &str,
317        parent_endpoint: Endpoints,
318        params: Option<Params>,
319    ) -> MMCResult<String> {
320        self.child_list(Endpoints::Asset, parent_id, parent_endpoint, params)
321    }
322
323    /// Shorthand for accessing a list of changes
324    pub fn changelog(&self, params: Params) -> MMCResult<String> {
325        self.list(Endpoints::Changelog, params)
326    }
327
328    /// Shorthand for accessing a single collection
329    pub fn collection(&self, id: &str, params: Option<Params>) -> MMCResult<String> {
330        self.get(Endpoints::Collection, id, params)
331    }
332
333    /// Shorthand for accessing a list of collections
334    pub fn collections(&self, params: Params) -> MMCResult<String> {
335        self.list(Endpoints::Collection, params)
336    }
337
338    /// Shorthand for accessing a single episode
339    pub fn episode(&self, id: &str, params: Option<Params>) -> MMCResult<String> {
340        self.get(Endpoints::Episode, id, params)
341    }
342
343    /// Shorthand for accessing a list of episodes
344    pub fn episodes(&self, season_id: &str, params: Option<Params>) -> MMCResult<String> {
345        self.child_list(Endpoints::Episode, season_id, Endpoints::Season, params)
346    }
347
348    /// Shorthand for accessing a single franchise
349    pub fn franchise(&self, id: &str, params: Option<Params>) -> MMCResult<String> {
350        self.get(Endpoints::Franchise, id, params)
351    }
352
353    /// Shorthand for accessing a list of franchises
354    pub fn franchises(&self, params: Params) -> MMCResult<String> {
355        self.list(Endpoints::Franchise, params)
356    }
357
358    /// Shorthand for accessing a single season
359    pub fn season(&self, id: &str, params: Option<Params>) -> MMCResult<String> {
360        self.get(Endpoints::Season, id, params)
361    }
362
363    /// Shorthand for accessing a list of seasons
364    pub fn seasons(&self, show_id: &str, params: Option<Params>) -> MMCResult<String> {
365        self.child_list(Endpoints::Season, show_id, Endpoints::Show, params)
366    }
367
368    /// Shorthand for accessing a single special
369    pub fn special(&self, id: &str, params: Option<Params>) -> MMCResult<String> {
370        self.get(Endpoints::Special, id, params)
371    }
372
373    /// Shorthand for accessing a list of specials
374    pub fn specials(&self, show_id: &str, params: Option<Params>) -> MMCResult<String> {
375        self.child_list(Endpoints::Special, show_id, Endpoints::Show, params)
376    }
377
378    /// Shorthand for accessing a single show
379    pub fn show(&self, id: &str, params: Option<Params>) -> MMCResult<String> {
380        self.get(Endpoints::Show, id, params)
381    }
382
383    /// Shorthand for accessing a list of shows
384    pub fn shows(&self, params: Params) -> MMCResult<String> {
385        self.list(Endpoints::Show, params)
386    }
387
388    // Handle read endpoints of the API
389    fn rq_get(&self, url: &str) -> MMCResult<String> {
390        self.rq_send(self.client.get(url))
391    }
392
393    // Handle create endpoints of the API
394    fn rq_post<T: Serialize>(&self, url: &str, body: &T) -> MMCResult<String> {
395        self.rq_send(self.client.post(url).json(body))
396    }
397
398    // Handle update endpoints of the API
399    fn rq_patch<T: Serialize>(&self, url: &str, body: &T) -> MMCResult<String> {
400        self.rq_send(self.client.request(Method::PATCH, url).json(body))
401    }
402
403    // Handle update endpoints of the API
404    fn rq_delete(&self, url: &str) -> MMCResult<String> {
405        self.rq_send(self.client.request(Method::DELETE, url))
406    }
407
408    // Handle authentication and response mapping
409    fn rq_send(&self, req: RequestBuilder) -> MMCResult<String> {
410        req.basic_auth(self.key.to_string(), Some(self.secret.to_string()))
411            .header(CONNECTION, "close")
412            .send()
413            .map_err(MMCError::Network)
414            .and_then(Client::handle_response)
415    }
416
417    fn build_edit_url(
418        base_url: &str,
419        parent: Option<ParentEndpoint>,
420        endpoint: Endpoints,
421        id: Option<&str>,
422        params: Params,
423    ) -> String {
424        let mut url = Client::build_url(base_url, parent, endpoint, id, params);
425        url.push_str("edit/");
426
427        url
428    }
429
430    fn build_url(
431        base_url: &str,
432        parent: Option<ParentEndpoint>,
433        endpoint: Endpoints,
434        id: Option<&str>,
435        params: Params,
436    ) -> String {
437        // Create the new base for the returned url
438        let mut url = base_url.to_string();
439        url.push('/');
440
441        // Add the parent endpoint if an endpoint and id was supplied
442        if let Some(p_endpoint) = parent {
443            url.push_str(p_endpoint.0.to_string().as_str());
444            url.push('/');
445            url.push_str(p_endpoint.1);
446            url.push('/');
447        }
448
449        // Parse the requested endpoint
450        let endpoint_string = endpoint.to_string();
451        url.push_str(endpoint_string.as_str());
452        url.push('/');
453
454        // Optional add the id if it was supplied
455        if let Some(id_val) = id {
456            url.push_str(id_val);
457            url.push('/');
458        }
459
460        // Add the query parameters to the url
461        url + Client::format_params(params).as_str()
462    }
463
464    fn format_params(params: Params) -> String {
465        if !params.is_empty() {
466            let param_string = params
467                .iter()
468                .map(|&(name, value)| format!("{}={}", name, value))
469                .collect::<Vec<String>>()
470                .join("&");
471
472            let mut args = "?".to_owned();
473            args.push_str(param_string.as_str());
474            args
475        } else {
476            String::new()
477        }
478    }
479
480    fn handle_response(response: Response) -> MMCResult<String> {
481        match response.status() {
482            StatusCode::OK | StatusCode::NO_CONTENT => Client::parse_success(response),
483            StatusCode::BAD_REQUEST => Client::parse_bad_request(response),
484            StatusCode::UNAUTHORIZED | StatusCode::FORBIDDEN => Err(MMCError::NotAuthorized),
485            StatusCode::NOT_FOUND => Err(MMCError::ResourceNotFound),
486            x => Err(MMCError::APIFailure(x)),
487        }
488    }
489
490    fn parse_success(response: Response) -> MMCResult<String> {
491        Client::parse_response_body(response)
492    }
493
494    fn parse_bad_request(response: Response) -> MMCResult<String> {
495        Client::parse_response_body(response).and_then(|body| Err(MMCError::BadRequest(body)))
496    }
497
498    fn parse_response_body(mut response: Response) -> MMCResult<String> {
499        // Create a buffer to read the response stream into
500        let mut buffer = Vec::new();
501
502        // Try to read the response into the buffer and return with a
503        // io error in the case of a failure
504        r#try!(response.read_to_end(&mut buffer).map_err(MMCError::Io));
505
506        // Generate a string from the buffer
507        let result = String::from_utf8(buffer);
508
509        // Return either successfully generated string or a conversion error
510        match result {
511            Ok(string) => Ok(string),
512            Err(err) => Err(MMCError::Convert(err)),
513        }
514    }
515}