lightql 0.2.0

lightweight graphql request builder.
//!
//! # LightQl
//!
//! This library is a lightweight library that let you make graphql request and serialize them
//! into the given type.
//! The fields of the given type should match with the "data" content of the reponses.
//! **Examples**:
//! ```no_test
//! use lightql::{Request, QlType};
//!
//! let response = json!(
//!     "data": {
//!         "players": [
//!             {
//!                 "id": 1,
//!                 "name": "fred"
//!             },
//!             {
//!                 "id": 2,
//!                 "name": "marcel"
//!             },
//!             {
//!                 "id": 3,
//!                 "name": "roger"
//!             }
//!         ]
//!     }
//! )
//!
//! // The given type should look like that:
//! #[derive(Deserialize)]
//! struct Player {
//!     #[serde(default = "get_default_id")]
//!     id: i32,
//!     name: String
//! }
//!
//! #[derive(Deserialize)]
//! struct Players {
//!     players: Vec<Player>
//! }
//!
//! // This will work.
//! // But here if you want to request one player:
//!
//! let response = json!(
//!     "data": {
//!         "player": {
//!             "id": 4,
//!             "name": "bouras"
//!         }
//!     }
//! );
//!
//! #[derive(Deserialize)]
//! struct OnePlayer {
//!     player: Player
//! }
//!
//! // default function
//! fn get_default_id() -> i32 {
//!     0
//! }
//!
//! // You have to match the "data" content with field.
//! ```
//!
//! # Usage
//! To use the library there is the `Request` struct that encapsulate the request.
//! ```no_test
//!     let player: OnePlayer = Request::new("player(id: 1) { name }", QlType::Query)
//!         .send("https://your-api/")
//!         .expect("Cannot fetch player");
//!
//!     // print: Player { id: 0, name: "fred" }
//!     println!("{:?}", player);
//!
//! ```
//! Send automatically infer type of deserialisation.
//!
//! # From file
//!
//! You can make complexe request request from a file for example:
//! ```no_test
//! let response = Request::from_path("path/to/ql/request.query")
//!     .send::<OnePlayer>("http://your-api/") // Note the turbo-fish
//!     .unwrap();
//! ```
//!
//! # Parameter
//!
//! If you need runtime parameter you can use prepare and an hasmap.
//! ```no_test
//! let user_id = get_user_id(&user);
//!
//! let mut params = Hasmap::new();
//! params.insert(
//!     "_id",
//!     user_id.parse()
//! );
//!
//! let response = Request::new("player(id: _id) { id, name }", QlType::Query)
//!     .prepare(Some(params)) // Should prepare before sending !
//!     .send::<OnePlayer>("https://your-api/")
//!     .unwrap(); // If not prepared return Err(NotPrepared)
//!
//! ```
#![feature(custom_attribute)]
#![feature(associated_type_defaults)]
#![feature(pattern)]
#![allow(dead_code)]
extern crate mio_httpc;
#[macro_use]
pub extern crate serde_derive;
extern crate serde_json;

mod test;

use mio_httpc::CallBuilder;
pub use serde_json::Value;
use std::collections::HashMap;
use std::error::Error;
use std::fmt;
use std::str::pattern::Pattern;
use mio_httpc::Error as MioError;


#[derive(Debug, PartialEq, Eq)]
/// Define the type of the request.
/// either: a query, a subscription or a mutation.
pub enum QlType {
    Subscribe,
    Query,
    Mutation,
}

#[derive(Debug, Deserialize)]
/// A location in a graphql Error.
pub struct Locations {
    line: u32,
    column: u32,
}

#[derive(Deserialize, Debug)]
/// A Error message that have to be Deserialized.
/// TODO add a pretty print.
pub struct QlErrorInternal {
    message: String,
    locations: Vec<Locations>,
}

#[derive(Debug)]
/// Error type of the crate represent an backendError or a lib usage error.
pub enum QlError {
    NotPrepared,
    EmptyHashmap,
    BadUrl,
    CannotExec(&'static str),
    Internal(Vec<QlErrorInternal>),
}

#[derive(Debug, PartialEq, Eq)]
/// The principal struct of the lib.
/// It's the API that you should use.
/// ```no_test
/// let players: Players = Request::new("{players { uuid }}", QlType::Query)
///     .send("https://your-api-url/"); // Simple inline request.
///
/// let id = 12332;
/// let mut user_id = Hasmap::new();
/// user_id.insert(
///     "_id",
///     id::parse()
/// );
/// let user = Request::from_path("path/to/ql/request.query")
///     .prepare(user_id)
///     .send::<User>("https://your-api-url/"); // Complexe request creation from file.
///
/// ```
pub struct Request {
    data: String,
    ql_type: QlType,
    prepared: bool,
    method: String
}

#[derive(Deserialize, Debug)]
struct QlResponse<T> {
    data: Option<T>,
    errors: Option<Vec<QlErrorInternal>>,
    #[serde(flatten)]
    extra: HashMap<String, Value>,
}

#[derive(Debug)]
/// A wrapper around Request where the type is contained.
pub struct QlRequestTyped<T> {
    t: std::marker::PhantomData<T>,
    request: Request,
}

impl<T> QlRequestTyped<T> {
    /// Create a new typed request.
    pub fn new(query: &str) -> Self {
        Self {
            t: std::marker::PhantomData,
            request: Request::new(query),
        }
    }

