async_odoors 1.0.0

Odoo JSON-RPC client async library
Documentation
use std::collections::HashMap;

use serde::de::DeserializeOwned;
use serde::{Deserialize, Deserializer, Serialize};
use serde_json::{Map, Number, Value};

use crate::api::{Request, Response};
use crate::error::Error;

pub fn deserialize_odoo_nullable<'de, D, E>(data: D) -> Result<Option<E>, D::Error>
where
    D: Deserializer<'de>,
    E: Deserialize<'de>,
{
    let b = Deserialize::deserialize(data);

    match b {
        Ok(e) => Ok(Some(e)),
        Err(_) => Ok(None),
    }
}

#[derive(Debug)]
pub struct Odoo {
    host: String,
    database: String,
    uid: Option<u32>,
    password: Option<String>,
}

impl Odoo {
    pub fn new(host: &str, database: &str) -> Odoo {
        Odoo {
            host: host.to_string(),
            database: database.to_string(),
            uid: None,
            password: None,
        }
    }

    pub async fn new_and_login(
        host: &str,
        database: &str,
        login: &str,
        password: &str,
    ) -> Result<Odoo, Error> {
        let mut odoo = Odoo::new(host, database);
        odoo.login(login, password).await?;
        Ok(odoo)
    }

    pub async fn login(&mut self, login: &str, password: &str) -> Result<u32, Error> {
        let request = Request::new(
            "common",
            Some("authenticate"),
            (self.database.as_str(), login, password, ""),
        );
        let response: Response<u32> = self
            .send(&request, None)
            .await
            .map_err(|e| Error(e.to_string()))?;
        self.uid = Some(response.result);
        self.password = Some(password.to_string());
        Ok(response.result)
    }

    pub async fn start(&self) -> Result<HashMap<String, String>, Error> {
        let request: Request<()> = Request::new("common", Some("start"), ());

        let response: Response<HashMap<String, String>> = self
            .send(&request, Some("start"))
            .await
            .map_err(|e| Error(e.to_string()))?;

        Ok(response.result)
    }

    pub async fn call<T: Serialize, U: DeserializeOwned>(
        &self,
        model: &str,
        method: &str,
        args: T,
    ) -> Result<Response<U>, Error> {
        let password = self.password.as_ref().unwrap().as_str();

        let request = Request::new(
            "object",
            None,
            (
                self.database.as_str(),
                self.uid,
                password,
                model,
                method,
                args,
            ),
        );

        self.send(&request, None)
            .await
            .map_err(|e| Error(e.to_string()))
    }

    pub async fn search_read<T: Serialize, U: DeserializeOwned>(
        &self,
        model: &str,
        domain: T,
        fields: Option<Vec<&str>>,
        limit: Option<u32>,
        offset: Option<u32>,
    ) -> Result<Response<U>, Error> {
        let password = self.password.as_ref().unwrap().as_str();
        let fields = fields.unwrap_or(vec![]);

        let mut values = Map::new();
        values.insert(
            "fields".to_string(),
            Value::Array(
                fields
                    .iter()
                    .map(|f| Value::String(f.to_string()))
                    .collect(),
            ),
        );
        if limit.is_some() {
            values.insert(
                "limit".to_string(),
                Value::Number(Number::from(limit.unwrap())),
            );
        }
        if offset.is_some() {
            values.insert(
                "offset".to_string(),
                Value::Number(Number::from(offset.unwrap())),
            );
        }

        let request = Request::new(
            "object",
            None,
            (
                self.database.as_str(),
                self.uid,
                password,
                model,
                "search_read",
                vec![domain],
                values,
            ),
        );

        self.send(&request, None)
            .await
            .map_err(|e| Error(e.to_string()))
    }

    async fn send<T: Serialize, U: DeserializeOwned>(
        &self,
        request: &Request<T>,
        url: Option<&str>,
    ) -> Result<Response<U>, reqwest::Error> {
        let client = reqwest::Client::new();
        let url = format!("{}/{}", self.host, url.unwrap_or("jsonrpc"));
        let resp = client.post(&url).json(&request).send().await;
        Ok(resp?.json().await?)
    }
}

#[cfg(test)]
mod tests {
    use std::collections::HashMap;

    use serde::Deserialize;
    use serde_json::{Map, Value};

    use crate::api::Response;
    use crate::odoo::{deserialize_odoo_nullable, Odoo};

    async fn get_odoo() -> Odoo {
        let odoo = Odoo::new("https://demo.odoo.com", "");
        let values = odoo.start().await.unwrap();
        Odoo::new_and_login(
            values.get("host").unwrap(),
            values.get("database").unwrap(),
            values.get("user").unwrap(),
            values.get("password").unwrap(),
        )
        .await
        .unwrap()
    }

