df_st_api 0.3.0-development-2

Starting an API server for the DF Storyteller project.
use crate::api_errors::APIErrorBadRequest;
use crate::api_objects::ApiObject;
use crate::pagination::{ApiCountPagination, ApiMinimalItem, ApiPageLinks};
use crate::ServerInfo;
use anyhow::Error;
use df_st_core::fillable::{Fillable, Filler};
use df_st_core::SchemaExample;
use df_st_db::{string_filter, DBObject, MatchBy};
#[allow(unused_imports)]
use log::{debug, error, info, trace, warn};
use rocket::State;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::cmp;
use std::collections::HashMap;
use std::fmt::Debug;

/// A paged response from the API.
/// The actual requested data can be found in the `data` object.
#[derive(Serialize, Deserialize, Clone, Debug, Default, JsonSchema)]
#[schemars(example = "Self::example")]
pub struct ApiCountPage<C, D>
where
    C: ApiObject + Serialize + SchemaExample,
    D: ApiObject + Serialize + SchemaExample,
{
    /// The maximum amount if items in this response.
    pub max_page_size: u32,
    /// The total amount of items. (across all pages)
    /// Note: id's usually start at 0 so if there are 10 total items the list id is 9.
    /// But id's can be missing so do not use this to calculate id's!
    pub total_item_count: u32,
    /// The offset from the start of the database structure.
    /// This value takes the sorting of the page into account.
    pub page_start: u32,
    /// The actual size of the response. (page_size<=max_page_size).
    pub page_size: u32,
    /// The number of the page.
    /// This value is `page_nr = page_start/max_page_size`
    pub page_nr: u32,
    /// Name of the field it is grouped by.
    pub group_by: Option<String>,
    /// Allows filtering over a property.
    /// The property has to be of type `String` or `i32`, so it will not work for all properties.
    /// Example: "type" or "year".
    #[serde(skip_serializing_if = "Option::is_none")]
    pub filter_by: Option<String>,
    /// The values that is used as the filter. Example: "hf_died" or "1000".
    /// If value is wrong type a `400 Bad Request` will be returned.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub filter_value: Option<String>,
    /// Tag to identify this version of the page.
    /// [More info here](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/ETag).
    /// This tag can be used to improve caching.
    /// ETag is a Sha256 of the whole response
    /// (including links and other metadata, excluding the etag itself.).
    pub etag: String,
    /// Links the various related items.
    pub links: ApiPageLinks,
    /// List of actual requested data.
    pub data: Vec<ApiMinimalItem<C>>,
    /// The etag given in the request.
    #[serde(skip)]
    pub given_etag: String,
    /// Server base url.
    /// Not included in response.
    #[serde(skip)]
    pub base_url: String,
    /// Maximum amount of items requested on a page.
    /// Set by server at startup, see config.
    /// Not included in response.
    #[serde(skip)]
    pub server_max_page_size: u32,
    /// Not used, but used to create dynamic links
    #[serde(skip)]
    parent_object: D,
}

impl<C, D> SchemaExample for ApiCountPage<C, D>
where
    C: SchemaExample + Serialize + ApiObject,
    D: SchemaExample + Serialize + ApiObject,
{
    fn example() -> Self {
        let link = D::get_count_link(&"http://127.0.0.1:20350/api".to_owned());
        Self {
            max_page_size: 100,
            total_item_count: 462,
            page_start: 0,
            page_size: 9,
            page_nr: 0,
            group_by: Some("type".to_owned()),
            filter_by: Some("type".to_owned()),
            filter_value: Some("library".to_owned()),
            etag: "b76e1b17f19bf1ab4901c4b21a747fc4898e656b3ac3080983498b07515fb655".to_owned(),
            links: ApiPageLinks {
                self_: format!("{}?group_by=type&per_page=100&page=1", link),
                first: Some(format!("{}?group_by=type&per_page=100&page=0", link)),
                prev: Some(format!("{}?group_by=type&per_page=100&page=0", link)),
                next: Some(format!("{}?group_by=type&per_page=100&page=2", link)),
                last: Some(format!("{}?group_by=type&per_page=100&page=5", link)),
            },
            data: vec![ApiMinimalItem::<C>::example()],

            given_etag: "".to_owned(),
            base_url: "".to_owned(),
            server_max_page_size: 0,
            parent_object: D::example(),
        }
    }
}

