boardwalk 1.0.0

Hypermedia server framework with reverse-tunnel federation
Documentation
use std::collections::BTreeMap;

use serde_json::{Map, Value as Json};

use crate::runtime::{
    Actor, DynFuture, Resource, ResourceCtx, ResourceError, ResourceSnapshot, ResourceSpec,
    SnapshotStreamSpec, StreamKind, StreamSpec, TransitionAffordance, TransitionCtx,
    TransitionError, TransitionInput, TransitionOutcome, TransitionSpec,
};

#[derive(Default)]
pub(crate) struct ActorLed {
    on: bool,
}

impl ActorLed {
    fn snapshot(&self) -> ResourceSnapshot {
        let state = if self.on { "on" } else { "off" };
        let mut properties = Map::new();
        properties.insert("fixture".into(), Json::String("actor-led".into()));

        ResourceSnapshot {
            id: "runtime-assigned".into(),
            kind: "led".into(),
            name: Some("LED".into()),
            state: Some(state.into()),
            node: "runtime-assigned".into(),
            properties,
            labels: BTreeMap::new(),
            transitions: vec![
                TransitionAffordance {
                    spec: TransitionSpec {
                        name: "turn-on".into(),
                        allowed_states: vec!["off".into()],
                        ..Default::default()
                    },
                    available: !self.on,
                    unavailable_reason: self.on.then_some("already on".into()),
                },
                TransitionAffordance {
                    spec: TransitionSpec {
                        name: "turn-off".into(),
                        allowed_states: vec!["on".into()],
                        ..Default::default()
                    },
                    available: self.on,
                    unavailable_reason: (!self.on).then_some("already off".into()),
                },
            ],
            streams: vec![SnapshotStreamSpec {
                name: "state".into(),
                kind: "object".into(),
            }],
            revision: None,
            metadata: Map::new(),
        }
    }
}

impl Resource for ActorLed {
    fn spec(&self) -> ResourceSpec {
        ResourceSpec {
            kind: "led".into(),
            name: Some("LED".into()),
            labels: BTreeMap::new(),
            property_schema: None,
            streams: vec![StreamSpec {
                name: "state".into(),
                kind: StreamKind::Object,
            }],
        }
    }

    fn snapshot<'a>(
        &'a self,
        _ctx: ResourceCtx,
    ) -> DynFuture<'a, Result<ResourceSnapshot, ResourceError>> {
        Box::pin(async move { Ok(self.snapshot()) })
    }
}

impl Actor for ActorLed {
    fn transition<'a>(
        &'a mut self,
        ctx: TransitionCtx,
        name: &'a str,
        _input: TransitionInput,
    ) -> DynFuture<'a, Result<TransitionOutcome, TransitionError>> {
        Box::pin(async move {
            match name {
                "turn-on" if !self.on => self.on = true,
                "turn-off" if self.on => self.on = false,
                other => {
                    return Err(TransitionError::NotAllowed(format!(
                        "transition `{other}` is not available"
                    )));
                }
            }

            let state = if self.on { "on" } else { "off" };
            ctx.publish(
                "state",
                "resource.state.changed",
                1,
                Json::String(state.into()),
            )
            .await?;

            Ok(TransitionOutcome::Completed {
                output: None,
                snapshot: self.snapshot(),
            })
        })
    }
}