use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct BehaviorConfig {
#[serde(rename = "$schema", skip_serializing_if = "Option::is_none")]
#[schemars(example = &"https://mecha10.dev/schemas/behavior-composition-v1.json")]
pub schema: Option<String>,
#[schemars(example = &"patrol_mission")]
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
#[schemars(example = &"Patrol behavior with safety layer")]
pub description: Option<String>,
pub root: CompositionConfig,
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
#[schemars(example = HashMap::from([
("safety".to_string(), serde_json::json!({"max_speed": 1.0})),
("detector".to_string(), serde_json::json!({"confidence": 0.7})),
("wander".to_string(), serde_json::json!({"speed": 0.5})),
]))]
pub configs: HashMap<String, serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum CompositionConfig {
Sequence {
children: Vec<CompositionConfig>,
#[serde(skip_serializing_if = "Option::is_none")]
name: Option<String>,
},
Selector {
children: Vec<CompositionConfig>,
#[serde(skip_serializing_if = "Option::is_none")]
name: Option<String>,
},
Parallel {
children: Vec<CompositionConfig>,
#[serde(default = "default_parallel_policy")]
policy: ParallelPolicyConfig,
#[serde(skip_serializing_if = "Option::is_none")]
name: Option<String>,
},
Watch {
children: Vec<CompositionConfig>,
#[serde(default = "default_parallel_policy")]
policy: ParallelPolicyConfig,
#[serde(skip_serializing_if = "Option::is_none")]
name: Option<String>,
},
Subsumption {
layers: Vec<SubsumptionLayerConfig>,
#[serde(skip_serializing_if = "Option::is_none")]
name: Option<String>,
},
Inverter {
child: Box<CompositionConfig>,
#[serde(skip_serializing_if = "Option::is_none")]
name: Option<String>,
},
ForceSuccess {
child: Box<CompositionConfig>,
#[serde(skip_serializing_if = "Option::is_none")]
name: Option<String>,
},
ForceFailure {
child: Box<CompositionConfig>,
#[serde(skip_serializing_if = "Option::is_none")]
name: Option<String>,
},
Repeat {
child: Box<CompositionConfig>,
#[serde(skip_serializing_if = "Option::is_none")]
count: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
name: Option<String>,
},
Retry {
child: Box<CompositionConfig>,
#[schemars(example = &3)]
max_attempts: u32,
#[serde(skip_serializing_if = "Option::is_none")]
name: Option<String>,
},
Timeout {
child: Box<CompositionConfig>,
#[schemars(example = &30.0)]
timeout_secs: f64,
#[serde(skip_serializing_if = "Option::is_none")]
name: Option<String>,
},
Node {
#[schemars(example = &"wander")]
node: String,
#[serde(skip_serializing_if = "Option::is_none")]
#[schemars(example = &"wander")]
config_ref: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
config: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
name: Option<String>,
},
}
fn default_parallel_policy() -> ParallelPolicyConfig {
ParallelPolicyConfig::RequireAll
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum ParallelPolicyConfig {
RequireAll,
RequireOne,
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),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct SubsumptionLayerConfig {
pub priority: u8,
pub node: CompositionConfig,
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct NodeConfig {
#[schemars(example = &"wander")]
pub node: String,
#[serde(default)]
pub config: serde_json::Value,
}
#[derive(Debug, Clone)]
pub struct NodeReference {
pub node_type: String,
pub config: serde_json::Value,
pub name: Option<String>,
}
impl BehaviorConfig {
pub fn from_json(json: &str) -> anyhow::Result<Self> {
Ok(serde_json::from_str(json)?)
}
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)
}
pub fn to_json(&self) -> anyhow::Result<String> {
Ok(serde_json::to_string_pretty(self)?)
}
pub fn generate_schema() -> anyhow::Result<String> {
Ok(serde_json::to_string_pretty(&Self::schema_value())?)
}
pub fn schema_value() -> serde_json::Value {
let schema = schemars::schema_for!(BehaviorConfig);
serde_json::to_value(&schema).expect("schemars schema always serializes to JSON")
}
pub fn get_config(&self, key: &str) -> Option<&serde_json::Value> {
self.configs.get(key)
}
}