df_st_api 0.3.0-development-1

Starting an API server for the DF Storyteller project.
Documentation
use failure::Error;
#[allow(unused_imports)]
use log::{debug, error, info, trace, warn};
use rocket::request::{FromQuery, Query};
use df_rocket_okapi::{
    gen::OpenApiGenerator, request::get_nested_query_parameters, request::OpenApiFromQuery,
};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::fmt;
use std::fmt::Debug;

/// A Query Guard for pagination of lists of items.
#[derive(Serialize, Deserialize, Clone, Debug, Default, JsonSchema)]
pub struct ApiPagination {
    /// Number of the page you want to request. (starting from page 0)
    pub page: Option<u32>,
    /// Number of maximum items per page. (at least 1)
    pub per_page: Option<u32>,
    /// Tag to identify the version of the page requested.
    /// [More info here](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/ETag).
    pub etag: Option<String>,
    /// Force the page to be ordered. "asc" = A-Z, "desc" = Z-A.
    pub order: Option<OrderTypes>,
    /// What field should the result be ordered by. Example: "id" or "name".
    /// If the field does not exist it will order by the default field. (usually "id")
    pub order_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".
    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.
    pub filter_value: Option<String>,
    /// Don't load nested items. This will speed up page loads but will not fill in all the data.
    /// Fields that can be used for ordering are usually the only field that are returned.
    /// - `Some(false)` If value is not set, or set to "false" or "0" (same result default)
    /// - `Some(true)` If value is set to ""(empty), "true" or "1"
    /// - `None` If any other value this will add all the data to the response (default)
    pub minimal_data: Option<bool>,
}

/// Possible ways of sorting data
#[derive(Serialize, Deserialize, Clone, Debug, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum OrderTypes {
    Asc,
    Desc,
}

/// From DB order to Api order.
impl From<df_st_db::OrderTypes> for OrderTypes {
    fn from(order: df_st_db::OrderTypes) -> Self {
        match order {
            df_st_db::OrderTypes::Asc => OrderTypes::Asc,
            df_st_db::OrderTypes::Desc => OrderTypes::Desc,
        }
    }
}

/// From Api order to DB order.
impl From<OrderTypes> for df_st_db::OrderTypes {
    fn from(order: OrderTypes) -> Self {
        match order {
            OrderTypes::Asc => df_st_db::OrderTypes::Asc,
            OrderTypes::Desc => df_st_db::OrderTypes::Desc,
        }
    }
}

impl fmt::Display for OrderTypes {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match *self {
            OrderTypes::Asc => write!(f, "asc"),
            OrderTypes::Desc => write!(f, "desc"),
        }
    }
}

/// Parse incoming query data and store in `ApiPagination`
impl<'q> FromQuery<'q> for ApiPagination {
    type Error = Error; //TODO Custom error

    fn from_query(query: Query<'q>) -> Result<Self, Self::Error> {
        let mut api_page_request = ApiPagination::default();
        for key in query {
            match key.key.url_decode()?.as_ref() {
                "page" => api_page_request.page = Some(key.value.parse::<u32>()?),
                "per_page" => api_page_request.per_page = Some(key.value.parse::<u32>()?),
                "etag" => api_page_request.etag = Some(key.value.url_decode_lossy()),
                "order" => {
                    api_page_request.order = Some(match key.value.url_decode_lossy().as_ref() {
                        "desc" => OrderTypes::Desc,
                        _ => OrderTypes::Asc,
                    })
                }
                "order_by" => api_page_request.order_by = Some(key.value.url_decode_lossy()),
                "filter_by" => api_page_request.filter_by = Some(key.value.url_decode_lossy()),
                "filter_value" => {
                    api_page_request.filter_value = Some(key.value.url_decode_lossy())
                }
                "minimal_data" => {
                    api_page_request.minimal_data = match key.value.url_decode_lossy().as_ref() {
                        "true" | "1" | "" => Some(true),
                        "false" | "0" => Some(false),
                        x => {
                            error!(
                                "Field `minimal_data` uses unknown value `{}`. \
                                Please use `true`, `1`, `false` or `0`.",
                                x
                            );
                            None
                        }
                    }
                }
                _ => {}
            }
        }
        Ok(api_page_request)
    }
}

/// Add support for OpenApi generator
impl<'r> OpenApiFromQuery<'r> for ApiPagination {
    fn query_multi_parameter(
        gen: &mut OpenApiGenerator,
        name: String,
        required: bool,
    ) -> df_rocket_okapi::Result<Vec<okapi::openapi3::Parameter>> {
        Ok(get_nested_query_parameters::<ApiPagination>(
            gen, name, required,
        ))
    }
}