Skip to main content

arora_behavior/
status.rs

1//! The behavior `Status` — the cross-interpreter run-status contract.
2//!
3//! `Status` is the value a task run writes to its status key and the value the
4//! ROS 2 action plane maps to a goal status. It lives here, in `arora-behavior`
5//! (beside [`BehaviorStatus`](crate::BehaviorStatus), `TaskHandle`, and
6//! `RunPolicy`), rather than in any single interpreter's crate: a node graph
7//! and a behavior tree both speak it, and neither should depend on the other's
8//! types for it.
9//!
10//! The Rust type is the source of truth for the schema — it derives
11//! [`AroraType`](arora_types::AroraType), pinning the enumeration id and each
12//! variant id, so no hand-authored record or codegen is needed. The value-plane
13//! conversions ([`From<Status>`](Value)/[`TryFrom<Value>`]) produce the
14//! `Value::Enumeration { id, variant_id, Unit }` form the plane already speaks;
15//! their ids match the derived schema, so the two never drift.
16
17use arora_types::value::{ConversionError, Enumeration, Value};
18use arora_types::AroraType;
19use arora_types::Uuid;
20
21/// A run's lifecycle status: still running, or a terminal outcome.
22///
23/// The one status vocabulary a behavior speaks on the value plane — the value
24/// an interpreter writes to a run's status key, and the value the ROS 2 action
25/// plane maps to `GoalStatus`.
26#[derive(Debug, Clone, Copy, PartialEq, Eq, AroraType)]
27#[arora(id = "325a5767-e344-4532-860e-0749bcf2e428")]
28pub enum Status {
29    /// The behavior finished cleanly.
30    #[arora(id = "766e9e9a-446d-4e46-83e6-14b7ca101169")]
31    Success,
32    /// The behavior finished in failure (also what a halted run reports — a
33    /// halted run did not reach its goal).
34    #[arora(id = "2468f46c-bb60-425c-9a4d-9ad326ccc7e2")]
35    Failure,
36    /// The behavior is still running; tick it again.
37    #[arora(id = "acd79ec6-0c44-401a-82f8-5da5422d3eec")]
38    Running,
39}
40
41/// The enumeration type id — the action-shape marker the ROS 2 bridge keys off,
42/// and the id every `Status` value carries. Matches the derived schema
43/// ([`Status::arora_type_id`](arora_types::AroraType::arora_type_id)).
44pub const STATUS_ENUMERATION_ID: Uuid = Uuid::from_bytes([
45    0x32, 0x5a, 0x57, 0x67, 0xe3, 0x44, 0x45, 0x32, 0x86, 0x0e, 0x07, 0x49, 0xbc, 0xf2, 0xe4, 0x28,
46]);
47/// The `Status::Success` variant id.
48pub const STATUS_SUCCESS_VARIANT_ID: Uuid = Uuid::from_bytes([
49    0x76, 0x6e, 0x9e, 0x9a, 0x44, 0x6d, 0x4e, 0x46, 0x83, 0xe6, 0x14, 0xb7, 0xca, 0x10, 0x11, 0x69,
50]);
51/// The `Status::Failure` variant id.
52pub const STATUS_FAILURE_VARIANT_ID: Uuid = Uuid::from_bytes([
53    0x24, 0x68, 0xf4, 0x6c, 0xbb, 0x60, 0x42, 0x5c, 0x9a, 0x4d, 0x9a, 0xd3, 0x26, 0xcc, 0xc7, 0xe2,
54]);
55/// The `Status::Running` variant id.
56pub const STATUS_RUNNING_VARIANT_ID: Uuid = Uuid::from_bytes([
57    0xac, 0xd7, 0x9e, 0xc6, 0x0c, 0x44, 0x40, 0x1a, 0x82, 0xf8, 0x5d, 0xa5, 0x42, 0x2d, 0x3e, 0xec,
58]);
59/// The version the `Status` type is registered under.
60pub const STATUS_ENUMERATION_VERSION: semver::Version = semver::Version::new(1, 0, 0);
61
62impl From<Status> for Value {
63    fn from(status: Status) -> Value {
64        let variant_id = match status {
65            Status::Success => STATUS_SUCCESS_VARIANT_ID,
66            Status::Failure => STATUS_FAILURE_VARIANT_ID,
67            Status::Running => STATUS_RUNNING_VARIANT_ID,
68        };
69        Value::Enumeration(Enumeration {
70            id: STATUS_ENUMERATION_ID,
71            variant_id,
72            value: Box::new(Value::Unit),
73        })
74    }
75}
76
77impl TryFrom<Value> for Status {
78    type Error = ConversionError;
79    fn try_from(value: Value) -> Result<Self, Self::Error> {
80        let Value::Enumeration(enumeration) = value else {
81            return Err(ConversionError {
82                message: "expected an enumeration value for Status".to_string(),
83            });
84        };
85        if enumeration.id != STATUS_ENUMERATION_ID {
86            return Err(ConversionError {
87                message: "enumeration id is not Status".to_string(),
88            });
89        }
90        match enumeration.variant_id {
91            STATUS_SUCCESS_VARIANT_ID => Ok(Status::Success),
92            STATUS_FAILURE_VARIANT_ID => Ok(Status::Failure),
93            STATUS_RUNNING_VARIANT_ID => Ok(Status::Running),
94            _ => Err(ConversionError {
95                message: "unknown Status variant".to_string(),
96            }),
97        }
98    }
99}
100
101/// Declare the `Status` enumeration in the record/registry form — the
102/// hand-authored declaration the behavior-tree and message codegen consume, kept
103/// alongside the type so the two stay in one place. The derived schema
104/// ([`Status::arora_type`](arora_types::AroraType::arora_type)) is the same type
105/// in `ty::low` form.
106pub fn declare_status_enumeration(
107    parent: Uuid,
108) -> arora_types::record::enumeration::unfrozen::Enumeration {
109    use arora_types::record::enumeration::unfrozen::{Enumeration, EnumerationVariant};
110    use arora_types::record::ty::{Primitive, PrimitiveKind, UnfrozenTy};
111    let unit = || {
112        UnfrozenTy::Primitive(Primitive {
113            kind: PrimitiveKind::Unit,
114        })
115    };
116    Enumeration {
117        name: "Status".to_string(),
118        parent,
119        variants: [
120            (
121                STATUS_SUCCESS_VARIANT_ID,
122                EnumerationVariant {
123                    name: "Success".to_string(),
124                    ty: unit(),
125                },
126            ),
127            (
128                STATUS_FAILURE_VARIANT_ID,
129                EnumerationVariant {
130                    name: "Failure".to_string(),
131                    ty: unit(),
132                },
133            ),
134            (
135                STATUS_RUNNING_VARIANT_ID,
136                EnumerationVariant {
137                    name: "Running".to_string(),
138                    ty: unit(),
139                },
140            ),
141        ]
142        .into_iter()
143        .collect(),
144    }
145}
146
147#[cfg(test)]
148mod tests {
149    use super::*;
150
151    /// The derived schema and the pinned const ids agree — the id the value
152    /// plane carries is the id the schema declares.
153    #[test]
154    fn derived_schema_matches_the_pinned_ids() {
155        assert_eq!(Status::arora_type_id(), STATUS_ENUMERATION_ID);
156        let arora_types::ty::low::TypeKind::Enumeration(enumeration) = Status::arora_type().kind
157        else {
158            panic!("Status is an enumeration");
159        };
160        let ids: Vec<_> = enumeration.values.keys().copied().collect();
161        assert_eq!(
162            ids,
163            vec![
164                STATUS_SUCCESS_VARIANT_ID,
165                STATUS_FAILURE_VARIANT_ID,
166                STATUS_RUNNING_VARIANT_ID
167            ]
168        );
169    }
170
171    /// The value-plane conversions round-trip and produce the stable
172    /// `Value::Enumeration { STATUS_ENUMERATION_ID, variant, Unit }` form.
173    #[test]
174    fn status_round_trips_through_value() {
175        for status in [Status::Success, Status::Failure, Status::Running] {
176            let value: Value = status.into();
177            let Value::Enumeration(ref enumeration) = value else {
178                panic!("a Status is an enumeration value");
179            };
180            assert_eq!(enumeration.id, STATUS_ENUMERATION_ID);
181            assert_eq!(*enumeration.value, Value::Unit);
182            assert_eq!(Status::try_from(value).unwrap(), status);
183        }
184    }
185}