robotrt-cli 0.1.0-beta.2

RobotRT modular robotics runtime and middleware components.
use serde_json::Value;

use crate::gateway::{
    STATUS_OP_GATEWAY_AUDIT_LIST, STATUS_OP_GATEWAY_OBSERVE, STATUS_OP_GATEWAY_POLICY_GET,
    STATUS_OP_GATEWAY_POLICY_SET, STATUS_OP_GATEWAY_SHUTDOWN, STATUS_SERVICE_NAME,
    StatusServiceResponse, build_op_payload_request, build_request, make_udp_service_client,
    next_request_id, validate_response,
};
use crate::helpers::{
    has_flag, option_value, parse_u64_option, resolve_runtime_endpoint_optional,
};

use super::policy::parse_gateway_policy_set_payload;

use super::DEFAULT_STATUS_BIND_ADDR;

pub(super) fn gateway_observe(args: &[String]) -> Result<(), String> {
    let json = has_flag(args, "--json");
    let audit_limit = parse_u64_option(args, "--audit-limit", 20)?;
    let payload = if option_value(args, "--audit-limit").is_some() {
        Some(serde_json::json!({"audit_limit": audit_limit}))
    } else {
        None
    };

    let response = call_gateway_op(args, STATUS_OP_GATEWAY_OBSERVE, payload)?;
    let result = response
        .op_result
        .ok_or_else(|| String::from("gateway observe response missing op_result"))?;

    if json {
        let body = serde_json::json!({
            "api_version": "robotrt.gateway.observe.v1",
            "kind": "gateway_observe",
            "source": response_source(args)?,
            "result": result,
        });
        println!(
            "{}",
            serde_json::to_string_pretty(&body)
                .map_err(|err| format!("serialize gateway observe output failed: {err}"))?
        );
    } else {
        println!("RobotRT Gateway Observe");
        println!("source: {}", response_source_label(args)?);
        println!(
            "policy: {}",
            serde_json::to_string(&result["policy"]).unwrap_or_else(|_| String::from("{}"))
        );
        println!(
            "sessions: topic_streams={} action_goals={} mission_queues={}",
            result["sessions"]["topic_streams"],
            result["sessions"]["action_goals"],
            result["sessions"]["mission_queues"],
        );
    }

    Ok(())
}

pub(super) fn gateway_policy_get(args: &[String]) -> Result<(), String> {
    let json = has_flag(args, "--json");
    let response = call_gateway_op(args, STATUS_OP_GATEWAY_POLICY_GET, None)?;
    let result = response
        .op_result
        .ok_or_else(|| String::from("gateway policy get response missing op_result"))?;

    if json {
        let body = serde_json::json!({
            "api_version": "robotrt.gateway.policy.v1",
            "kind": "gateway_policy_get",
            "source": response_source(args)?,
            "result": result,
        });
        println!(
            "{}",
            serde_json::to_string_pretty(&body)
                .map_err(|err| format!("serialize gateway policy get output failed: {err}"))?
        );
    } else {
        println!("RobotRT Gateway Policy");
        println!("source: {}", response_source_label(args)?);
        println!(
            "policy: {}",
            serde_json::to_string(&result["policy"]).unwrap_or_else(|_| String::from("{}"))
        );
    }

    Ok(())
}

pub(super) fn gateway_policy_set(args: &[String]) -> Result<(), String> {
    let json = has_flag(args, "--json");
    let payload = parse_gateway_policy_set_payload(args)?;

    if payload.is_empty() {
        return Err(String::from(
            "gateway policy set requires at least one override option",
        ));
    }

    let response = call_gateway_op(
        args,
        STATUS_OP_GATEWAY_POLICY_SET,
        Some(Value::Object(payload)),
    )?;
    let result = response
        .op_result
        .ok_or_else(|| String::from("gateway policy set response missing op_result"))?;

    if json {
        let body = serde_json::json!({
            "api_version": "robotrt.gateway.policy.v1",
            "kind": "gateway_policy_set",
            "source": response_source(args)?,
            "result": result,
        });
        println!(
            "{}",
            serde_json::to_string_pretty(&body)
                .map_err(|err| format!("serialize gateway policy set output failed: {err}"))?
        );
    } else {
        println!("RobotRT Gateway Policy Set");
        println!("source: {}", response_source_label(args)?);
        println!("receipt_id: {}", result["receipt_id"]);
        println!(
            "policy: {}",
            serde_json::to_string(&result["policy"]).unwrap_or_else(|_| String::from("{}"))
        );
    }

    Ok(())
}