    /// Create a typed request from a file path.
    /// The file **have to** end with .(QlType)
    /// `player_request.query` or `player_update.mutation` etc...
    pub fn from_path(name: &str) -> Result<Self, std::io::Error> {
        let request = Request::from_path(name)?;
        Ok(Self {
            t: std::marker::PhantomData,
            request,
        })
    }

    /// prepare the request with a hasmap for each argument
    /// ```no_test
    /// let mut param_map = Hashmap::new();
    /// param_map.insert("_uuid", "The real value");
    /// let response = Request::new("player(uuid: \"_uuid\") { uuid }", QlType::Query)
    ///     .prepare(param_map)
    ///     .send::<Player>()
    ///     .expect("Player cannot be retrieved");
    /// ```
    /// If the is no args to the query the send should detect it, so you don't need to use prepare.
    pub fn prepare(mut self, opt_map: HashMap<String, String>) -> Result<Self, QlError> {
        self.request = self.request.prepare(opt_map)?;
        Ok(self)
    }

    /// Force the preparation in certain case that can be usefull.
    pub fn force_prepare(mut self) -> Self {
        self.request.prepared = true;
        self
    }

    /// The send the request and return the requested type.
    pub fn send(&self, uri: &str) -> Result<T, QlError>
    where
        T: serde::de::DeserializeOwned,
    {
        self.request.send::<T>(uri)
    }
}

impl Request {
    /// Create a new request.
    pub fn new(query: &str) -> Request {
        Request {
            data: String::from(query),
            ql_type: QlType::Query,
            prepared: false,
            method: "POST".to_string()
        }
    }

    /// Get method for the request. Default is post.
    pub fn get(mut self) -> Request {
        self.method = "GET".to_string();
        self
    }

    pub fn post(mut self) -> Request {
        self.method = "POST".to_string();
        self
    }

    /// type of the request. default is query
    pub fn ql_type(mut self, ql_type: QlType) -> Request {
        self.ql_type = ql_type;
        self
    }

    /// Create a request from a file.
    pub fn from_path(name: &str) -> Result<Request, std::io::Error> {
        let request = std::fs::read_to_string(name)?;

        let ql_type = if name.is_suffix_of(".mutation") {
            QlType::Mutation
        } else if name.is_suffix_of(".subscription") {
            QlType::Subscribe
        } else {
            QlType::Query
        };

        Ok(Request {
            data: request,
            ql_type,
            prepared: false,
            method: "POST".to_string()
        })
    }

    /// prepare the request with a hasmap for each argument
    /// ```no_test
    /// let mut param_map = Hashmap::new();
    /// param_map.insert("_uuid", "The real value");
    /// let response = Request::new("player(uuid: \"_uuid\") { uuid }", QlType::Query)
    ///     .prepare(param_map)
    ///     .send::<Player>()
    ///     .expect("Player cannot be retrieved");
    /// ```
    /// If the is no args to the query the send should detect it, so you don't need to use prepare.
    pub fn prepare(mut self, opt_map: HashMap<String, String>) -> Result<Self, QlError> {
        for (key, data) in opt_map {
            self.data = self.data.replace(&key, &data);
        }
        self.prepared = true;
        Ok(self)
    }

    /// Force the preparation in certain case that can be usefull.
    pub fn force_prepare(mut self) -> Self {
        self.prepared = true;
        self
    }

    /// Send need a type to Deserialize the response of the sended request.
    /// you can either use it like:
    /// ```no_test
    /// let player: Player = request.send();
    ///
    /// let player = request.send::<Player>(); // Turbo-fish :D
    /// ```
    pub fn send<T>(&self, uri: &str) -> Result<T, QlError>
        where T: serde::de::DeserializeOwned,
    {
        if !self.prepared && self.param_needed() > 0 {
            return Err(QlError::NotPrepared);
        }
        let res = CallBuilder::new()
            .method(self.method.as_str())
            .body(self.data.clone().into_bytes())
            .header("content-type", "application/graphql")
            .url(uri)?
            .exec()?;
        let string = String::from_utf8(res.1).expect("Cannot stringify");
        let res: QlResponse<T> = serde_json::from_str(&string).expect("Cannot Jsonify");
        if let Some(err) = res.errors {
            Err(QlError::Internal(err))
        } else {
            Ok(res.data.unwrap())
        }
    }

    /// Count the number of param needed by the request.
    pub fn param_needed(&self) -> i32 {
        let mut output = 0;

        for elem in self.data.chars() {
            if elem == ':' {
                output += 1;
            }
        }
        output
    }
}

/// Trait that will be derived in a near future.
pub trait FromQl: serde::de::DeserializeOwned + Sized {
    fn request(_uri: &str, _request: &str, _identifier: &str) -> Option<Self> {
        unimplemented!("TODO implement a derive");
    }
}

// Convert from MioError to QlError.
impl From<MioError> for QlError {
    fn from(error: MioError) -> QlError {
        match error {
            MioError::Httparse(_) => QlError::BadUrl,
            MioError::TimeOut => QlError::CannotExec("Time out"),
            MioError::Url(_) => QlError::BadUrl,
            _ => QlError::CannotExec("Cannot exec")
        }
    }
}

// TODO: increment.
impl fmt::Display for QlError {
    fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
        write!(f, "{}", self.description())
    }
}

impl Error for QlError {
    fn description(&self) -> &str {
        match self {
            QlError::NotPrepared => "You should prepare your request before sending it.",
            QlError::EmptyHashmap => "You should replace needed params",
            QlError::Internal(_) => "Internal Error: {}",
            QlError::BadUrl => "Bad url, cannot use it",
            QlError::CannotExec(_) => "Cannot exec this request {}"
        }
    }
}