modio-mqttbridge 0.6.1

Modio on-device MQTT bridge
// Author: D.S. Ljungmark <spider@skuggor.se>, Modio AB
// SPDX-License-Identifier: AGPL-3.0-or-later
use serde::Deserialize;
mod values;

use values::{ExpectedValues, Values};

#[derive(Debug, thiserror::Error)]
/// Our error only sums `uuid` and `serde_json` errors
pub enum Error {
    #[error("Invalid token")]
    Token(#[from] uuid::Error),
    #[error("Invalid JSON data")]
    Json(#[from] serde_json::error::Error),
}

/// A Transaction coming from json to the internal dbus interface type
#[derive(Debug, Deserialize)]
pub struct Transaction {
    pub n: String,
    expected: ExpectedValues,
    #[serde(flatten)]
    v: Values,
    pub token: String,
}

impl Transaction {
    /// Create a new Transaction object from raw bytes
    pub fn from_bytes(data: &[u8]) -> Result<Transaction, Error> {
        let res: Transaction = serde_json::de::from_slice(data)?;
        Self::validate_token(&res.token)?;
        Ok(res)
    }

    /// Internal helper, validate a token
    fn validate_token(token: &str) -> Result<(), Error> {
        uuid::Uuid::parse_str(token)?;
        Ok(())
    }

    /// Clone the value out of this object
    pub fn value(&self) -> String {
        self.v.to_string()
    }

    /// Clone the expected data out of this object
    pub fn expected(&self) -> String {
        self.expected.to_string()
    }
}

#[cfg(test)]
mod test {
    use super::Transaction;

    type TestResult = Result<(), Box<dyn std::error::Error>>;

    #[test]
    fn good_parsing_json_values() -> TestResult {
        let various = vec![
            r#"{"n":"test.test","expected":"0","token":"6d27d3dd-a409-43a4-9905-9951211d5058", "v":321}"#,
            r#"{"n":"test.test","expected":"","token":"6d27d3dd-a409-43a4-9905-9951211d5058", "vs":"abc1234"}"#,
            r#"{"n":"test.test","expected":true,"token":"6d27d3dd-a409-43a4-9905-9951211d5058", "vb":false}"#,
            r#"{"n":"test.test","expected":false,"token":"6d27d3dd-a409-43a4-9905-9951211d5058", "vb":true}"#,
            r#"{"n":"test.test","expected":1234.01,"token":"6d27d3dd-a409-43a4-9905-9951211d5058", "vs":"true"}"#,
            r#"{"n":"test.test","expected":"1234","token":"6d27d3dd-a409-43a4-9905-9951211d5058", "v":1230.000}"#,
        ];
        for input in various {
            Transaction::from_bytes(input.as_bytes())?;
        }
        Ok(())
    }

    #[test]
    fn good_transaction_uuid() -> TestResult {
        let input = r#"{"n":"test.test","expected":"abc123","v":321,"token":"6d27d3dd-a409-43a4-9905-9951211d5058"}"#;
        Transaction::from_bytes(input.as_bytes())?;
        Ok(())
    }

    #[test]
    fn good_transaction_uuid_short() -> TestResult {
        let input = r#"{"n":"test.test","expected":"abc123","v":321,"token":"6d27d3dda40943a499059951211d5058"}"#;
        Transaction::from_bytes(input.as_bytes())?;
        Ok(())
    }

    #[test]
    /// The on-device code and modio code can use this. But we dont allow it from MQTT as we would
    /// like to transition out of it.
    fn bad_transaction_date() {
        let input = r#"{"n":"test.test","expected":"abc123","v":321,"token":"1637163026"}"#;
        Transaction::from_bytes(input.as_bytes()).unwrap_err();
    }

    #[test]
    fn bad_transaction_alphabetic() {
        let input = r#"{"n":"test.test","expected":"abc123","v":321,"token":"test-test"}"#;
        Transaction::from_bytes(input.as_bytes()).unwrap_err();
    }

    #[test]
    fn bad_transaction_spaces() {
        let input = r#"{"n":"test.test","expected":"abc123","v":321,"token":"   6d27d3dda40943a499059951211d5058"}"#;
        Transaction::from_bytes(input.as_bytes()).unwrap_err();
    }
}