lirays 0.1.2

Rust client for LiRAYS SCADA over WebSocket + Protobuf
Documentation
use lirays_scada_proto::namespace::v1::{
    NamespaceFolder, NamespaceNode, NamespaceSchema, NamespaceVariable, VarDataType, namespace_node,
};

use crate::types::errors::ClientError;

/// Parses JSON text into a protobuf `NamespaceSchema` used by bulk creation APIs.
///
/// Accepted shapes:
/// - nested objects for folders,
/// - string leaf values (`"int"`, `"float"`, `"text"`, `"boolean"`),
/// - object leaf values under `{"variable": {...}}` for full metadata.
pub(crate) fn namespace_schema_from_json(json: &str) -> Result<NamespaceSchema, ClientError> {
    let value: serde_json::Value =
        serde_json::from_str(json).map_err(|_| ClientError::InvalidInput("invalid JSON"))?;
    let roots_obj = value
        .as_object()
        .ok_or(ClientError::InvalidInput("root JSON must be an object"))?;

    let mut roots = std::collections::HashMap::new();
    for (key, val) in roots_obj {
        roots.insert(key.clone(), build_namespace_node(val)?);
    }

    Ok(NamespaceSchema { roots })
}

/// Recursively converts one JSON value into a schema node.
fn build_namespace_node(value: &serde_json::Value) -> Result<NamespaceNode, ClientError> {
    if let Some(obj) = value.as_object() {
        if let Some(var_val) = obj.get("variable") {
            return Ok(NamespaceNode {
                node: Some(namespace_node::Node::Variable(build_namespace_variable(
                    var_val,
                )?)),
            });
        }

        let mut children = std::collections::HashMap::new();
        for (k, v) in obj {
            children.insert(k.clone(), build_namespace_node(v)?);
        }

        return Ok(NamespaceNode {
            node: Some(namespace_node::Node::Folder(NamespaceFolder { children })),
        });
    }

    if let Some(dtype) = value.as_str() {
        return Ok(NamespaceNode {
            node: Some(namespace_node::Node::Variable(NamespaceVariable {
                var_d_type: string_to_dtype(dtype) as i32,
                unit: None,
                min: None,
                max: None,
                options: vec![],
                max_len: None,
            })),
        });
    }

    Err(ClientError::InvalidInput(
        "invalid namespace JSON structure",
    ))
}

/// Converts a `{"variable": {...}}` object into `NamespaceVariable`.
fn build_namespace_variable(val: &serde_json::Value) -> Result<NamespaceVariable, ClientError> {
    let obj = val
        .as_object()
        .ok_or(ClientError::InvalidInput("variable must be an object"))?;

    let dtype_str = obj
        .get("var_d_type")
        .and_then(|v| v.as_str())
        .ok_or(ClientError::InvalidInput("variable.var_d_type missing"))?;

    Ok(NamespaceVariable {
        var_d_type: string_to_dtype(dtype_str) as i32,
        unit: obj
            .get("unit")
            .and_then(|v| v.as_str())
            .map(|s| s.to_string()),
        min: obj.get("min").and_then(|v| v.as_f64()),
        max: obj.get("max").and_then(|v| v.as_f64()),
        options: obj
            .get("options")
            .and_then(|v| v.as_array())
            .map(|arr| {
                arr.iter()
                    .filter_map(|x| x.as_str().map(|s| s.to_string()))
                    .collect()
            })
            .unwrap_or_default(),
        max_len: obj.get("max_len").and_then(|v| v.as_u64()),
    })
}

/// Maps textual data type names to protocol enum values.
fn string_to_dtype(s: &str) -> VarDataType {
    match s.to_ascii_lowercase().as_str() {
        "float" => VarDataType::Float,
        "integer" | "int" => VarDataType::Integer,
        "text" | "string" => VarDataType::Text,
        "boolean" | "bool" => VarDataType::Boolean,
        _ => VarDataType::Unspecified,
    }
}