phoxal 0.8.0

Phoxal — production-oriented autonomous robot framework (engine, model, typed bus, contracts).
Documentation
use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
pub struct ScenarioSpec {
    pub name: String,
    pub world: WebotsWorld,
    pub test: String,
    pub timeout_secs: u64,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub category: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub tier: Option<u8>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub fixture_bundle: Option<String>,
}

#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
#[serde(transparent)]
pub struct WebotsWorld {
    name: String,
}

impl WebotsWorld {
    pub fn named(name: impl Into<String>) -> Self {
        Self { name: name.into() }
    }

    pub fn as_str(&self) -> &str {
        &self.name
    }
}

impl ScenarioSpec {
    pub fn new(name: impl Into<String>) -> Self {
        let name = name.into();
        Self {
            test: name.clone(),
            name,
            world: WebotsWorld::named("headless"),
            timeout_secs: 30,
            category: None,
            tier: None,
            fixture_bundle: None,
        }
    }

    pub fn world(mut self, world: WebotsWorld) -> Self {
        self.world = world;
        self
    }

    pub fn test(mut self, test: impl Into<String>) -> Self {
        self.test = test.into();
        self
    }

    pub fn timeout_secs(mut self, timeout_secs: u64) -> Self {
        self.timeout_secs = timeout_secs;
        self
    }

    pub fn category(mut self, category: impl Into<String>) -> Self {
        self.category = Some(category.into());
        self
    }

    pub fn tier(mut self, tier: u8) -> Self {
        self.tier = Some(tier);
        self
    }

    pub fn fixture_bundle(mut self, fixture_bundle: impl Into<String>) -> Self {
        self.fixture_bundle = Some(fixture_bundle.into());
        self
    }
}

#[cfg(test)]
mod tests {
    use super::{ScenarioSpec, WebotsWorld};

    #[test]
    fn scenario_spec_builds_robot_acceptance_shape() {
        let spec = ScenarioSpec::new("drive-forward")
            .world(WebotsWorld::named("ArenaWorld"))
            .test("drive_forward")
            .timeout_secs(30)
            .category("directed-drive")
            .tier(1);

        assert_eq!(spec.name, "drive-forward");
        assert_eq!(spec.world.as_str(), "ArenaWorld");
        assert_eq!(spec.test, "drive_forward");
        assert_eq!(spec.timeout_secs, 30);
        assert_eq!(spec.category.as_deref(), Some("directed-drive"));
        assert_eq!(spec.tier, Some(1));
        assert_eq!(spec.fixture_bundle, None);
    }

    #[test]
    fn webots_world_serializes_as_name() {
        let json = serde_json::to_string(&WebotsWorld::named("SimpleWorld")).unwrap();

        assert_eq!(json, "\"SimpleWorld\"");
    }
}