    #[tokio::test]
    async fn test_start() {
        let odoo = Odoo::new("https://demo.odoo.com", "");
        let values = odoo.start().await.unwrap();
        assert_eq!(values.is_empty(), false);
        assert_eq!(values.contains_key("host"), true);
        assert_eq!(values.contains_key("database"), true);
        assert_eq!(values.contains_key("user"), true);
        assert_eq!(values.contains_key("password"), true);
    }

    #[tokio::test]
    async fn test_login() {
        let odoo = get_odoo().await;
        assert_ne!(odoo.uid.unwrap(), 0);
    }

    #[tokio::test]
    async fn test_login_failed() {
        let mut odoo = Odoo::new("https://demo.odoo.com", "fake");
        let resp = odoo.login("admin", "admin").await;
        assert_eq!(resp.is_err(), true);
    }

    #[tokio::test]
    async fn test_new_and_login_failed() {
        let odoo = Odoo::new_and_login("https://demo.odoo.com", "fake", "admin", "admin").await;
        assert_eq!(odoo.is_err(), true);
    }

    #[tokio::test]
    async fn test_search() {
        let odoo = get_odoo();
        let partners: Response<Vec<u32>> = odoo
            .await
            .call("res.partner", "search", [[["id", ">", "2"]]])
            .await
            .unwrap();
        assert_ne!(partners.result.len(), 0);
    }

    #[tokio::test]
    async fn test_read() {
        let odoo = get_odoo().await;
        let partners: Response<Vec<HashMap<String, Value>>> = odoo
            .call("res.partner", "read", ([2], ["name"]))
            .await
            .unwrap();
        assert_eq!(partners.result.len(), 1);
        assert_eq!(partners.result.get(0).unwrap().get("id").unwrap(), 2);
    }

    #[tokio::test]
    async fn test_search_read() {
        let odoo = get_odoo().await;
        let partners: Response<Vec<Value>> = odoo
            .search_read(
                "res.partner",
                (("id", ">", 2),),
                Some(vec!["name"]),
                None,
                None,
            )
            .await
            .unwrap();
        assert_ne!(partners.result.len(), 0);
        for partner in partners.result {
            let p = partner.as_object().unwrap();
            assert_ne!(p.get("id").unwrap().as_i64().unwrap(), 0);
        }

        let partners: Response<Vec<Value>> = odoo
            .search_read(
                "res.partner",
                (("id", ">", 0),),
                Some(vec!["name"]),
                Some(5),
                None,
            )
            .await
            .unwrap();
        assert_eq!(partners.result.len(), 5);
    }

    #[tokio::test]
    async fn test_create_and_write() {
        let odoo = get_odoo().await;
        let mut values = Map::new();
        values.insert("name".to_string(), Value::from("Test"));
        let result: Response<u32> = odoo
            .call("res.partner", "create", vec![&values])
            .await
            .unwrap();
        let id = result.result;
        assert_ne!(id, 0);
        let result: Response<bool> = odoo
            .call("res.partner", "write", (vec![id], &values))
            .await
            .unwrap();
        assert_eq!(result.result, true);
    }

    #[derive(Deserialize)]
    struct Partner {
        id: u32,
        name: String,
    }

    #[tokio::test]
    async fn test_search_read_serde() {
        let odoo = get_odoo().await;

        let partners: Response<Vec<Partner>> = odoo
            .search_read("res.partner", (("id", ">", 2),), None, Some(5), None)
            .await
            .unwrap();
        let partners = partners.result;
        assert_eq!(partners.len(), 5);
        for partner in partners {
            assert_ne!(partner.id, 0);
            assert_ne!(partner.name.len(), 0);
        }
    }

    #[derive(Deserialize)]
    struct ProductTemplate {
        pub id: u32,
        pub name: String,
        #[serde(deserialize_with = "deserialize_odoo_nullable")]
        pub default_code: Option<String>,
    }

    #[tokio::test]
    async fn test_search_read_serde_nullable() {
        let odoo = get_odoo().await;

        let products: Response<Vec<ProductTemplate>> = odoo
            .search_read(
                "product.template",
                (("default_code", "=", false),),
                Some(vec!["name", "default_code"]),
                Some(5),
                None,
            )
            .await
            .unwrap();
        let products = products.result;
        assert_eq!(products.len(), 5);
        for product in products {
            assert_ne!(product.id, 0);
            assert_ne!(product.name.len(), 0);
            assert_eq!(product.default_code, None);
        }
    }
}