robomotion 0.1.3

Official Rust SDK for building Robomotion RPA packages
Documentation
//! Strongly-typed variable system for node inputs and outputs.

use crate::message::Context;
use crate::runtime::{client, lmo, Result, RobomotionError};
use serde::{de::DeserializeOwned, Deserialize, Serialize};
use serde_json::Value;
use std::marker::PhantomData;

/// Base variable configuration.
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct VariableConfig {
    #[serde(default)]
    pub scope: String,
    #[serde(default)]
    pub name: Value, // Can be string or object
}

/// Input variable for reading values from the message context.
///
/// # Example
/// ```ignore
/// #[input(title = "Name", var_type = "string", scope = "Message", name = "name")]
/// in_name: InVariable<String>,
/// ```
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct InVariable<T> {
    #[serde(flatten)]
    pub config: VariableConfig,
    #[serde(skip)]
    _marker: PhantomData<T>,
}

/// Output variable for writing values to the message context.
///
/// # Example
/// ```ignore
/// #[output(title = "Result", var_type = "string", scope = "Message", name = "result")]
/// out_result: OutVariable<String>,
/// ```
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct OutVariable<T> {
    #[serde(flatten)]
    pub config: VariableConfig,
    #[serde(skip)]
    _marker: PhantomData<T>,
}

/// Optional input variable that may not have a value.
///
/// Works like InVariable but returns Option<T> instead of requiring a value.
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct OptVariable<T> {
    #[serde(flatten)]
    pub inner: InVariable<T>,
}

impl<T> InVariable<T>
where
    T: DeserializeOwned + Default + Clone,
{
    /// Create a new InVariable with the given configuration.
    pub fn new(scope: impl Into<String>, name: impl Into<Value>) -> Self {
        Self {
            config: VariableConfig {
                scope: scope.into(),
                name: name.into(),
            },
            _marker: PhantomData,
        }
    }

    /// Check if the variable has no name configured.
    pub fn is_nil(&self) -> bool {
        self.config.name.is_null()
    }

    /// Get the value from the context.
    pub async fn get(&self, ctx: &Context) -> Result<T> {
        let scope = &self.config.scope;

        // Handle Custom scope - value is in the name field
        if scope == "Custom" {
            let value = &self.config.name;
            if value.is_null() {
                return Ok(T::default());
            }
            return convert_value::<T>(value.clone());
        }

        // Handle Message or AI scope - value is in the context
        if scope == "Message" || scope == "AI" {
            let name = match &self.config.name {
                Value::String(s) => s.clone(),
                _ => return Err(RobomotionError::Variable("Invalid variable name".to_string())),
            };

            let mut value = ctx.get(&name);

            // For AI scope, check __parameters__ if not found at top level
            if value.is_none() && scope == "AI" {
                if let Some(msg_type) = ctx.get("__message_type__") {
                    if msg_type.as_str() == Some("tool_request") {
                        if let Some(params) = ctx.get("__parameters__") {
                            if let Some(obj) = params.as_object() {
                                value = obj.get(&name).cloned();
                            }
                        }
                    }
                }
            }

            match value {
                Some(v) => {
                    // Check if this is an LMO
                    if lmo::is_lmo(&v) {
                        let lmo_obj = lmo::deserialize_lmo_from_map(
                            v.as_object()
                                .ok_or_else(|| RobomotionError::Lmo("Invalid LMO".to_string()))?,
                        )
                        .await?;
                        return convert_value::<T>(
                            lmo_obj.data.unwrap_or(Value::Null),
                        );
                    }
                    convert_value::<T>(v)
                }
                None => Ok(T::default()),
            }
        } else {
            // Other scopes - use runtime client
            let name = match &self.config.name {
                Value::String(s) => s.clone(),
                _ => return Err(RobomotionError::Variable("Invalid variable name".to_string())),
            };

            let payload = ctx.get_raw()?;
            let value = client::get_variable(scope, &name, &payload).await?;

            // Check if this is an LMO
            if lmo::is_lmo(&value) {
                let lmo_obj = lmo::deserialize_lmo_from_map(
                    value
                        .as_object()
                        .ok_or_else(|| RobomotionError::Lmo("Invalid LMO".to_string()))?,
                )
                .await?;
                return convert_value::<T>(lmo_obj.data.unwrap_or(Value::Null));
            }

            convert_value::<T>(value)
        }
    }
}

impl<T> OutVariable<T>
where
    T: Serialize,
{
    /// Set the value in the context.
    pub async fn set(&self, ctx: &Context, value: T) -> Result<()> {
        let scope = &self.config.scope;

        let name = match &self.config.name {
            Value::String(s) if !s.is_empty() => s.clone(),
            _ => return Err(RobomotionError::Variable("Empty message object".to_string())),
        };

        // Handle Message or AI scope
        if scope == "Message" || scope == "AI" {
            // Check if we need to serialize as LMO
            if client::is_lmo_capable().await {
                if let Some(lmo) = lmo::serialize_lmo(&value).await? {
                    ctx.set(&name, serde_json::to_value(&lmo)?)?;
                    return Ok(());
                }
            }
            ctx.set(&name, serde_json::to_value(&value)?)?;
            Ok(())
        } else {
            // Other scopes - use runtime client
            let serialized = serde_json::to_value(&value)?;

            // Check if we need to serialize as LMO
            if client::is_lmo_capable().await {
                if let Some(lmo) = lmo::serialize_lmo(&value).await? {
                    return client::set_variable(scope, &name, serde_json::to_value(&lmo)?).await;
                }
            }

            client::set_variable(scope, &name, serialized).await
        }
    }
}

impl<T> OptVariable<T>
where
    T: DeserializeOwned + Default + Clone,
{
    /// Check if the variable has no name configured.
    pub fn is_nil(&self) -> bool {
        self.inner.is_nil()
    }

    /// Get the value from the context, returning None if not present.
    pub async fn get(&self, ctx: &Context) -> Result<Option<T>> {
        if self.is_nil() {
            return Ok(None);
        }
        self.inner.get(ctx).await.map(Some)
    }
}

/// Convert a JSON value to the target type.
fn convert_value<T: DeserializeOwned + Default>(value: Value) -> Result<T> {
    // Handle primitive types with better conversion
    match &value {
        Value::Null => Ok(T::default()),
        Value::String(s) => {
            // Try to parse as the target type
            if let Ok(result) = serde_json::from_value::<T>(value.clone()) {
                return Ok(result);
            }
            // For string targets, return as-is
            serde_json::from_value(Value::String(s.clone())).map_err(|e| e.into())
        }
        Value::Number(n) => {
            // Numbers might need conversion
            if let Ok(result) = serde_json::from_value::<T>(value.clone()) {
                return Ok(result);
            }
            // Try converting to string first
            serde_json::from_value(Value::String(n.to_string())).map_err(|e| e.into())
        }
        Value::Bool(b) => {
            if let Ok(result) = serde_json::from_value::<T>(value.clone()) {
                return Ok(result);
            }
            serde_json::from_value(Value::String(b.to_string())).map_err(|e| e.into())
        }
        _ => serde_json::from_value(value).map_err(|e| e.into()),
    }
}