greentic-start-dev 1.1.26572933845

Greentic lifecycle runner for start/restart/stop orchestration
Documentation
use anyhow::Context;
use serde_json::{Value as JsonValue, json};

use crate::domains::Domain;
use crate::messaging_dto::{
    EncodeInV1, ProviderPayloadV1, RenderPlanInV1, SendPayloadInV1, TenantHint,
};
use crate::runner_host::{DemoRunnerHost, FlowOutcome, OperatorContext};

pub fn build_render_plan_input(message: JsonValue) -> RenderPlanInV1 {
    RenderPlanInV1 { v: 1, message }
}

pub fn build_encode_input(message: JsonValue, plan: JsonValue) -> EncodeInV1 {
    EncodeInV1 {
        v: 1,
        message,
        plan,
    }
}

pub fn build_send_payload(
    payload: ProviderPayloadV1,
    provider_type: impl Into<String>,
    tenant: impl Into<String>,
    team: Option<String>,
) -> SendPayloadInV1 {
    SendPayloadInV1 {
        v: 1,
        provider_type: provider_type.into(),
        payload,
        tenant: TenantHint {
            tenant: tenant.into(),
            team,
            user: None,
            correlation_id: None,
        },
        reply_scope: None,
    }
}

pub fn render_plan(
    runner_host: &DemoRunnerHost,
    ctx: &OperatorContext,
    provider: &str,
    message: JsonValue,
) -> anyhow::Result<JsonValue> {
    let input = build_render_plan_input(message);
    let outcome = invoke_flow(
        runner_host,
        ctx,
        provider,
        "render_plan",
        serde_json::to_value(&input)?,
    )?;
    let validated = ensure_success(&outcome, provider, "render_plan")?;
    Ok(validated.output.clone().unwrap_or_else(|| json!({})))
}

pub fn encode_payload(
    runner_host: &DemoRunnerHost,
    ctx: &OperatorContext,
    provider: &str,
    message: JsonValue,
    plan: JsonValue,
) -> anyhow::Result<ProviderPayloadV1> {
    let input = build_encode_input(message, plan);
    let outcome = invoke_flow(
        runner_host,
        ctx,
        provider,
        "encode",
        serde_json::to_value(&input)?,
    )?;
    let validated = ensure_success(&outcome, provider, "encode")?;
    let value = validated.output.clone().unwrap_or_else(|| json!({}));
    let payload_value = value.get("payload").cloned().unwrap_or(value);
    serde_json::from_value(payload_value)
        .context("failed to parse ProviderPayloadV1 from encode output")
}

fn invoke_flow(
    runner_host: &DemoRunnerHost,
    ctx: &OperatorContext,
    provider: &str,
    op: &str,
    payload: JsonValue,
) -> anyhow::Result<FlowOutcome> {
    let input_bytes = serde_json::to_vec(&payload)?;
    runner_host.invoke_provider_op(Domain::Messaging, provider, op, &input_bytes, ctx)
}

fn ensure_success<'a>(
    outcome: &'a FlowOutcome,
    provider: &str,
    op: &str,
) -> anyhow::Result<&'a FlowOutcome> {
    if outcome.success {
        Ok(outcome)
    } else {
        Err(anyhow::anyhow!(
            "{provider}.{op} failed: {}",
            outcome
                .error
                .clone()
                .unwrap_or_else(|| "unknown error".to_string())
        ))
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn build_inputs_wrap_message_plan_and_tenant_hints() {
        let render = build_render_plan_input(json!({"text": "hello"}));
        assert_eq!(render.v, 1);
        assert_eq!(render.message["text"], "hello");

        let encode = build_encode_input(json!({"text": "hello"}), json!({"steps": []}));
        assert_eq!(encode.v, 1);
        assert_eq!(encode.plan["steps"], json!([]));

        let send = build_send_payload(
            ProviderPayloadV1 {
                content_type: "application/json".to_string(),
                body_b64: "e30=".to_string(),
                metadata_json: Some("{}".to_string()),
                metadata: None,
            },
            "messaging-slack",
            "demo",
            Some("ops".to_string()),
        );
        assert_eq!(send.v, 1);
        assert_eq!(send.provider_type, "messaging-slack");
        assert_eq!(send.tenant.tenant, "demo");
        assert_eq!(send.tenant.team.as_deref(), Some("ops"));
        assert!(send.reply_scope.is_none());
    }

    #[test]
    fn ensure_success_returns_errors_with_provider_and_op_context() {
        let ok = FlowOutcome {
            success: true,
            output: Some(
                json!({"payload": {"body_b64": "e30=", "content_type": "application/json"}}),
            ),
            raw: None,
            error: None,
            mode: crate::runner_host::RunnerExecutionMode::Exec,
        };
        assert!(ensure_success(&ok, "provider-a", "encode").is_ok());

        let failed = FlowOutcome {
            success: false,
            output: None,
            raw: None,
            error: Some("boom".to_string()),
            mode: crate::runner_host::RunnerExecutionMode::Exec,
        };
        match ensure_success(&failed, "provider-a", "encode") {
            Ok(_) => panic!("expected error"),
            Err(err) => assert!(err.to_string().contains("provider-a.encode failed: boom")),
        }
    }
}