greentic-dev 0.4.65

Developer CLI and local tooling for Greentic flows, packs, and components
Documentation
use greentic_interfaces_guest::{
    component,
    component::exports::greentic::component::node,
    http_client,
    secrets_store,
    state_store,
    telemetry_logger,
    types,
};
use serde::{Deserialize, Serialize};
use serde_json::{json, Value};

struct Component;

component::export!(Component);

#[derive(Debug, Deserialize)]
struct InvokeInput {
    #[serde(default)]
    message: String,
    #[serde(default = "default_secret_name")]
    secret_name: String,
}

#[derive(Debug, Serialize)]
struct InvokeOutput {
    echoed: String,
    operation: String,
    secret_sample: Option<String>,
    state_value: Option<String>,
    http_status: Option<u16>,
}

impl node::Guest for Component {
    fn get_manifest() -> String {
        let schema: Value = serde_json::from_str(include_str!("../schemas/v1/config.schema.json"))
            .expect("config schema json to be valid");
        json!({
            "name": "{{component_name}}",
            "description": "Generated Greentic component scaffold",
            "capabilities": ["telemetry", "secrets", "state", "http"],
            "exports": [
                { "operation": "invoke" }
            ],
            "config_schema": schema,
            "secrets": ["{{component_name}}/example"],
            "wit_compat": {
                "package": "greentic:component",
                "min": "{{component_world_version}}",
                "max": "{{component_world_version}}"
            }
        })
        .to_string()
    }

    fn on_start(ctx: node::ExecCtx) -> Result<node::LifecycleStatus, String> {
        let tenant_ctx = tenant_ctx_from_exec(&ctx);
        let span = span_from_exec(&ctx);
        let _ = telemetry_logger::log(
            span,
            vec![("event".to_string(), "component-start".to_string())],
            Some(tenant_ctx),
        )
        .map_err(format_host_error);
        Ok(node::LifecycleStatus::Ok)
    }

    fn on_stop(
        ctx: node::ExecCtx,
        reason: String,
    ) -> Result<node::LifecycleStatus, String> {
        let tenant_ctx = tenant_ctx_from_exec(&ctx);
        let span = span_from_exec(&ctx);
        let _ = telemetry_logger::log(
            span,
            vec![
                ("event".to_string(), "component-stop".to_string()),
                ("reason".to_string(), reason),
            ],
            Some(tenant_ctx),
        )
        .map_err(format_host_error);
        Ok(node::LifecycleStatus::Ok)
    }

    fn invoke(
        ctx: node::ExecCtx,
        operation: String,
        input: String,
    ) -> node::InvokeResult {
        let payload: InvokeInput =
            serde_json::from_str(&input).unwrap_or(InvokeInput { message: input, secret_name: default_secret_name() });
        let tenant_ctx = tenant_ctx_from_exec(&ctx);

        let secret_sample = secrets_store::read(&payload.secret_name)
            .ok()
            .and_then(Result::ok)
            .and_then(|bytes| String::from_utf8(bytes).ok());

        let state_key: types::StateKey = format!("{{component_name}}:echo").into();
        let state_value = state_store::read(state_key.clone(), Some(tenant_ctx.clone()))
            .ok()
            .and_then(Result::ok)
            .and_then(|bytes| String::from_utf8(bytes).ok());

        let http_status = http_client::send(
            http_client::Request {
                method: "GET".to_string(),
                url: "https://example.com/health".to_string(),
                headers: Vec::new(),
                body: None,
            },
            Some(tenant_ctx.clone()),
        )
        .ok()
        .and_then(Result::ok)
        .map(|resp| resp.status);

        let response = InvokeOutput {
            echoed: payload.message,
            operation,
            secret_sample,
            state_value,
            http_status,
        };
        node::InvokeResult::Ok(
            serde_json::to_string(&response).expect("serialize response"),
        )
    }

    fn invoke_stream(
        _ctx: node::ExecCtx,
        _operation: String,
        _input: String,
    ) -> Vec<node::StreamEvent> {
        Vec::new()
    }
}

fn tenant_ctx_from_exec(ctx: &node::ExecCtx) -> types::TenantCtx {
    let tenant = ctx.tenant.tenant.clone();
    types::TenantCtx {
        env: tenant.clone(),
        tenant: tenant.clone(),
        tenant_id: tenant.clone(),
        team: ctx.tenant.team.clone(),
        team_id: ctx.tenant.team.clone(),
        user: ctx.tenant.user.clone(),
        user_id: ctx.tenant.user.clone(),
        session_id: None,
        flow_id: Some(ctx.flow_id.clone()),
        node_id: ctx.node_id.clone(),
        provider_id: None,
        trace_id: ctx.tenant.trace_id.clone(),
        correlation_id: ctx.tenant.correlation_id.clone(),
        attributes: Vec::new(),
        deadline_ms: ctx
            .tenant
            .deadline_unix_ms
            .and_then(|d| i64::try_from(d).ok()),
        attempt: ctx.tenant.attempt,
        idempotency_key: ctx.tenant.idempotency_key.clone(),
        impersonation: None,
    }
}

fn span_from_exec(ctx: &node::ExecCtx) -> types::SpanContext {
    types::SpanContext {
        tenant: ctx.tenant.tenant.clone(),
        session_id: None,
        flow_id: ctx.flow_id.clone(),
        node_id: ctx.node_id.clone(),
        provider: "{{component_name}}".to_string(),
        start_ms: None,
        end_ms: None,
    }
}

fn format_host_error<E: std::fmt::Display>(err: E) -> String {
    err.to_string()
}

fn default_secret_name() -> String {
    "{{component_name}}/example".to_string()
}