greentic-types 1.1.0-dev.26640931069

Shared primitives for Greentic: TenantCtx, InvocationEnvelope, NodeError, ids.
Documentation
//! Provider manifest and index data structures.
//!
//! These types model the JSON returned by provider-core `describe()` and the provider index
//! entries used by store, deployer, and runner components.

use alloc::collections::{BTreeMap, BTreeSet};
use alloc::{format, string::String, vec::Vec};

#[cfg(feature = "schemars")]
use schemars::JsonSchema;
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
use serde_json::Value;

use crate::{ErrorCode, GResult, GreenticError};

/// Canonical provider extension identifier stored in pack manifests.
pub const PROVIDER_EXTENSION_ID: &str = "greentic.provider-extension.v1";

/// Manifest describing a provider returned by `describe()`.
#[derive(Clone, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "schemars", derive(JsonSchema))]
pub struct ProviderManifest {
    /// Provider type identifier (string-based to avoid enum coupling).
    pub provider_type: String,
    /// Capabilities advertised by the provider.
    #[cfg_attr(
        feature = "serde",
        serde(default, skip_serializing_if = "Vec::is_empty")
    )]
    pub capabilities: Vec<String>,
    /// Operations exposed by the provider.
    #[cfg_attr(
        feature = "serde",
        serde(default, skip_serializing_if = "Vec::is_empty")
    )]
    pub ops: Vec<String>,
    /// Optional JSON Schema reference for configuration.
    #[cfg_attr(
        feature = "serde",
        serde(default, skip_serializing_if = "Option::is_none")
    )]
    pub config_schema_ref: Option<String>,
    /// Optional JSON Schema reference for provider state.
    #[cfg_attr(
        feature = "serde",
        serde(default, skip_serializing_if = "Option::is_none")
    )]
    pub state_schema_ref: Option<String>,
}

/// Runtime binding for a provider implementation.
#[derive(Clone, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "schemars", derive(JsonSchema))]
pub struct ProviderRuntimeRef {
    /// Component identifier for the provider runtime.
    pub component_ref: String,
    /// Exported function implementing the provider runtime.
    pub export: String,
    /// WIT world for the provider runtime.
    pub world: String,
}

/// Provider declaration stored in indexes and extension payloads.
#[derive(Clone, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "schemars", derive(JsonSchema))]
pub struct ProviderDecl {
    /// Provider type identifier (string-based to avoid enum coupling).
    pub provider_type: String,
    /// Instance identifier for this provider declaration. Distinct from
    /// `provider_type`: a single `provider_type` may be instanced multiple
    /// times at the environment level, each instance carrying its own
    /// `provider_id`. Absent when the declaration is single-instance.
    #[cfg_attr(
        feature = "serde",
        serde(default, skip_serializing_if = "Option::is_none")
    )]
    pub provider_id: Option<String>,
    /// Capabilities advertised by the provider.
    #[cfg_attr(
        feature = "serde",
        serde(default, skip_serializing_if = "Vec::is_empty")
    )]
    pub capabilities: Vec<String>,
    /// Operations exposed by the provider.
    #[cfg_attr(
        feature = "serde",
        serde(default, skip_serializing_if = "Vec::is_empty")
    )]
    pub ops: Vec<String>,
    /// JSON Schema reference for configuration.
    pub config_schema_ref: String,
    /// Optional JSON Schema reference for provider state.
    #[cfg_attr(
        feature = "serde",
        serde(default, skip_serializing_if = "Option::is_none")
    )]
    pub state_schema_ref: Option<String>,
    /// Runtime binding information for the provider.
    pub runtime: ProviderRuntimeRef,
    /// Optional documentation reference.
    #[cfg_attr(
        feature = "serde",
        serde(default, skip_serializing_if = "Option::is_none")
    )]
    pub docs_ref: Option<String>,
}

/// Inline extension payload embedding provider declarations.
#[derive(Clone, Debug, PartialEq, Eq, Default)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "schemars", derive(JsonSchema))]
pub struct ProviderExtensionInline {
    /// Providers included in the extension payload.
    pub providers: Vec<ProviderDecl>,
    /// Additional fields preserved for forward-compatibility.
    #[cfg_attr(
        feature = "serde",
        serde(default, skip_serializing_if = "BTreeMap::is_empty", flatten)
    )]
    pub additional_fields: BTreeMap<String, Value>,
}

impl ProviderExtensionInline {
    /// Performs basic structural validation without provider-specific semantics.
    ///
    /// Rules:
    /// - `provider_type` must be non-empty and globally unique within the inline.
    /// - `provider_id`, when present, must be non-empty and globally unique
    ///   within the inline. Absent `provider_id` is permitted and marks the
    ///   declaration as single-instance.
    /// - An explicit `provider_id` may equal its OWN `provider_type` (harmless
    ///   alias), but must NOT equal any OTHER declaration's `provider_type`.
    ///   This prevents a lookup keyed on `id == "teams"` from silently
    ///   resolving to a Slack runtime when a Slack decl declared
    ///   `provider_id: "teams"`.
    /// - Runtime fields (`component_ref`, `export`, `world`) must be set.
    pub fn validate_basic(&self) -> GResult<()> {
        // Pass 1: per-provider structural checks + collect every provider_type.
        // Building the complete provider_type set up front is what makes the
        // cross-namespace check in Pass 2 order-independent (a provider_id on
        // decl A can collide with a provider_type on a later-declared decl B).
        let mut seen_types = BTreeSet::new();
        for provider in &self.providers {
            if provider.provider_type.is_empty() {
                return Err(GreenticError::new(
                    ErrorCode::InvalidInput,
                    "ProviderDecl.provider_type must not be empty",
                ));
            }
            if !seen_types.insert(provider.provider_type.as_str()) {
                return Err(GreenticError::new(
                    ErrorCode::InvalidInput,
                    format!(
                        "duplicate provider_type '{}' in ProviderExtensionInline",
                        provider.provider_type
                    ),
                ));
            }
            if provider.runtime.component_ref.trim().is_empty()
                || provider.runtime.export.trim().is_empty()
                || provider.runtime.world.trim().is_empty()
            {
                return Err(GreenticError::new(
                    ErrorCode::InvalidInput,
                    format!(
                        "runtime fields must be set for provider '{}'",
                        provider.provider_type
                    ),
                ));
            }
        }

        // Pass 2: provider_id uniqueness + cross-namespace collision.
        let mut seen_ids = BTreeSet::new();
        for provider in &self.providers {
            if let Some(id) = provider.provider_id.as_deref() {
                if id.is_empty() {
                    return Err(GreenticError::new(
                        ErrorCode::InvalidInput,
                        format!(
                            "ProviderDecl.provider_id must not be empty when set (provider_type '{}')",
                            provider.provider_type
                        ),
                    ));
                }
                if !seen_ids.insert(id) {
                    return Err(GreenticError::new(
                        ErrorCode::InvalidInput,
                        format!("duplicate provider_id '{id}' in ProviderExtensionInline"),
                    ));
                }
                if id != provider.provider_type && seen_types.contains(id) {
                    return Err(GreenticError::new(
                        ErrorCode::InvalidInput,
                        format!(
                            "provider_id '{id}' collides with another declaration's provider_type"
                        ),
                    ));
                }
            }
        }
        Ok(())
    }
}