greentic-config-types 0.4.1

Schema-only types for Greentic host configuration (no IO)
Documentation
use serde::{Deserialize, Serialize};
use std::path::PathBuf;

pub use greentic_types::{ConnectionKind, DeploymentCtx, EnvId};

#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(transparent)]
pub struct ConfigVersion(pub String);

impl ConfigVersion {
    pub fn v1() -> Self {
        Self("1".to_string())
    }
}

impl Default for ConfigVersion {
    fn default() -> Self {
        Self::v1()
    }
}

#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct GreenticConfig {
    #[serde(default = "ConfigVersion::v1")]
    pub schema_version: ConfigVersion,
    pub environment: EnvironmentConfig,
    pub paths: PathsConfig,
    #[serde(default)]
    pub packs: Option<PacksConfig>,
    pub runtime: RuntimeConfig,
    pub telemetry: TelemetryConfig,
    pub network: NetworkConfig,
    pub secrets: SecretsBackendRefConfig,
    pub dev: Option<DevConfig>,
}

#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum ConfigSource {
    Default,
    User,
    Project,
    Environment,
    Cli,
}

#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(transparent)]
pub struct ProvenancePath(pub String);

#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct EnvironmentConfig {
    pub env_id: EnvId,
    pub deployment: Option<DeploymentCtx>,
    pub connection: Option<ConnectionKind>,
    pub region: Option<String>,
}

#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct PathsConfig {
    pub greentic_root: PathBuf,
    pub state_dir: PathBuf,
    pub cache_dir: PathBuf,
    pub logs_dir: PathBuf,
}

#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct PacksConfig {
    pub source: PackSourceConfig,
    pub cache_dir: PathBuf,
    #[serde(default)]
    pub index_cache_ttl_secs: Option<u64>,
    #[serde(default)]
    pub trust: Option<PackTrustConfig>,
}

#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum PackSourceConfig {
    LocalIndex { path: PathBuf },
    HttpIndex { url: String },
    OciRegistry { reference: String },
}

#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct PackTrustConfig {
    #[serde(default)]
    pub public_keys: Vec<String>,
    #[serde(default)]
    pub require_signatures: bool,
}

#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Default)]
pub struct RuntimeConfig {
    #[serde(default)]
    pub max_concurrency: Option<u32>,
    #[serde(default)]
    pub task_timeout_ms: Option<u64>,
    #[serde(default)]
    pub shutdown_grace_ms: Option<u64>,
}

#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct TelemetryConfig {
    #[serde(default = "default_enabled")]
    pub enabled: bool,
    #[serde(default)]
    pub exporter: TelemetryExporterKind,
    #[serde(default)]
    pub endpoint: Option<String>,
    #[serde(default = "default_sampling")]
    pub sampling: f32,
}

impl Default for TelemetryConfig {
    fn default() -> Self {
        Self {
            enabled: default_enabled(),
            exporter: TelemetryExporterKind::None,
            endpoint: None,
            sampling: default_sampling(),
        }
    }
}

fn default_enabled() -> bool {
    true
}

fn default_sampling() -> f32 {
    1.0
}

#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum TelemetryExporterKind {
    Otlp,
    Stdout,
    #[default]
    None,
}

#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct NetworkConfig {
    #[serde(default)]
    pub proxy_url: Option<String>,
    #[serde(default)]
    pub tls_mode: TlsMode,
    #[serde(default)]
    pub connect_timeout_ms: Option<u64>,
    #[serde(default)]
    pub read_timeout_ms: Option<u64>,
}

impl Default for NetworkConfig {
    fn default() -> Self {
        Self {
            proxy_url: None,
            tls_mode: TlsMode::System,
            connect_timeout_ms: None,
            read_timeout_ms: None,
        }
    }
}

#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum TlsMode {
    Disabled,
    #[default]
    System,
    Strict,
}

#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct SecretsBackendRefConfig {
    #[serde(default = "default_backend_kind")]
    pub kind: String,
    #[serde(default)]
    pub reference: Option<String>,
}

impl Default for SecretsBackendRefConfig {
    fn default() -> Self {
        Self {
            kind: default_backend_kind(),
            reference: None,
        }
    }
}

fn default_backend_kind() -> String {
    "none".to_string()
}

#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct DevConfig {
    pub default_env: EnvId,
    pub default_tenant: String,
    #[serde(default)]
    pub default_team: Option<String>,
}

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

    fn sample_toml() -> &'static str {
        r#"
schema_version = "1"

[environment]
env_id = "dev"
region = "us-east-1"

[paths]
greentic_root = "/workspace"
state_dir = "/workspace/.greentic"
cache_dir = "/workspace/.greentic/cache"
logs_dir = "/workspace/.greentic/logs"

[packs]
cache_dir = "/workspace/.greentic/cache/packs"
index_cache_ttl_secs = 3600

[packs.source]
type = "local_index"
path = "/workspace/.greentic/packs/index.json"

[packs.trust]
public_keys = ["/keys/key1.pem", "/keys/key2.pem"]
require_signatures = true

[runtime]
max_concurrency = 8
task_timeout_ms = 30000
shutdown_grace_ms = 5000

[telemetry]
enabled = true
exporter = "otlp"
endpoint = "http://localhost:4317"
sampling = 0.5

[network]
proxy_url = "http://proxy"
tls_mode = "system"
connect_timeout_ms = 1000
read_timeout_ms = 2000

[secrets]
kind = "vault"
reference = "ops"

[dev]
default_env = "dev"
default_tenant = "acme"
default_team = "devex"
"#
    }

    #[test]
    fn toml_round_trip() {
        let config: GreenticConfig = toml::from_str(sample_toml()).expect("deserialize");
        let serialized = toml::to_string(&config).expect("serialize");
        let config_back: GreenticConfig = toml::from_str(&serialized).expect("deserialize again");
        assert_eq!(config, config_back);
    }

    #[test]
    fn json_round_trip() {
        let config: GreenticConfig = serde_json::from_str(
            r#"{
            "schema_version": "1",
            "environment": {"env_id": "dev", "region": "us-west-2"},
            "paths": {
                "greentic_root": "/workspace",
                "state_dir": "/workspace/.greentic",
                "cache_dir": "/workspace/.greentic/cache",
                "logs_dir": "/workspace/.greentic/logs"
            },
            "packs": {
                "cache_dir": "/workspace/.greentic/cache/packs",
                "index_cache_ttl_secs": 3600,
                "source": {"type": "http_index", "url": "https://example.com/index.json"},
                "trust": {"public_keys": ["inline-key", "/keys/key.pem"], "require_signatures": true}
            },
            "runtime": {"max_concurrency": 4, "task_timeout_ms": 120000, "shutdown_grace_ms": 1000},
            "telemetry": {"enabled": true, "exporter": "stdout", "sampling": 1.0},
            "network": {"tls_mode": "system"},
            "secrets": {"kind": "none"},
            "dev": {"default_env": "dev", "default_tenant": "acme"}
        }"#,
        )
        .expect("json decode");

        let serialized = serde_json::to_string(&config).expect("json encode");
        let round: GreenticConfig = serde_json::from_str(&serialized).expect("json decode round");
        assert_eq!(config, round);
    }
}