firebase-rs 2.0.0

Rust based Firebase library
Documentation
#![allow(warnings)]

use reqwest::{Error, StatusCode};
use serde::de::DeserializeOwned;
use serde::Serialize;
use serde_json::Value;
use std::fmt::Debug;
use url::Url;

use constants::{Method, AUTH};
use errors::UrlParseError;
use utils::check_uri;

use crate::constants::Response;
use crate::errors::RequestError;
use crate::params::Params;

mod constants;
mod errors;
mod utils;
mod params;

#[derive(Debug)]
pub struct Firebase {
    uri: Url,
}

impl Firebase {
    pub fn new(uri: String) -> Result<Self, UrlParseError>
        where
            Self: Sized,
    {
        match check_uri(&uri) {
            Ok(uri) => Ok(Self { uri }),
            Err(err) => Err(err),
        }
    }

    pub fn auth(uri: String, auth_key: String) -> Result<Self, UrlParseError>
        where
            Self: Sized,
    {
        match check_uri(&uri) {
            Ok(mut uri) => {
                uri.set_query(Some(&format!("{}={}", AUTH, auth_key)));
                Ok(Self { uri })
            }
            Err(err) => Err(err),
        }
    }

    pub fn with_params(&self) -> Params {
        let uri = self.uri.clone();
        Params::new(uri)
    }

    pub fn at(&mut self, path: &str) -> Self {
        let mut new_path = String::default();

        let paths = self.uri.path_segments().map(|p| p.collect::<Vec<_>>());
        for mut path in paths.unwrap() {
            if path.find(".json").is_some() {
                path = path.trim_end_matches(".json");
            }
            new_path += format!("{}/", path).as_str();
        }

        new_path += path;

        if new_path.find(".json").is_some() {
            new_path = new_path.trim_end_matches(".json").to_string();
        }

        self.uri
            .set_path(format!("{}.json", new_path.as_str()).as_str());

        Self {
            uri: self.uri.clone(),
        }
    }

    pub fn get_uri(&self) -> String {
        self.uri.to_string()
    }

    pub async fn request(
        &self,
        method: Method,
        data: Option<Value>,
    ) -> Result<Response, RequestError> {
        let client = reqwest::Client::new();

        match method {
            Method::GET => {
                let request = client.get(self.uri.to_string()).send().await;
                return match request {
                    Ok(response) => {
                        if response.status() == StatusCode::from_u16(200).unwrap() {
                            return match response.text().await {
                                Ok(data) => {
                                    if data == String::from("null") {
                                        return Err(RequestError::NotFoundOrNullBody);
                                    }
                                    return Ok(Response { data });
                                }
                                Err(_) => Err(RequestError::NotJSON),
                            };
                        } else {
                            Err(RequestError::NetworkError)
                        }
                    }
                    Err(_) => return Err(RequestError::NetworkError),
                };
            }
            Method::POST => {
                if !data.is_some() {
                    return Err(RequestError::SerializeError);
                }

                let request = client.post(self.uri.to_string()).json(&data).send().await;
                return match request {
                    Ok(response) => {
                        let data = response.text().await.unwrap();
                        Ok(Response { data })
                    }
                    Err(_) => Err(RequestError::NetworkError),
                };
            }
            Method::PUT => {
                if !data.is_some() {
                    return Err(RequestError::SerializeError);
                }

                let request = client.put(self.uri.to_string()).json(&data).send().await;
                return match request {
                    Ok(response) => {
                        let data = response.text().await.unwrap();
                        Ok(Response { data })
                    }
                    Err(_) => Err(RequestError::NetworkError),
                };
            }
            Method::DELETE => {
                let request = client.delete(self.uri.to_string()).send().await;
                return match request {
                    Ok(_) => Ok(Response {
                        data: String::default(),
                    }),
                    Err(_) => Err(RequestError::NetworkError),
                };
            }
            _ => {}
        }

        Err(RequestError::NetworkError)
    }

    pub async fn request_generic<T>(&self, method: Method) -> Result<T, RequestError>
        where
            T: Serialize + DeserializeOwned + Debug,
    {
        let request = self.request(method, None).await;

        match request {
            Ok(response) => {
                let data: T = serde_json::from_str(response.data.as_str()).unwrap();

                Ok(data)
            }
            Err(err) => Err(err),
        }
    }

    pub async fn set<T>(&self, data: &T) -> Result<Response, RequestError>
        where
            T: Serialize + DeserializeOwned + Debug,
    {
        let data = serde_json::to_value(&data).unwrap();
        self.request(Method::POST, Some(data)).await
    }

    pub async fn get(&self) -> Result<Response, RequestError> {
        self.request(Method::GET, None).await
    }

    pub async fn get_generic<T>(&self) -> Result<T, RequestError>
        where
            T: Serialize + DeserializeOwned + Debug,
    {
        self.request_generic::<T>(Method::GET).await
    }

    pub async fn delete(&self) -> Result<Response, RequestError> {
        self.request(Method::DELETE, None).await
    }

    pub async fn update<T>(&self, data: &T) -> Result<Response, RequestError>
        where
            T: DeserializeOwned + Serialize + Debug,
    {
        let value = serde_json::to_value(&data).unwrap();
        self.request(Method::PUT, Some(value)).await
    }
}

#[cfg(test)]
mod tests {
    use serde::{Deserialize, Serialize};
    use std::collections::HashMap;
    use std::num::NonZeroU16;

    use crate::{Firebase, Method, UrlParseError};

    const URI: &str = "https://firebase_id.firebaseio.com";
    const URI_WITH_SLASH: &str = "https://firebase_id.firebaseio.com/";
    const URI_NON_HTTPS: &str = "http://firebase_id.firebaseio.com/";

    #[tokio::test]
    async fn simple() {
        let firebase = Firebase::new(URI.to_string()).unwrap();
        assert_eq!(URI_WITH_SLASH.to_string(), firebase.get_uri());
    }

    #[tokio::test]
    async fn non_https() {
        let firebase = Firebase::new(URI_NON_HTTPS.to_string()).map_err(|e| e.to_string());
        assert_eq!(
            firebase.err(),
            Some(String::from(UrlParseError::NotHttps.to_string()))
        );
    }

    #[tokio::test]
    async fn with_auth() {
        let firebase = Firebase::auth(URI.to_string(), String::from("auth_key")).unwrap();
        assert_eq!(
            format!("{}/?auth=auth_key", URI.to_string()),
            firebase.get_uri()
        );
    }

    #[tokio::test]
    async fn at() {
        let firebase = Firebase::new(URI.to_string())
            .unwrap()
            .at("movies/movie1.json");
        assert_eq!(format!("{}/movies/movie1.json", URI), firebase.get_uri());
    }
}