pub(super) fn gateway_audit_list(args: &[String]) -> Result<(), String> {
    let json = has_flag(args, "--json");
    let limit = parse_u64_option(args, "--limit", 20)?;
    let mut payload = serde_json::json!({"limit": limit});
    if let Some(category) = option_value(args, "--category") {
        payload["category"] = Value::String(category);
    }

    let response = call_gateway_op(args, STATUS_OP_GATEWAY_AUDIT_LIST, Some(payload))?;
    let result = response
        .op_result
        .ok_or_else(|| String::from("gateway audit list response missing op_result"))?;

    if json {
        let body = serde_json::json!({
            "api_version": "robotrt.gateway.audit.v1",
            "kind": "gateway_audit_list",
            "source": response_source(args)?,
            "result": result,
        });
        println!(
            "{}",
            serde_json::to_string_pretty(&body)
                .map_err(|err| format!("serialize gateway audit list output failed: {err}"))?
        );
    } else {
        println!("RobotRT Gateway Audit");
        println!("source: {}", response_source_label(args)?);
        println!("entries: {}", result["entries"].as_array().map_or(0, Vec::len));
    }

    Ok(())
}

pub(super) fn gateway_shutdown(args: &[String]) -> Result<(), String> {
    let json = has_flag(args, "--json");
    let response = call_gateway_op(args, STATUS_OP_GATEWAY_SHUTDOWN, None)?;
    let result = response
        .op_result
        .ok_or_else(|| String::from("gateway shutdown response missing op_result"))?;

    if json {
        let body = serde_json::json!({
            "api_version": "robotrt.gateway.shutdown.v1",
            "kind": "gateway_shutdown",
            "source": response_source(args)?,
            "result": result,
        });
        println!(
            "{}",
            serde_json::to_string_pretty(&body)
                .map_err(|err| format!("serialize gateway shutdown output failed: {err}"))?
        );
    } else {
        println!("RobotRT Gateway Shutdown");
        println!("source: {}", response_source_label(args)?);
        println!("accepted: {}", result["accepted"]);
    }

    Ok(())
}

fn call_gateway_op(
    args: &[String],
    op: &str,
    payload: Option<Value>,
) -> Result<StatusServiceResponse, String> {
    let endpoint = resolve_runtime_endpoint_optional(args, DEFAULT_STATUS_BIND_ADDR)?
        .ok_or_else(|| String::from("missing gateway endpoint"))?;
    let timeout_ms = parse_u64_option(args, "--timeout-ms", 1000)?;
    let client = make_udp_service_client(endpoint.clone(), timeout_ms)?;

    let request_id = next_request_id();
    let request = payload
        .map(|value| build_op_payload_request(op, value))
        .unwrap_or_else(|| build_request(op));
    let response: StatusServiceResponse = client
        .call_json(STATUS_SERVICE_NAME, request_id, &request)
        .map_err(|err| format!("gateway op={op} query to {endpoint} failed: {err}"))?;

    validate_response(&response, request_id, op)?;
    Ok(response)
}

fn response_source(args: &[String]) -> Result<Value, String> {
    let endpoint = resolve_runtime_endpoint_optional(args, DEFAULT_STATUS_BIND_ADDR)?
        .ok_or_else(|| String::from("missing gateway endpoint"))?;
    let timeout_ms = parse_u64_option(args, "--timeout-ms", 1000)?;
    Ok(serde_json::json!({
        "mode": "remote_service",
        "service": STATUS_SERVICE_NAME,
        "endpoint": endpoint,
        "timeout_ms": timeout_ms,
    }))
}

fn response_source_label(args: &[String]) -> Result<String, String> {
    let endpoint = resolve_runtime_endpoint_optional(args, DEFAULT_STATUS_BIND_ADDR)?
        .ok_or_else(|| String::from("missing gateway endpoint"))?;
    Ok(format!("remote:{endpoint}"))
}