df_st_api 0.3.0-development-1

Starting an API server for the DF Storyteller project.
Documentation
#![forbid(unsafe_code)]
#![deny(clippy::all)]
#![feature(proc_macro_hygiene, decl_macro)]
#![doc(html_root_url = "https://docs.dfstoryteller.com/rust-docs/")]
#![doc(html_favicon_url = "https://docs.dfstoryteller.com/favicon/favicon-16x16.png")]
#![doc(html_logo_url = "https://docs.dfstoryteller.com/logo.svg")]

//! # DF Storyteller - API Documentation
//!
//! This crate includes all the documentation about the API setup and API calls.
//! For the objects them self look in the [`df_st_core`](df_st_core) crate.
//!
//!

pub mod api;
pub mod api_errors;
pub mod api_objects;
pub mod graphql;
pub mod item_count;
mod launch_errors;
pub mod pagination;
pub mod server_config;

use df_st_core::config::RootConfig;
use failure::Error;
use graphql::{MutationRoot, QueryRoot, Schema};
use launch_errors::handle_launch_errors;
#[allow(unused_imports)]
use log::{debug, error, info, trace, warn};
use rocket::http::{ContentType, Status};
use rocket::*;
use rocket_contrib::serve::StaticFiles;
use rocket_contrib::*;
use rust_embed::RustEmbed;
use serde::Serialize;
use server_config::{set_server_config, ServerInfo};
use std::ffi::OsStr;
use std::fs::File;
use std::io::{Cursor, Write};
use std::path::{Path, PathBuf};

pub use api::get_openapi_spec;

#[derive(RustEmbed)]
#[folder = "./pages/"]
struct StaticAssets;

#[derive(RustEmbed)]
#[folder = "./docs/"]
struct StaticDocs;

#[derive(RustEmbed)]
#[folder = "./serve-paintings/"]
struct StaticPaintings;

// Can be configured different ways:
// https://api.rocket.rs/v0.4/rocket_contrib/databases/index.html
#[database("df_st_database")]
pub struct DfStDatabase(df_st_db::DbConnection);

/// Main information page
#[get("/")]
fn index_page<'r>() -> response::Result<'r> {
    StaticAssets::get("index.html").map_or_else(
        || Err(Status::NotFound),
        |d| {
            response::Response::build()
                .header(ContentType::HTML)
                .sized_body(Cursor::new(d))
                .ok()
        },
    )
}

/// Serve other static file from pages folder.
#[get("/static/<file..>")]
fn static_file<'r>(file: PathBuf) -> response::Result<'r> {
    let filename = file
        .display()
        .to_string()
        // replace the Windows `\` with the unix `/` This effect the windows release build.
        .replace("\\", "/");
    StaticAssets::get(&filename).map_or_else(
        || Err(Status::NotFound),
        |d| {
            let ext = file
                .as_path()
                .extension()
                .and_then(OsStr::to_str)
                .ok_or_else(|| Status::new(400, "Could not get file extension"))?;
            let content_type = match ext {
                "map" => ContentType::JavaScript,
                _ => ContentType::from_extension(ext)
                    .ok_or_else(|| Status::new(400, "Could not get file content type"))?,
            };
            response::Response::build()
                .header(content_type)
                .sized_body(Cursor::new(d))
                .ok()
        },
    )
}

/// Server other static file from pages folder.
#[get("/docs/<file..>")]
fn static_docs<'r>(file: PathBuf) -> response::Result<'r> {
    let temp_filename = file
        .display()
        .to_string()
        // replace the Windows `\` with the unix `/` This effect the windows release build.
        .replace("\\", "/");
    let filename = match temp_filename.as_ref() {
        "swagger-ui" => "swagger-ui/index.html".to_string(),
        "rapidoc" => "rapidoc/index.html".to_string(),
        _ => temp_filename,
    };
    let file = PathBuf::from(&filename);
    StaticDocs::get(&filename).map_or_else(
        || Err(Status::NotFound),
        |d| {
            let ext = file
                .as_path()
                .extension()
                .and_then(OsStr::to_str)
                .ok_or_else(|| Status::new(400, "Could not get file extension"))?;
            let content_type = match ext {
                "map" => ContentType::JavaScript,
                _ => ContentType::from_extension(ext)
                    .ok_or_else(|| Status::new(400, "Could not get file content type"))?,
            };
            response::Response::build()
                .header(content_type)
                .sized_body(Cursor::new(d))
                .ok()
        },
    )
}

