index : license-api-rs.git

ascending towards madness

use async_std::{
    fs,
    path::{Path, PathBuf},
};
use data::LicenseDatabase;
use tide::{http::mime, log, Response, Result, Server, StatusCode};

use crate::{
    config::{RunningConfig, ServerConfig},
    data::Queryable,
    messages::ErrorMsg,
};

mod config;
mod data;
mod messages;

#[async_std::main]
async fn main() -> Result<()> {
    log::start();

    match <ServerConfig as jealousy::FromEnv>::from_env() {
        Ok(config) => {
            // TODO: fix the borrow issue here
            let run_config = RunningConfig {
                config: config.clone(),
                database: LicenseDatabase::load(config.database.clone()).unwrap_or_default(),
            };

            let mut app = tide::with_state(run_config);

            create_routes(&mut app);

            app.listen(format!("{}:{}", config.server_ip, config.server_port))
                .await?;
        }
        Err(_) => (), // jealousy prints its own error message so this is safe to ignore...i think
    };

    Ok(())
}

fn create_routes(server: &mut Server<RunningConfig>) {
    // TODO: figure out how to not need clone
    let url = server.clone().state().config.base_url.clone();
    let mut base_route = server.at(&url);

    // BASE_URL/<license>
    base_route.at(":license").get(get_requested_license);

    /*
    BASE_URL/list
    BASE_URL/full
    BASE_URL/list/
    BASE_URL/full/
    */
    base_route.at("list").get(get_license_list);
    base_route.at("list/full").get(get_detailed_license_list);

    // nyaaaaa whyyyy https://github.com/http-rs/tide/issues/205
    let mut fallback_routes = base_route.at("");
    fallback_routes.get(get_welcome_message);
    fallback_routes.at("list/").get(get_license_list);
    fallback_routes
        .at("list/full/")
        .get(get_detailed_license_list);

    fallback_routes.at("list/*").get(get_fallback_response);
    fallback_routes.at("list/full/*").get(get_fallback_response);
}

fn build_response(status: StatusCode, data: String) -> Response {
    // this is a plain text only service
    let response = Response::builder(status)
        .body(format!("{}\n", data))
        .content_type(mime::PLAIN)
        .build();

    response
}

async fn get_welcome_message(req: tide::Request<RunningConfig>) -> tide::Result<Response> {
    match &req.state().config.welcome_message {
        Some(file) => match async_std::fs::read_to_string(file).await {
            Ok(data) => return Ok(build_response(StatusCode::Ok, data)),
            Err(_) => {
                log::error!("Unable to read welcome message file.");
                return Ok(build_response(StatusCode::Ok, build_welcome_message(req)));
            }
        },
        None => return Ok(build_response(StatusCode::Ok, build_welcome_message(req))),
    }
}

fn build_welcome_message(req: tide::Request<RunningConfig>) -> String {
    // TODO: make this better
    let base_url = req.state().config.base_url.clone();
    let header = String::from("Available endpoints:");
    let base = format!("{}", base_url);
    let list = format!("{}list", base_url);
    let all = format!("{}list/full", base_url);
    let license = format!("{}<license>", base_url);
    format!(
        "{}\n  {}\n  {}\n  {}\n  {}",
        header, base, list, all, license
    )
}

async fn get_fallback_response(req: tide::Request<RunningConfig>) -> tide::Result<Response> {
    Ok(error_response(req.url().path()))
}

async fn get_license_list(req: tide::Request<RunningConfig>) -> tide::Result<Response> {
    // TODO: env var the chunks
    match req.state().database.get_licenses(5) {
        Some(licenses) => Ok(build_response(StatusCode::Ok, format!("{}", licenses))),
        None => Ok(build_response(
            StatusCode::NotFound,
            ErrorMsg::NoLicensesAvailable.to_string(),
        )),
    }
}

async fn get_detailed_license_list(req: tide::Request<RunningConfig>) -> tide::Result<Response> {
    match req.state().database.get_detailed_licenses() {
        Some(licenses) => Ok(build_response(StatusCode::Ok, format!("{}", licenses))),
        None => Ok(build_response(
            StatusCode::NotFound,
            ErrorMsg::NoLicensesAvailable.to_string(),
        )),
    }
}

async fn get_requested_license(req: tide::Request<RunningConfig>) -> tide::Result<Response> {
    // remove prefix from requested route
    // TODO: make this better
    let request = req.url().path().strip_prefix("/").unwrap_or("");

    // TODO: break this up
    let response = match req.state().database.get_license(request) {
        Some(license) => {
            // is there a better way to do this?
            let source_dir = PathBuf::from(req.state().config.database.clone());
            let source_dir = source_dir.parent().unwrap_or(Path::new(""));

            let mut license_file_path = PathBuf::from(source_dir);
            license_file_path.push(license.filename);

            match fs::read_to_string(&license_file_path).await {
                Ok(contents) => {
                    build_response(StatusCode::Accepted, contents.trim_end().to_string())
                }
                Err(_) => {
                    log::error!(
                        "{} '{}'.",
                        ErrorMsg::LicenseReadError,
                        license_file_path.to_string_lossy().to_string()
                    );

                    error_response(request)
                }
            }
        }
        None => {
            log::error!("{} '{}'", ErrorMsg::InvalidLicense, request);
            error_response(request)
        }
    };

    Ok(response)
}

fn error_response(_req: &str) -> Response {
    build_response(
        StatusCode::NotFound,
        format!("{}", ErrorMsg::InvalidLicenseRequest),
    )
}