shakemyleg 3.0.0

A simple state machine definition language and interpreter.
Documentation
use json::JsonValue;
use itertools::{Itertools, Position};

use crate::error::{SML_Error, SML_Result};
use crate::value::Value;


#[derive(Clone, Debug)]
pub enum IdentifierStore {
    Inputs,
    Outputs,
    Globals
}

#[derive(Clone, Debug)]
pub struct Identifier {
    store: IdentifierStore,
    name: String,
    path: Vec<String>,
}

impl Identifier {
    pub fn from_str(s: String) -> SML_Result<Self> {
        let parts: Vec<_> = s.split(".").collect();
        if parts.len() < 2 {
            return Err(SML_Error::SyntaxError(format!("Identifier must specify store location i.e., start with \"inputs.\", \"globals.\", or \"outputs.\". (Note full-stops.) Got {s:?}")));
        }

        let store = match parts[0] {
            "inputs" => IdentifierStore::Inputs,
            "outputs" => IdentifierStore::Outputs,
            "globals" => IdentifierStore::Globals,
            _ => {
                return Err(SML_Error::SyntaxError(format!("Identifier must specify store location i.e., start with \"inputs.\", \"globals.\", or \"outputs.\". Got: {:?}", parts[0])));
            }
        };

        let path: Vec<_> = parts[1..].iter().map(|s| { s.to_string() } ).collect();
        let name = path.join(".");

        Ok(Self { name, path, store })
    }

    pub fn get(&self, i: &JsonValue, o: &JsonValue, g: &JsonValue) -> SML_Result<Value> {
        let mut store = match self.store {
            IdentifierStore::Inputs => i,
            IdentifierStore::Outputs => o,
            IdentifierStore::Globals => g,
        };

        for node in &self.path {
            if !store.is_object() || !store.has_key(node) {
                return Err(SML_Error::IdentifierNameError(self.name.clone()));
            }

            store = &store[node];
        }

        let value = Value::new(store)?;

        Ok(value)
    }

    pub fn set(&self, o: &mut JsonValue, g: &mut JsonValue, v: &Value) -> SML_Result<()> {
        let mut store = match self.store {
            IdentifierStore::Inputs => { return Err(SML_Error::InputsWriteError); },
            IdentifierStore::Outputs => o,
            IdentifierStore::Globals => g,
        };

        let mut key = None;
        for (pos, node) in self.path.iter().with_position() {
            if let Position::Last | Position::Only = pos {
                key = Some(node);
                break;
            }
            if !store.is_object() {
                if store.has_key(node) {
                    return Err(SML_Error::IdentifierError(format!("Cannot set sub-value of non-object. Identifier \"{}\" collides with another variable.", self.name)));
                }
                else {
                    store[node] = json::object! { };
                }
            }

            store = &mut store[node];
        }

        let json_value = v.as_json();
        store[key.unwrap()] = json_value;

        Ok(())
    }
}

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

    #[test]
    fn test_store_set() {
        let mut g = json::object! { };
        let mut o = json::object! { };
        let ident = Identifier::from_str("outputs.foo.bar".to_string()).unwrap();
        let v = Value::Number(1.0);
        ident.set(&mut o, &mut g, &v).unwrap();

        assert!(g.is_empty());
        assert!(!o.is_empty());
        assert!(o.has_key("foo"));
        assert!(o["foo"].has_key("bar"));
        assert!(matches!(o["foo"]["bar"], JsonValue::Number(_)));
    }
}