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 okapi::openapi3::Responses;
use rocket::catch;
use rocket::http::{ContentType, Status};
use rocket::request::Request;
use rocket::response::{self, Responder, Response};
use rocket_contrib::json::Json;
use df_rocket_okapi::gen::OpenApiGenerator;
use df_rocket_okapi::response::OpenApiResponder;
use df_rocket_okapi::Result as OpenApiResult;
use serde::{Deserialize, Serialize};

fn add_204_error(responses: &mut Responses) {
    responses
        .responses
        .entry("204".to_owned())
        .or_insert_with(|| {
            let mut response = okapi::openapi3::Response::default();
            response.description = "\
            # [204 No Content](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/204)\n\
            This response is given when you request an item using a filter and there is no \
            item with the exact filter criteria.\n\n\
            In most cases you requested an `id` that does not exists in the database. \
            The body of this response will be empty.\
            "
            .to_owned();
            response.into()
        });
}

fn add_304_error(responses: &mut Responses) {
    responses
        .responses
        .entry("304".to_owned())
        .or_insert_with(|| {
            let mut response = okapi::openapi3::Response::default();
            response.description = "\
            # [304 Not Modified](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/304)\n\
            This response is given when the ETag of in the query parameters matches the \
            generated response by the server. This can then be used to enable caching of \
            responses.\n\n\
            If the ETags do not match it will give you the full response (with a 200 OK). \
            The response will give include the new ETag.\n\n\
            [More info can be found here]\
            (https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-None-Match).\
            "
            .to_owned();
            response.into()
        });
}

fn add_400_error(responses: &mut Responses) {
    responses
        .responses
        .entry("400".to_owned())
        .or_insert_with(|| {
            let mut response = okapi::openapi3::Response::default();
            response.description = "\
            # [400 Bad Request](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/400)\n\
            This request given is wrongly formatted or data asked could not be fulfilled.\n\n\
            This is most likely because of the `filter_by`, `order_by` or `group_by` parameters. \
            The properties you are asking for do not exist or does not allow filtering or ordering. \
            "
            .to_owned();
            response.into()
        });
}

fn add_404_error(responses: &mut Responses) {
    responses.responses.entry("404".to_owned())
        .or_insert_with(|| {
            let mut response = okapi::openapi3::Response::default();
            response.description = "\
            # [404 Not Found](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/404)\n\
            This response is given when you request a page that does not exists.\n\n\
            **Note:** This is not exactly a response by this endpoint. But might returned when \
            you wrongly input one or more of the path or query parameters. An example would be \
            that the server expects and `int32` and you have given it \"100m\", which is a `string` \
            because of the `m` character.\n\n\
            So when you get this error and you expect a result. Check all the types of the parameters. \
            ".to_owned();
            response.into()
        });
}

fn add_500_error(responses: &mut Responses) {
    responses
        .responses
        .entry("500".to_owned())
        .or_insert_with(|| {
            let mut response = okapi::openapi3::Response::default();
            response.description = format!(
                "\
                # [500 Internal Server Error]\
                (https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/500)\n\
                This response is given when the server has an internal error that it could not \
                recover from.\n\n\
                If you get this response please report this as an [issue here]({}).\
                ",
                df_st_core::git_issue::link_to_issue_template("bug")
            );
            response.into()
        });
}

#[derive(Serialize, Deserialize)]
pub struct APIErrorResponse {
    errors: Vec<APIError>,
}

#[derive(Serialize, Deserialize)]
pub struct APIError {
    pub message: String,
    pub issue_link: Option<String>,
    pub code: u16,
}

#[derive(Serialize, Deserialize)]
pub struct APIMessageResponse {
    message: APIMessage,
}

#[derive(Serialize, Deserialize)]
pub struct APIMessage {
    pub message: String,
    pub code: u16,
}

#[derive(Debug, Default)]
pub struct APIErrorBadRequest {
    pub err: Option<Error>,
    pub message: Option<String>,
}

impl From<Error> for APIErrorBadRequest {
    fn from(error: Error) -> Self {
        APIErrorBadRequest {
            err: Some(error),
            message: None,
        }
    }
}

impl From<String> for APIErrorBadRequest {
    fn from(message: String) -> Self {
        APIErrorBadRequest {
            err: None,
            message: Some(message),
        }
    }
}

impl From<&str> for APIErrorBadRequest {
    fn from(message: &str) -> Self {
        APIErrorBadRequest {
            err: None,
            message: Some(message.to_owned()),
        }
    }
}

#[derive(Debug, Default)]
pub struct APIErrorNoContent {
    pub err: Option<Error>,
    pub bad_request: bool,
    pub message: Option<String>,
}

impl APIErrorNoContent {
    pub fn new() -> Self {
        APIErrorNoContent::default()
    }
}

impl From<Error> for APIErrorNoContent {
    fn from(error: Error) -> Self {
        APIErrorNoContent {
            err: Some(error),
            bad_request: false,
            message: None,
        }
    }
}

impl From<APIErrorBadRequest> for APIErrorNoContent {
    fn from(error: APIErrorBadRequest) -> Self {
        APIErrorNoContent {
            err: error.err,
            bad_request: true,
            message: error.message,
        }
    }
}