impl<C, D> ApiCountPage<C, D>
where
    C: ApiObject + Default + Serialize + SchemaExample,
    D: ApiObject + Default + Serialize + SchemaExample,
{
    pub fn new(pagination: &ApiCountPagination, server_info: &State<ServerInfo>) -> Self {
        let server_info = server_info.inner().clone();
        let mut new_object = Self {
            base_url: server_info.base_url.clone(),
            filter_by: pagination.filter_by.clone(),
            filter_value: pagination.filter_value.clone(),
            server_max_page_size: server_info.page_max_limit,
            max_page_size: server_info.default_max_page_size,
            ..Default::default()
        };
        if let Some(per_page) = pagination.per_page {
            // Make sure to not go over `page_max_limit`
            new_object.max_page_size = cmp::min(per_page, server_info.page_max_limit);
        }
        // make sure it is at least 1 (>=1)
        new_object.max_page_size = cmp::max(new_object.max_page_size, 1);
        if let Some(page) = pagination.page {
            new_object.page_start = page * new_object.max_page_size;
        }
        if let Some(etag) = &pagination.etag {
            new_object.given_etag = etag.clone();
        }

        new_object
    }

    /// Store the data in the object and construct links and calculate ETag.
    /// Return if true etag match with request tag.
    pub fn wrap(&mut self, list: Vec<C>) -> bool {
        self.data = self.warp_data_list(list);
        self.page_nr = self.page_start / self.max_page_size;
        self.set_links();
        self.set_etag()
    }

    fn set_links(&mut self) {
        let mut query_parameters = Vec::new();
        let qp_page = if self.page_start != 0 {
            format!("page={}", self.page_nr)
        } else {
            "".to_owned()
        };

        if self.max_page_size != self.server_max_page_size {
            query_parameters.push(format!("per_page={}", self.max_page_size));
        }

        if let Some(group_by) = &self.group_by {
            query_parameters.push(format!("group_by={}", group_by));
        }

        if let Some(filter_by) = &self.filter_by {
            query_parameters.push(format!("filter_by={}", filter_by));
        }

        if let Some(filter_value) = &self.filter_value {
            query_parameters.push(format!("filter_value={}", filter_value));
        }

        // Self
        let mut links = ApiPageLinks::default();
        let base_api_path = D::get_count_link(&self.base_url);
        if qp_page.is_empty() {
            links.self_ = format!("{}?{}", base_api_path, query_parameters.join("&"));
        } else {
            let mut local_qp = query_parameters.clone();
            local_qp.push(qp_page);
            links.self_ = format!("{}?{}", base_api_path, local_qp.join("&"));
        }
        // First
        if self.page_start >= 1 {
            let mut local_qp = query_parameters.clone();
            local_qp.push("page=0".to_owned());
            links.first = Some(format!("{}?{}", base_api_path, local_qp.join("&")));
        }
        // Prev
        if self.page_start >= 1 {
            let mut local_qp = query_parameters.clone();
            local_qp.push(format!("page={}", self.page_nr - 1));
            links.prev = Some(format!("{}?{}", base_api_path, local_qp.join("&")));
        }
        // Next
        if self.page_start + self.max_page_size < self.total_item_count {
            let mut local_qp = query_parameters.clone();
            local_qp.push(format!("page={}", self.page_nr + 1));
            links.next = Some(format!("{}?{}", base_api_path, local_qp.join("&")));
        }
        // Last
        if self.page_start + self.max_page_size < self.total_item_count {
            let mut local_qp = query_parameters;
            local_qp.push(format!(
                "page={}",
                (self.total_item_count - 1) / self.max_page_size
            ));
            links.last = Some(format!("{}?{}", base_api_path, local_qp.join("&")));
        }
        self.links = links;
    }

    /// Hash the data using Sha256 to create the ETag.
    /// After this function is called no extra data should be added.
    /// (or it should be rehashed).
    fn set_etag(&mut self) -> bool {
        // If Sha256 is no longer strong enough or is broken it should and can be easily
        // replaced in this function.
        use sha2::{Digest, Sha256};
        // Reset any already set ETag (so this does not effect the hash)
        self.etag = "".to_owned();
        // Hash the data to create the etag.
        let mut hasher = Sha256::new();
        // Convert data to json string.
        // Conversion is done to eliminate non-public values to effect the hash.
        let json_data = serde_json::to_string(self).unwrap();
        hasher.update(json_data);
        // Convert to `String` and store result in etag
        self.etag = format!("{:x}", hasher.finalize());
        // Check if ETags match
        self.etag == self.given_etag
    }

    fn warp_data_list(&mut self, list: Vec<C>) -> Vec<ApiMinimalItem<C>> {
        let mut data = Vec::new();
        self.page_size = list.len() as u32;

        for item in list {
            data.push(ApiMinimalItem::wrap_new(item, &self.base_url));
        }

        data
    }

    pub fn get_string_filter(&self) -> HashMap<String, String> {
        if self.filter_by.is_some() && self.filter_value.is_some() {
            let filter_by = self.filter_by.clone().unwrap_or_default();
            let filter_value = self.filter_value.clone().unwrap_or_default();
            string_filter![filter_by => filter_value]
        } else {
            string_filter![]
        }
    }

    pub fn add_int_filter(&self, mut int_filter: HashMap<String, i32>) -> HashMap<String, i32> {
        if self.filter_by.is_some() && self.filter_value.is_some() {
            let filter_by = self.filter_by.clone().unwrap_or_default();
            let filter_value = self.filter_value.clone().unwrap_or_default();
            if let Ok(value) = filter_value.parse::<i32>() {
                int_filter.insert(filter_by, value);
            }
            int_filter
        } else {
            int_filter
        }
    }

    pub fn match_fields<T, CO, DB>(&mut self) -> Result<(), APIErrorBadRequest>
    where
        T: DBObject<CO, DB>,
        // Traits needed for `DBObject`
        CO: Fillable + Filler<CO, DB> + Default + Debug + Clone,
        DB: PartialEq<CO> + Debug + Clone,
    {
        if self.group_by.is_some() {
            self.group_by = match T::match_field_by_opt(self.group_by.clone(), MatchBy::GroupBy) {
                Some(group_by) => Some(group_by),
                None => {
                    let message = format!(
                        "Group by field `{}` does not exists. Allowed fields are: {}",
                        self.group_by.as_ref().unwrap(),
                        T::match_field_by(MatchBy::OrderBy).join(", "),
                    );
                    error!("{}", message);
                    return Err(APIErrorBadRequest::from(message));
                }
            }
        }
        // Check if both `filter_by` and `filter_value` is set
        if (self.filter_by.is_some() && self.filter_value.is_none())
            || (self.filter_by.is_none() && self.filter_value.is_some())
        {
            return Err(APIErrorBadRequest::from(
                "Both `filter_by` and `filter_value` \
                have to be provided for the filter to work.",
            ));
        }
        if self.filter_by.is_some() {
            // If the field is a integer field, check if value is integer
            if T::match_field_by_opt(self.filter_by.clone(), MatchBy::IntFilterBy).is_some() {
                let filter_value = self.filter_value.clone().unwrap_or_default();
                match filter_value.parse::<i32>() {
                    Ok(_) => {}
                    Err(err) => match err.to_string().as_ref() {
                        "invalid digit found in string" => {
                            let message = format!(
                                "Filter error value `{}` is not an integer, \
                                please insert a number.",
                                filter_value
                            );
                            error!("{}", message);
                            return Err(APIErrorBadRequest::from(message));
                        }
                        _ => {
                            error!("Filter error: {}", err.to_string());
                            return Err(APIErrorBadRequest::from(Error::from(err)));
                        }
                    },
                }
            }
            self.filter_by = match T::match_field_by_opt(self.filter_by.clone(), MatchBy::FilterBy)
            {
                Some(filter_by) => Some(filter_by),
                None => {
                    let message = format!(
                        "Filter by field `{}` does not exists. Allowed fields are: {}",
                        self.filter_by.as_ref().unwrap(),
                        T::match_field_by(MatchBy::OrderBy).join(", "),
                    );
                    error!("{}", message);
                    return Err(APIErrorBadRequest::from(message));
                }
            }
        }
        Ok(())
    }
}