iot-core 0.0.1

Core types for the iot-protocols SDK: Thing model, IotClient trait, errors, paths, protocol bindings.
Documentation
//! Identifiers and aggregates for the Eclipse-Ditto-style Thing model.
//!
//! A [`Thing`] is the canonical digital twin: a top-level entity (`ThingId`)
//! holds free-form `attributes`, plus zero or more named `Feature`s. Each
//! `Feature` is a typed bag of `properties` and `desired_properties` (the
//! desired-state pattern from device twins).
//!
//! Every protocol in the SDK ultimately reads or writes a `Property`
//! identified by a [`crate::path::PropertyPath`] = `(ThingId, FeatureId,
//! PropertyId)`. The mapping from that triple to a Modbus register / MQTT
//! topic / CoAP URI lives in [`crate::binding::ThingMapping`].

#[cfg(feature = "alloc")]
use alloc::{collections::BTreeMap, vec::Vec};

use smol_str::SmolStr;

use crate::value::Value;

/// Globally unique identifier of a Thing — `namespace:name`.
///
/// The format mirrors Eclipse Ditto: a non-empty namespace, a colon, and a
/// non-empty name. We do not enforce the regex at construction time (it
/// would force allocations on the failure path); use [`ThingId::is_valid`]
/// when accepting external input.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(transparent))]
pub struct ThingId(pub SmolStr);

impl ThingId {
    /// Construct from any string-like input.
    pub fn new(s: impl Into<SmolStr>) -> Self {
        Self(s.into())
    }

    /// Borrow the underlying `&str`.
    pub fn as_str(&self) -> &str {
        self.0.as_str()
    }

    /// Return `(namespace, name)` if the id has the canonical shape.
    pub fn split(&self) -> Option<(&str, &str)> {
        let (ns, name) = self.0.as_str().split_once(':')?;
        if ns.is_empty() || name.is_empty() {
            return None;
        }
        Some((ns, name))
    }

    /// Lightweight syntactic check — *not* a full Ditto regex match.
    pub fn is_valid(&self) -> bool {
        self.split().is_some()
    }
}

/// Identifier of a Feature inside a Thing.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(transparent))]
pub struct FeatureId(pub SmolStr);

impl FeatureId {
    /// Construct.
    pub fn new(s: impl Into<SmolStr>) -> Self {
        Self(s.into())
    }
    /// Borrow.
    pub fn as_str(&self) -> &str {
        self.0.as_str()
    }
}

/// Identifier of a Property inside a Feature.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(transparent))]
pub struct PropertyId(pub SmolStr);

impl PropertyId {
    /// Construct.
    pub fn new(s: impl Into<SmolStr>) -> Self {
        Self(s.into())
    }
    /// Borrow.
    pub fn as_str(&self) -> &str {
        self.0.as_str()
    }
}

/// Optional access-policy identifier — referenced by `Thing.policy_id`.
///
/// The SDK does not interpret policies; it only round-trips the id so it
/// can be re-emitted to the cloud side.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(transparent))]
pub struct PolicyId(pub SmolStr);

impl PolicyId {
    /// Construct.
    pub fn new(s: impl Into<SmolStr>) -> Self {
        Self(s.into())
    }
    /// Borrow.
    pub fn as_str(&self) -> &str {
        self.0.as_str()
    }
}

/// Reference to a Vorto / W3C-WoT Thing-Description definition.
///
/// Stored verbatim; the SDK never resolves it.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(transparent))]
pub struct DefinitionIdentifier(pub SmolStr);

impl DefinitionIdentifier {
    /// Construct.
    pub fn new(s: impl Into<SmolStr>) -> Self {
        Self(s.into())
    }
    /// Borrow.
    pub fn as_str(&self) -> &str {
        self.0.as_str()
    }
}