impl<'r> Responder<'r> for APIErrorNoContent {
    fn respond_to(self, _: &Request) -> response::Result<'r> {
        let mut error_response = APIErrorResponse { errors: vec![] };
        if let Some(error) = self.err {
            error!("{}", error);
            error_response.errors.push(APIError {
                message: error.to_string(),
                issue_link: None,
                code: match self.bad_request {
                    true => 400,
                    false => 500,
                },
            });
        }

        if let Some(message) = self.message {
            error_response.errors.push(APIError {
                message,
                issue_link: None,
                code: match self.bad_request {
                    true => 400,
                    false => 500,
                },
            });
        }

        let mut response = Response::build();
        response.status(if error_response.errors.is_empty() {
            Status::NoContent
        } else if self.bad_request {
            Status::BadRequest
        } else {
            Status::InternalServerError
        });

        if !error_response.errors.is_empty() {
            response.header(ContentType::JSON);
            response.sized_body(std::io::Cursor::new(
                serde_json::to_string(&error_response).unwrap(),
            ));
        }
        response.ok()
    }
}

impl OpenApiResponder<'_> for APIErrorNoContent {
    fn responses(_: &mut OpenApiGenerator) -> OpenApiResult<Responses> {
        let mut responses = Responses::default();
        add_204_error(&mut responses);
        add_400_error(&mut responses);
        add_404_error(&mut responses);
        add_500_error(&mut responses);
        Ok(responses)
    }
}

#[derive(Debug, Default)]
pub struct APIErrorNotModified {
    pub err: Option<Error>,
    pub bad_request: bool,
    pub message: Option<String>,
}

impl APIErrorNotModified {
    pub fn new() -> Self {
        APIErrorNotModified::default()
    }
}

impl From<Error> for APIErrorNotModified {
    fn from(error: Error) -> Self {
        APIErrorNotModified {
            err: Some(error),
            bad_request: false,
            message: None,
        }
    }
}

impl From<APIErrorBadRequest> for APIErrorNotModified {
    fn from(error: APIErrorBadRequest) -> Self {
        APIErrorNotModified {
            err: error.err,
            bad_request: true,
            message: error.message,
        }
    }
}

impl<'r> Responder<'r> for APIErrorNotModified {
    fn respond_to(self, _: &Request) -> response::Result<'r> {
        let mut error_response = APIErrorResponse { errors: vec![] };
        if let Some(error) = self.err {
            error!("{}", error);
            error_response.errors.push(APIError {
                message: error.to_string(),
                issue_link: None,
                code: match self.bad_request {
                    true => 400,
                    false => 500,
                },
            });
        }

        if let Some(message) = self.message {
            error_response.errors.push(APIError {
                message,
                issue_link: None,
                code: match self.bad_request {
                    true => 400,
                    false => 500,
                },
            });
        }

        let mut response = Response::build();
        response.status(if error_response.errors.is_empty() {
            Status::NotModified
        } else if self.bad_request {
            Status::BadRequest
        } else {
            Status::InternalServerError
        });

        if !error_response.errors.is_empty() {
            response.header(ContentType::JSON);
            response.sized_body(std::io::Cursor::new(
                serde_json::to_string(&error_response).unwrap(),
            ));
        }
        response.ok()
    }
}

impl OpenApiResponder<'_> for APIErrorNotModified {
    fn responses(_: &mut OpenApiGenerator) -> OpenApiResult<Responses> {
        let mut responses = Responses::default();
        add_304_error(&mut responses);
        add_400_error(&mut responses);
        add_404_error(&mut responses);
        add_500_error(&mut responses);
        Ok(responses)
    }
}

#[catch(500)]
pub fn internal_error() -> Json<APIErrorResponse> {
    Json(APIErrorResponse {
        errors: vec![APIError {
            message: "I don't know what happened there... \
                Did someone leave the caverns open?\n\
                Please open a new issue for this: \
                https://gitlab.com/df_storyteller/df-storyteller/issues/new"
                .to_owned(),
            issue_link: Some(
                "https://gitlab.com/df_storyteller/df-storyteller/issues/new".to_owned(),
            ),
            code: 500,
        }],
    })
}

#[catch(400)]
pub fn bad_request(req: &Request) -> Json<APIErrorResponse> {
    Json(APIErrorResponse {
        errors: vec![APIError {
            message: format!("Bad request: URL `{}`.", req.uri()),
            issue_link: None,
            code: 400,
        }],
    })
}

#[catch(404)]
pub fn not_found(req: &Request) -> Json<APIErrorResponse> {
    Json(APIErrorResponse {
        errors: vec![APIError {
            message: format!("This requested URL `{}` was not found.", req.uri()),
            issue_link: None,
            code: 404,
        }],
    })
}

#[catch(204)]
pub fn no_content(req: &Request) -> Json<APIMessageResponse> {
    Json(APIMessageResponse {
        message: APIMessage {
            message: format!("No Content found here `{}`.", req.uri()),
            code: 204,
        },
    })
}

#[catch(304)]
pub fn not_modified(_req: &Request) -> Json<APIMessageResponse> {
    Json(APIMessageResponse {
        message: APIMessage {
            message: "Content has not been modified.".to_owned(),
            code: 304,
        },
    })
}