oi-pkg-visualizer 1.0.0

Implementation of OpenIndiana package visualizer website and server
use std::{
    env::args,
    net::SocketAddr
};
use axum::{
    extract::State,
    routing::{get, post},
    http::StatusCode,
    Json, Router, Server
};
use serde::{Deserialize, Serialize};
use tokio::signal;
use tower_http::cors::CorsLayer;
use tracing_subscriber::fmt::init;
use oi_pkg_checker_core::{
    Components, DependTypes, DependencyTypes, Package
};

/// Represents package name
#[derive(Deserialize, Debug)]
struct PackageName(String);

/// Represents nodes(package_name, depend_type(Runtime/Build/Test/SystemBuild/SystemTest/None), package_type(obsoleted/partly-obsoleted/renamed/none))
#[derive(Serialize)]
struct Nodes(Vec<(String, String, String)>);

#[tokio::main]
async fn main() {
    init();

    let args: Vec<String> = args().collect();

    if args.len() != 3 {
        panic!("Usage: {} <listening_addr_and_port> <data_path>", args[0]);
    }

    let components = Components::deserialize(&args[2]);

    let addr = match args[1].parse::<SocketAddr>() {
        Ok(socket_addr) => socket_addr,
        Err(e) => {
            panic!("Failed to parse SocketAddr: {}", e);
        }
    };

    let app = Router::new()
        .route("/", get(discover))
        .route("/nodes", post(nodes))
        .with_state(components)
        .layer(CorsLayer::permissive());

    tracing::debug!("listening on {}", addr);
    Server::bind(&addr)
        .serve(app.into_make_service())
        .with_graceful_shutdown(shutdown_signal())
        .await
        .unwrap();
}

/// Basic discover handler for testing
async fn discover() -> &'static str {
    tracing::debug!("discovered");
    "discovered!"
}

/// Handler for returning dependencies(nodes) of package
async fn nodes(
    State(components): State<Components>,
    Json(package): Json<PackageName>,
) -> (StatusCode, Json<Nodes>) {
    let nodes: &mut Vec<(String, String, String)> = &mut Vec::new();

    tracing::debug!("got request on package: {:?}", package);

    let mut obsoleted_packages: Vec<String> = Vec::new();
    for fmri in components.get_obsoleted_ref().get_ref() {
        obsoleted_packages.push(fmri.get_package_name_as_ref_string().clone())
    }

    let mut found = false;
    for component in components.get_ref() {
        for package_versions in component.get_versions_ref() {
            if package_versions.fmri_ref().get_package_name_as_ref_string() == &package.0 {
                let package = package_versions.get_packages_ref().last().unwrap();

                get_nodes_from_dependencies(nodes, &obsoleted_packages, DependencyTypes::Runtime, package);
                get_nodes_from_dependencies(nodes, &obsoleted_packages, DependencyTypes::Build, package);
                get_nodes_from_dependencies(nodes, &obsoleted_packages, DependencyTypes::Test, package);
                get_nodes_from_dependencies(nodes, &obsoleted_packages, DependencyTypes::SystemBuild, package);
                get_nodes_from_dependencies(nodes, &obsoleted_packages, DependencyTypes::SystemTest, package);

                found = true;
                break;
            }
        }

        if found { break; }
    }

    if !found {
        tracing::error!("package: {:?} not found", package);
        return (StatusCode::NOT_FOUND, Json(Nodes(nodes.clone())));
    }

    tracing::debug!("sending on package: {:?} packages: {:?}", package, nodes);
    (StatusCode::OK, Json(Nodes(nodes.clone())))
}

/// Simply extracts package names from dependencies
fn get_nodes_from_dependencies(nodes: &mut Vec<(String, String, String)>, obsoleted_packages: &Vec<String>, dependency_type: DependencyTypes, package: &Package) {
    let (d_type, dependencies) = match dependency_type {
        DependencyTypes::Runtime => ("runtime".to_owned(), package.get_runtime_dependencies()),
        DependencyTypes::Build => ("build".to_owned(), package.get_build_dependencies()),
        DependencyTypes::Test => ("test".to_owned(), package.get_test_dependencies()),
        DependencyTypes::SystemBuild => ("system-build".to_owned(), package.get_system_build_dependencies()),
        DependencyTypes::SystemTest => ("system-test".to_owned(), package.get_system_test_dependencies()),
        DependencyTypes::None => panic!()
    };

    let node_type_closure = |
        package_name: &String,
        package: &Package,
        obsoleted_packages: &[String]
    | -> String {
        if package.is_obsolete() {
            return "partly-obsoleted".to_owned();
        } else if package.is_renamed() {
            return "renamed".to_owned();
        } else {
            for obsoleted_package in obsoleted_packages {
                if obsoleted_package == package_name {
                    return "obsoleted".to_owned();
                }
            }
        }

        return "none".to_owned();
    };

    for dependency in dependencies {
        let package_name = match dependency.get_ref() {
            DependTypes::Require(fmri) => fmri.clone().get_package_name_as_string(),
            DependTypes::Optional(fmri) => fmri.clone().get_package_name_as_string(),
            DependTypes::Incorporate(fmri) => fmri.clone().get_package_name_as_string(),
            DependTypes::RequireAny(fmri_list) => {
                for fmri in fmri_list.get_ref() {
                    let package_name = fmri.clone().get_package_name_as_ref_string().clone();
                    nodes.push((
                        package_name.clone(),
                        d_type.clone(),
                        node_type_closure(&package_name, package, obsoleted_packages)
                    ));
                }
                continue;
            }
            DependTypes::Conditional(fmri, _) => fmri.clone().get_package_name_as_string(),
            DependTypes::Group(fmri) => fmri.clone().get_package_name_as_string(),
            _ => unimplemented!()
        };

        nodes.push((
            package_name.clone(),
            d_type.clone(),
            node_type_closure(&package_name, package, obsoleted_packages)
        ));
    }
}

///  For graceful shutdown
async fn shutdown_signal() {
    let ctrl_c = async {
        signal::ctrl_c().await.expect("failed to install Ctrl+C handler");
    };

    let terminate = async {
        signal::unix::signal(signal::unix::SignalKind::terminate())
            .expect("failed to install signal handler")
            .recv()
            .await;
    };

    tokio::select! {
        _ = ctrl_c => {},
        _ = terminate => {},
    }

    tracing::debug!("signal received, starting graceful shutdown");
}