/// Write A Json object to a file
pub fn write_json_file<P: AsRef<std::path::Path>, C: Serialize>(
    filename: P,
    object: &C,
) -> Result<(), Error> {
    let mut file = File::create(filename)?;
    let json_string = serde_json::to_string_pretty(object)?;
    file.write_all(json_string.as_bytes())?;
    Ok(())
}

pub fn create_serve_paintings_folder() -> Result<(), Error> {
    let folder_name = "./serve-paintings";
    if !Path::new(folder_name).exists() {
        // Print folder name where it creates the serve-paintings folder
        info!(
            "Creating `serve-paintings` folder in: {}",
            std::env::current_dir()?
                .to_str()
                .unwrap_or("path contains special characters it could not display.")
        );
        // Create folders
        std::fs::create_dir(folder_name)?;
        std::fs::create_dir(format!("{}/timeline", folder_name))?;
        std::fs::create_dir(format!("{}/timeline/chartjs/", folder_name))?;
        // Create all files inside folder
        let file_list_copy = vec![
            "index.html",
            "paintings.json",
            "timeline/index.html",
            "timeline/chartjs/Chart.min.css",
            "timeline/chartjs/Chart.min.js",
        ];
        for filename in file_list_copy {
            let file_bytes = match StaticPaintings::get(filename) {
                Some(data) => data,
                None => {
                    panic!("Could not get file: {}", filename);
                }
            };
            let mut file = File::create(format!("{}/{}", folder_name, filename))?;
            file.write_all(&file_bytes)?;
        }
    }
    Ok(())
}

pub fn create_service(server_config: &RootConfig, world_id: u32) -> rocket::Rocket {
    // Panic if db mismatch
    df_st_db::check_db_and_config_match(&server_config);
    // CORS
    let cors = rocket_cors::CorsOptions::default().to_cors().unwrap();

    let server_info = ServerInfo::new(&server_config, "/api/", world_id);

    info!("Your `World_id` = `{}`", server_info.world_id);
    info!("Your `Base_url` = `{}`", server_info.base_url);

    // TODO create 'serve-paintings' folder and add the files to this folder that are in the repo.
    // Do this only if the folder does not already exist.

    rocket::custom(set_server_config(&server_config))
        .attach(DfStDatabase::fairing())
        .attach(cors)
        .register(catchers![
            api_errors::internal_error,
            api_errors::not_found,
            api_errors::no_content,
            api_errors::not_modified
        ])
        .manage(server_info)
        // Index and other static pages that should not be added as part of openapi docs
        // when adding more static pages see: https://github.com/pyros2097/rust-embed/blob/master/examples/rocket.rs
        // Add documentation static pages.
        .mount("/", routes![index_page, static_file, static_docs])
        // Allow users to put static files in the `serve-paintings` folder and the server will
        // serve them as static pages.
        .mount("/paintings", StaticFiles::from("./serve-paintings"))
        // Add RESTful api requests
        .mount("/api/", api::get_routes())
        // Add GraphQL Schema
        .manage(Schema::new(QueryRoot, MutationRoot))
        // Add GraphQL Routes
        .mount(
            "/graphql/",
            rocket::routes![
                graphql::graphiql,
                graphql::get_graphql_handler,
                graphql::post_graphql_handler,
                graphql::graphiql_playground,
            ],
        )
}

/// Start the Rocket API server that serves the RESTful API, GraphQL and documentation.
pub fn start_server(server_config: &RootConfig, world_id: u32) {
    info!("Starting server");

    match create_serve_paintings_folder() {
        Ok(_) => {}
        Err(err) => {
            error!("{}", err);
        }
    }

    let launch_error = create_service(server_config, world_id).launch();
    handle_launch_errors(launch_error);
}