mecha10-behavior-runtime 0.1.22

Behavior tree runtime for Mecha10 - unified AI and logic composition system
Documentation
//! JSON configuration types for behavior composition
//!
//! This module provides types for defining behavior trees in JSON configuration files.

use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;

/// Root configuration for a behavior composition.
///
/// This is the top-level structure for defining behavior trees in JSON.
///
/// # Example JSON
///
/// ```json
/// {
///   "$schema": "https://mecha10.dev/schemas/behavior-composition-v1.json",
///   "name": "patrol_mission",
///   "description": "Patrol behavior with safety layer",
///   "root": {
///     "type": "sequence",
///     "children": [
///       {
///         "type": "node",
///         "node": "safety_check",
///         "config_ref": "safety"
///       },
///       {
///         "type": "selector",
///         "children": [
///           {
///             "type": "node",
///             "node": "detect_obstacles",
///             "config_ref": "detector"
///           },
///           {
///             "type": "node",
///             "node": "wander",
///             "config_ref": "wander"
///           }
///         ]
///       }
///     ]
///   },
///   "configs": {
///     "safety": { "max_speed": 1.0 },
///     "detector": { "confidence": 0.7 },
///     "wander": { "speed": 0.5 }
///   }
/// }
/// ```
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct BehaviorConfig {
    /// Schema reference for validation and IntelliSense
    #[serde(rename = "$schema", skip_serializing_if = "Option::is_none")]
    pub schema: Option<String>,

    /// Name of this behavior composition
    pub name: String,

    /// Optional description
    #[serde(skip_serializing_if = "Option::is_none")]
    pub description: Option<String>,

    /// Root composition node
    pub root: CompositionConfig,

    /// Configuration values referenced by nodes
    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
    pub configs: HashMap<String, serde_json::Value>,
}

/// Configuration for a composition node (sequence, selector, parallel, or leaf).
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum CompositionConfig {
    /// A sequence node - executes children in order
    Sequence {
        /// Child nodes to execute in sequence
        children: Vec<CompositionConfig>,

        /// Optional name for this sequence
        #[serde(skip_serializing_if = "Option::is_none")]
        name: Option<String>,
    },

    /// A selector node - tries children until one succeeds
    Selector {
        /// Child nodes to try in order
        children: Vec<CompositionConfig>,

        /// Optional name for this selector
        #[serde(skip_serializing_if = "Option::is_none")]
        name: Option<String>,
    },

    /// A parallel node - executes all children concurrently
    Parallel {
        /// Child nodes to execute in parallel
        children: Vec<CompositionConfig>,

        /// Policy for success/failure
        #[serde(default = "default_parallel_policy")]
        policy: ParallelPolicyConfig,

        /// Optional name for this parallel node
        #[serde(skip_serializing_if = "Option::is_none")]
        name: Option<String>,
    },

    /// A reference to a Rust behavior node
    Node {
        /// Name of the behavior node type
        node: String,

        /// Reference to configuration in the configs map
        #[serde(skip_serializing_if = "Option::is_none")]
        config_ref: Option<String>,

        /// Inline configuration (alternative to config_ref)
        #[serde(skip_serializing_if = "Option::is_none")]
        config: Option<serde_json::Value>,

        /// Optional name for this node
        #[serde(skip_serializing_if = "Option::is_none")]
        name: Option<String>,
    },
}

fn default_parallel_policy() -> ParallelPolicyConfig {
    ParallelPolicyConfig::RequireAll
}

/// Policy configuration for parallel nodes.
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum ParallelPolicyConfig {
    /// All children must succeed
    RequireAll,

    /// At least one child must succeed
    RequireOne,

    /// At least N children must succeed
    RequireN(usize),
}

impl From<ParallelPolicyConfig> for crate::composition::ParallelPolicy {
    fn from(config: ParallelPolicyConfig) -> Self {
        match config {
            ParallelPolicyConfig::RequireAll => crate::composition::ParallelPolicy::RequireAll,
            ParallelPolicyConfig::RequireOne => crate::composition::ParallelPolicy::RequireOne,
            ParallelPolicyConfig::RequireN(n) => crate::composition::ParallelPolicy::RequireN(n),
        }
    }
}

/// Configuration for a single node.
///
/// This is used when defining node configurations separately from the composition tree.
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct NodeConfig {
    /// Name of the node type
    pub node: String,

    /// Configuration values for this node
    #[serde(default)]
    pub config: serde_json::Value,
}

/// Reference to a behavior node that will be instantiated at runtime.
///
/// This is used by the execution engine to look up and instantiate behaviors.
#[derive(Debug, Clone)]
pub struct NodeReference {
    /// Node type identifier
    pub node_type: String,

    /// Configuration for the node
    pub config: serde_json::Value,

    /// Optional name for debugging
    pub name: Option<String>,
}

impl BehaviorConfig {
    /// Load a behavior configuration from a JSON string.
    ///
    /// # Example
    ///
    /// ```rust
    /// use mecha10_behavior_runtime::BehaviorConfig;
    ///
    /// let json = r#"{
    ///     "name": "test_behavior",
    ///     "root": {
    ///         "type": "sequence",
    ///         "children": []
    ///     }
    /// }"#;
    ///
    /// let config = BehaviorConfig::from_json(json).unwrap();
    /// assert_eq!(config.name, "test_behavior");
    /// ```
    pub fn from_json(json: &str) -> anyhow::Result<Self> {
        Ok(serde_json::from_str(json)?)
    }

    /// Load a behavior configuration from a JSON file.
    pub fn from_file(path: impl AsRef<std::path::Path>) -> anyhow::Result<Self> {
        let content = std::fs::read_to_string(path)?;
        Self::from_json(&content)
    }

    /// Convert this configuration to JSON.
    pub fn to_json(&self) -> anyhow::Result<String> {
        Ok(serde_json::to_string_pretty(self)?)
    }

    /// Generate a JSON schema for this configuration type.
    ///
    /// This can be used for editor IntelliSense and validation.
    pub fn generate_schema() -> anyhow::Result<String> {
        let schema = schemars::schema_for!(BehaviorConfig);
        Ok(serde_json::to_string_pretty(&schema)?)
    }

    /// Get a configuration value by reference key.
    pub fn get_config(&self, key: &str) -> Option<&serde_json::Value> {
        self.configs.get(key)
    }
}