cortical-io 0.1.1

Cortical.io API client
Documentation
use std::error::Error;

use serde::{Deserialize, Serialize};

#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Retina {
    #[serde(rename = "retinaName")]
    pub retina_name: String,
    #[serde(rename = "numberOfColumns")]
    pub number_of_columns: i64,
    #[serde(rename = "numberOfTermsInRetina")]
    pub number_of_terms_in_retina: i64,
    pub description: String,
    #[serde(rename = "numberOfRows")]
    pub number_of_rows: i64,
}

#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Fingerprint {
    pub positions: Vec<i32>,
}

impl Fingerprint {
    pub fn expand(&self, len: usize) -> Vec<u8> {
        let mut expanded = vec![0; len];

        for pos in &self.positions {
            expanded[*pos as usize] = 1;
        }

        expanded
    }
}

#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct TextSlice {
    pub text: String,
    pub fingerprint: Option<Fingerprint>,
}

#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct TextSliceRequest {
    pub retina_name: String,
    pub start_index: usize,
    pub max_results: usize,
    pub get_fingerprint: bool,
}

impl TextSliceRequest {
    pub fn new() -> Self {
        Self {
            retina_name: "en_general".to_string(),
            start_index: 0,
            max_results: 10,
            get_fingerprint: false,
        }
    }

    pub fn with_retina_name(mut self, retina_name: &str) -> Self {
        self.retina_name = retina_name.to_string();
        self
    }

    pub fn with_start_index(mut self, start_index: usize) -> Self {
        self.start_index = start_index;
        self
    }

    pub fn with_max_results(mut self, max_results: usize) -> Self {
        self.max_results = max_results;
        self
    }

    pub fn with_get_fingerprint(mut self, get_fingerprint: bool) -> Self {
        self.get_fingerprint = get_fingerprint;
        self
    }
}

#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct TextEnvelope {
    pub text: String,
}

impl TextEnvelope {
    pub fn new(text: &str) -> Self {
        Self {
            text: text.to_string(),
        }
    }
}

#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct CreateCategoryFilterRequest {
    pub category_name: Option<String>,
    pub positive_examples: Vec<TextEnvelope>,
    pub negative_examples: Vec<TextEnvelope>,
}

#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct LanguageResponse {
    pub language: Option<String>,
    pub iso_tag: Option<String>,
    pub wiki_url: Option<String>,
}


#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct CompareResponse {
    #[serde(rename = "sizeLeft")]
    pub size_left: i64,
    #[serde(rename = "sizeRight")]
    pub size_right: i64,
    #[serde(rename = "weightedScoring")]
    pub weighted_scoring: f64,
    #[serde(rename = "euclideanDistance")]
    pub euclidean_distance: f64,
    #[serde(rename = "jaccardDistance")]
    pub jaccard_distance: f64,
    #[serde(rename = "overlappingAll")]
    pub overlapping_all: i64,
    #[serde(rename = "overlappingLeftRight")]
    pub overlapping_left_right: f64,
    #[serde(rename = "overlappingRightLeft")]
    pub overlapping_right_left: f64,
    #[serde(rename = "cosineSimilarity")]
    pub cosine_similarity: f64,
}

pub struct Cortical {
    pub client: reqwest::Client,
    pub base_url: String,
}

impl Cortical {
    pub fn new() -> Cortical {
        Cortical {
            client: reqwest::Client::new(),
            base_url: std::env::var("CORTICAL_API_URL").unwrap_or("https://languages.cortical.io".to_string()),
        }
    }

    pub async fn get_retinas(&self) -> Result<Vec<Retina>, Box<dyn Error>> {
        let response =
            self.client
                .get(format!("{}{}", &self.base_url, "/rest/retinas"))
                .send()
                .await?;

        Ok(
            response
                .json()
                .await?
        )
    }