/// A Feature — a named, typed bag of properties on a Thing.
///
/// `properties` is the *reported* state (what the device says it is);
/// `desired_properties` is the cloud-driven target state — twin pattern.
#[cfg(feature = "alloc")]
#[derive(Debug, Clone, PartialEq, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Feature {
    /// Definitions implemented by this feature, in priority order.
    #[cfg_attr(
        feature = "serde",
        serde(default, skip_serializing_if = "Vec::is_empty")
    )]
    pub definition: Vec<DefinitionIdentifier>,
    /// Reported state.
    #[cfg_attr(
        feature = "serde",
        serde(default, skip_serializing_if = "BTreeMap::is_empty")
    )]
    pub properties: BTreeMap<PropertyId, Value>,
    /// Desired state (target).
    #[cfg_attr(
        feature = "serde",
        serde(
            default,
            skip_serializing_if = "BTreeMap::is_empty",
            rename = "desiredProperties"
        )
    )]
    pub desired_properties: BTreeMap<PropertyId, Value>,
}

#[cfg(feature = "alloc")]
impl Feature {
    /// Empty feature — no definition, no properties.
    pub fn new() -> Self {
        Self::default()
    }

    /// Set or overwrite a reported property.
    pub fn with_property(mut self, id: PropertyId, value: Value) -> Self {
        self.properties.insert(id, value);
        self
    }

    /// Look up a reported property.
    pub fn property(&self, id: &PropertyId) -> Option<&Value> {
        self.properties.get(id)
    }
}

/// A Thing — the top-level digital twin.
#[cfg(feature = "alloc")]
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Thing {
    /// Globally unique id, `namespace:name`.
    #[cfg_attr(feature = "serde", serde(rename = "thingId"))]
    pub thing_id: ThingId,
    /// Optional access policy reference.
    #[cfg_attr(
        feature = "serde",
        serde(default, skip_serializing_if = "Option::is_none", rename = "policyId")
    )]
    pub policy_id: Option<PolicyId>,
    /// Free-form attributes (manufacturer, location, …).
    #[cfg_attr(
        feature = "serde",
        serde(default, skip_serializing_if = "BTreeMap::is_empty")
    )]
    pub attributes: BTreeMap<SmolStr, Value>,
    /// Features by id.
    #[cfg_attr(
        feature = "serde",
        serde(default, skip_serializing_if = "BTreeMap::is_empty")
    )]
    pub features: BTreeMap<FeatureId, Feature>,
}

#[cfg(feature = "alloc")]
impl Thing {
    /// Construct an empty Thing — only the id is required.
    pub fn new(thing_id: ThingId) -> Self {
        Self {
            thing_id,
            policy_id: None,
            attributes: BTreeMap::new(),
            features: BTreeMap::new(),
        }
    }

    /// Insert or replace a feature.
    pub fn with_feature(mut self, id: FeatureId, feature: Feature) -> Self {
        self.features.insert(id, feature);
        self
    }

    /// Borrow a feature by id.
    pub fn feature(&self, id: &FeatureId) -> Option<&Feature> {
        self.features.get(id)
    }

    /// Mutably borrow a feature by id, creating it if absent.
    pub fn feature_mut_or_default(&mut self, id: FeatureId) -> &mut Feature {
        self.features.entry(id).or_default()
    }
}

#[cfg(test)]
#[cfg(feature = "alloc")]
mod tests {
    use super::*;

    #[test]
    fn thing_id_split() {
        let id = ThingId::new("acme:pump-7");
        assert!(id.is_valid());
        assert_eq!(id.split(), Some(("acme", "pump-7")));

        let bad = ThingId::new("no-colon");
        assert!(!bad.is_valid());
    }

    #[test]
    fn build_thing() {
        let t = Thing::new(ThingId::new("acme:pump-7")).with_feature(
            FeatureId::new("temperature"),
            Feature::new().with_property(PropertyId::new("value"), 23.5.into()),
        );

        assert_eq!(
            t.feature(&FeatureId::new("temperature"))
                .and_then(|f| f.property(&PropertyId::new("value")))
                .and_then(Value::as_float),
            Some(23.5)
        );
    }
}