greentic-component 0.5.0

High-level component loader and store for Greentic components
Documentation
#![cfg(feature = "cli")]

use std::collections::BTreeMap;

use serde_json::{Value as JsonValue, json};

use super::validate::ValidationError;

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RuntimeCapabilitiesInput {
    pub filesystem_mode: String,
    pub filesystem_mounts: Vec<RuntimeFilesystemMount>,
    pub messaging_inbound: bool,
    pub messaging_outbound: bool,
    pub events_inbound: bool,
    pub events_outbound: bool,
    pub http_client: bool,
    pub http_server: bool,
    pub state_read: bool,
    pub state_write: bool,
    pub state_delete: bool,
    pub telemetry_scope: String,
    pub telemetry_span_prefix: Option<String>,
    pub telemetry_attributes: BTreeMap<String, String>,
    pub secret_keys: Vec<String>,
    pub secret_env: String,
    pub secret_tenant: String,
    pub secret_format: String,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RuntimeFilesystemMount {
    pub name: String,
    pub host_class: String,
    pub guest_path: String,
}

impl Default for RuntimeCapabilitiesInput {
    fn default() -> Self {
        Self {
            filesystem_mode: "none".to_string(),
            filesystem_mounts: Vec::new(),
            messaging_inbound: false,
            messaging_outbound: false,
            events_inbound: false,
            events_outbound: false,
            http_client: false,
            http_server: false,
            state_read: false,
            state_write: false,
            state_delete: false,
            telemetry_scope: "node".to_string(),
            telemetry_span_prefix: None,
            telemetry_attributes: BTreeMap::new(),
            secret_keys: Vec::new(),
            secret_env: "dev".to_string(),
            secret_tenant: "default".to_string(),
            secret_format: "text".to_string(),
        }
    }
}

impl RuntimeCapabilitiesInput {
    fn effective_filesystem_mounts(&self) -> &[RuntimeFilesystemMount] {
        if self.filesystem_mode == "none" {
            &[]
        } else {
            &self.filesystem_mounts
        }
    }

    pub fn manifest_secret_requirements(&self) -> JsonValue {
        JsonValue::Array(
            self.secret_keys
                .iter()
                .map(|key| {
                    json!({
                        "key": key,
                        "required": true,
                        "scope": {
                            "env": self.secret_env,
                            "tenant": self.secret_tenant
                        },
                        "format": self.secret_format
                    })
                })
                .collect(),
        )
    }

    pub fn manifest_capabilities(&self) -> JsonValue {
        let mut wasi = serde_json::Map::new();
        wasi.insert(
            "filesystem".to_string(),
            json!({
                "mode": self.filesystem_mode,
                "mounts": self.effective_filesystem_mounts().iter().map(|mount| {
                    json!({
                        "name": mount.name,
                        "host_class": mount.host_class,
                        "guest_path": mount.guest_path
                    })
                }).collect::<Vec<_>>()
            }),
        );
        wasi.insert("random".to_string(), JsonValue::Bool(true));
        wasi.insert("clocks".to_string(), JsonValue::Bool(true));

        let mut host = serde_json::Map::new();
        if self.messaging_inbound || self.messaging_outbound {
            host.insert(
                "messaging".to_string(),
                json!({
                    "inbound": self.messaging_inbound,
                    "outbound": self.messaging_outbound
                }),
            );
        }
        if self.events_inbound || self.events_outbound {
            host.insert(
                "events".to_string(),
                json!({
                    "inbound": self.events_inbound,
                    "outbound": self.events_outbound
                }),
            );
        }
        host.insert(
            "telemetry".to_string(),
            json!({
                "scope": self.telemetry_scope
            }),
        );
        host.insert(
            "secrets".to_string(),
            json!({
                "required": self.manifest_secret_requirements()
            }),
        );
        if self.http_client || self.http_server {
            host.insert(
                "http".to_string(),
                json!({
                    "client": self.http_client,
                    "server": self.http_server
                }),
            );
        }
        let state_write = self.state_write || self.state_delete;
        if self.state_read || state_write || self.state_delete {
            host.insert(
                "state".to_string(),
                json!({
                    "read": self.state_read,
                    "write": state_write,
                    "delete": self.state_delete
                }),
            );
        }

        let mut capabilities = serde_json::Map::new();
        capabilities.insert("wasi".to_string(), JsonValue::Object(wasi));
        capabilities.insert("host".to_string(), JsonValue::Object(host));
        JsonValue::Object(capabilities)
    }

    pub fn manifest_telemetry(&self) -> Option<JsonValue> {
        self.telemetry_span_prefix.as_ref().map(|prefix| {
            json!({
                "span_prefix": prefix,
                "attributes": self.telemetry_attributes,
                "emit_node_spans": true
            })
        })
    }
}

pub fn parse_filesystem_mode(value: &str) -> Result<String, ValidationError> {
    match value.trim() {
        "none" | "read_only" | "sandbox" => Ok(value.trim().to_string()),
        other => Err(ValidationError::InvalidFilesystemMode(other.to_string())),
    }
}

pub fn parse_telemetry_scope(value: &str) -> Result<String, ValidationError> {
    match value.trim() {
        "tenant" | "pack" | "node" => Ok(value.trim().to_string()),
        other => Err(ValidationError::InvalidTelemetryScope(other.to_string())),
    }
}

pub fn parse_secret_format(value: &str) -> Result<String, ValidationError> {
    match value.trim() {
        "bytes" | "text" | "json" => Ok(value.trim().to_string()),
        other => Err(ValidationError::InvalidSecretFormat(other.to_string())),
    }
}

pub fn parse_filesystem_mount(value: &str) -> Result<RuntimeFilesystemMount, ValidationError> {
    let mut parts = value.splitn(3, ':').map(str::trim);
    let name = parts.next().unwrap_or_default();
    let host_class = parts.next().unwrap_or_default();
    let guest_path = parts.next().unwrap_or_default();
    if name.is_empty() || host_class.is_empty() || guest_path.is_empty() {
        return Err(ValidationError::InvalidFilesystemMount(value.to_string()));
    }
    Ok(RuntimeFilesystemMount {
        name: name.to_string(),
        host_class: host_class.to_string(),
        guest_path: guest_path.to_string(),
    })
}

pub fn parse_telemetry_attributes(
    values: &[String],
) -> Result<BTreeMap<String, String>, ValidationError> {
    let mut attributes = BTreeMap::new();
    for value in values {
        let Some((key, attr_value)) = value.split_once('=') else {
            return Err(ValidationError::InvalidTelemetryAttribute(value.clone()));
        };
        let key = key.trim();
        let attr_value = attr_value.trim();
        if key.is_empty() || attr_value.is_empty() {
            return Err(ValidationError::InvalidTelemetryAttribute(value.clone()));
        }
        attributes.insert(key.to_string(), attr_value.to_string());
    }
    Ok(attributes)
}