    pub async fn get_text_analysis(
        &self,
        text: &str,
        retina_name: Option<&str>,
    ) -> Result<Vec<Fingerprint>, Box<dyn Error>> {
        let retina_name = retina_name.unwrap_or("en_general");

        let response =
            self.client
                .post(format!("{}/rest/text?retina_name={}", &self.base_url, retina_name))
                .header("Accept", "application/json")
                .header("Referer", "")
                .header("Content-Type", "application/json")
                .body(text.to_string())
                .send()
                .await?;

        Ok(
            response
                .json()
                .await?
        )
    }

    pub async fn get_text_keywords(
        &self,
        text: &str,
        retina_name: Option<&str>,
    ) -> Result<Vec<String>, Box<dyn Error>> {
        let retina_name = retina_name.unwrap_or("en_general");

        let response =
            self.client
                .post(format!("{}/rest/text/keywords?retina_name={}", &self.base_url, retina_name))
                .header("Accept", "application/json")
                .header("Referer", "")
                .header("Content-Type", "text/plain;charset=UTF-8")
                .body(text.to_string())
                .send()
                .await?;

        Ok(
            response
                .json()
                .await?
        )
    }

    pub async fn get_text_slices(
        &self,
        text: &str,
        params: Option<TextSliceRequest>,
    ) -> Result<Vec<TextSlice>, Box<dyn Error>> {
        let params = params.unwrap_or_default();

        let response =
            self.client
                .post(
                    format!(
                        "{}/rest/text/slices?retina_name={}&start_index={}&max_results={}&get_fingerprint={}",
                        &self.base_url,
                        params.retina_name,
                        params.start_index,
                        params.max_results,
                        params.get_fingerprint
                    )
                )
                .header("Accept", "application/json")
                .header("Referer", "")
                .header("Content-Type", "application/json")
                .body(text.to_string())
                .send()
                .await?;

        Ok(
            response
                .json()
                .await?
        )
    }

    pub async fn get_text_detect_language(
        &self,
        text: &str,
    ) -> Result<LanguageResponse, Box<dyn Error>> {
        let response =
            self.client
                .post(format!("{}/rest/text/detect_language", &self.base_url))
                .header("Accept", "application/json")
                .header("Referer", "")
                .header("Content-Type", "application/json")
                .body(text.to_string())
                .send()
                .await?;

        Ok(
            response
                .json()
                .await?
        )
    }

    pub async fn create_category_filter(
        &self,
        positive_examples: Vec<String>,
        negative_examples: Vec<String>,
        retina_name: Option<&str>,
    ) -> Result<(), Box<dyn Error>> {
        let retina_name = retina_name.unwrap_or("en_general");

        let positive_examples = positive_examples
            .into_iter()
            .map(|text| TextEnvelope { text })
            .collect();

        let negative_examples = negative_examples
            .into_iter()
            .map(|text| TextEnvelope { text })
            .collect();

        let request = CreateCategoryFilterRequest {
            category_name: None,
            positive_examples,
            negative_examples,
        };

        Ok(
            self.client
                .post(
                    format!(
                        "{}/rest/classify/create_category_filter?retina_name={}&filter_name={}",
                        &self.base_url,
                        retina_name,
                        "filter_name"
                    )
                )
                .header("Accept", "application/json")
                .header("Referer", "")
                .header("Content-Type", "application/json")
                .body(serde_json::to_string(&request)?)
                .send()
                .await?
                .json()
                .await?
        )
    }

    pub async fn get_compare(
        &self,
        (text1, text2): (&str, &str),
        retina_name: Option<&str>,
    ) -> Result<CompareResponse, Box<dyn Error>> {
        let retina_name = retina_name.unwrap_or("en_general");

        Ok(
            self.client
                .post(format!("{}/rest/compare?retina_name={}", &self.base_url, retina_name))
                .header("Accept", "application/json")
                .header("Referer", "")
                .header("Content-Type", "application/json")
                .body(
                    serde_json::to_string(
                        &vec![
                            TextEnvelope::new(text1),
                            TextEnvelope::new(text2),
                        ]
                    )?
                )
                .send()
                .await?
                .json()
                .await?
        )
    }
}