diff --git a/server/src/steel/handlers.rs b/server/src/steel/handlers.rs index 7a1c667..2e1c6a4 100644 --- a/server/src/steel/handlers.rs +++ b/server/src/steel/handlers.rs @@ -1 +1,4 @@ // src/steel/handlers.rs +pub mod execution; + +pub use execution::*; diff --git a/server/src/steel/handlers/execution.rs b/server/src/steel/handlers/execution.rs new file mode 100644 index 0000000..0981d98 --- /dev/null +++ b/server/src/steel/handlers/execution.rs @@ -0,0 +1,66 @@ +// src/steel/execution.rs +use std::fmt; + +#[derive(Debug)] +pub enum ScriptOperation { + SetToColumn { source: String }, + // Future operations can be added here +} + +#[derive(Debug)] +pub enum ScriptExecutionError { + ParseError(String), + MissingSourceColumn(String), + Mismatch { expected: String, actual: String }, +} + +impl fmt::Display for ScriptExecutionError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::ParseError(msg) => write!(f, "Parse error: {}", msg), + Self::MissingSourceColumn(col) => write!(f, "Missing source column: {}", col), + Self::Mismatch { expected, actual } => write!( + f, + "Value does not match script expectation. Expected: {}, Actual: {}", + expected, actual + ), + } + } +} + +pub fn parse_script(script: &str, expected_target: &str) -> Result { + let script = script.trim(); + + if !script.starts_with("(set! ") || !script.ends_with(')') { + return Err(ScriptExecutionError::ParseError( + "Script must be in the form (set! target source)".to_string(), + )); + } + + let content = &script[6..script.len() - 1].trim(); + let mut parts = content.split_whitespace(); + + let target = parts.next().ok_or_else(|| { + ScriptExecutionError::ParseError("Missing target in set! expression".to_string()) + })?; + let source = parts.next().ok_or_else(|| { + ScriptExecutionError::ParseError("Missing source in set! expression".to_string()) + })?; + + if parts.next().is_some() { + return Err(ScriptExecutionError::ParseError( + "Too many arguments in set! expression".to_string(), + )); + } + + if target != expected_target { + return Err(ScriptExecutionError::ParseError(format!( + "Script target '{}' does not match expected '{}'", + target, expected_target + ))); + } + + Ok(ScriptOperation::SetToColumn { + source: source.to_string(), + }) +} diff --git a/server/src/table_script/handlers/post_table_script.rs b/server/src/table_script/handlers/post_table_script.rs index 1fecff6..d10e92f 100644 --- a/server/src/table_script/handlers/post_table_script.rs +++ b/server/src/table_script/handlers/post_table_script.rs @@ -5,6 +5,10 @@ use common::proto::multieko2::table_script::{PostTableScriptRequest, TableScript use crate::steel::validation::script::validate_script; use serde_json::Value; +// Add these imports for the execution module and ScriptOperation +use crate::steel::handlers::execution; +use crate::steel::handlers::ScriptOperation; + const SYSTEM_COLUMNS: &[&str] = &["id", "deleted", "created_at"]; fn validate_target_column( @@ -51,6 +55,15 @@ pub async fn post_table_script( .map_err(|e| Status::internal(format!("Database error: {}", e)))? .ok_or_else(|| Status::not_found("Table definition not found"))?; + // Use the full path to parse_script + let operation = execution::parse_script(&request.script, &request.target_column) + .map_err(|e| Status::invalid_argument(e.to_string()))?; + + // Ensure the operation is valid (additional checks if needed) + match operation { + ScriptOperation::SetToColumn { .. } => {}, // Use directly without 'execution::' + } + // Call validation functions validate_script(&request.script) .map_err(|e| Status::invalid_argument(e.to_string()))?; diff --git a/server/src/tables_data/handlers/post_table_data.rs b/server/src/tables_data/handlers/post_table_data.rs index c4a88cc..41a04e5 100644 --- a/server/src/tables_data/handlers/post_table_data.rs +++ b/server/src/tables_data/handlers/post_table_data.rs @@ -4,6 +4,7 @@ use sqlx::{PgPool, Arguments}; use sqlx::postgres::PgArguments; use chrono::{DateTime, Utc}; use common::proto::multieko2::tables_data::{PostTableDataRequest, PostTableDataResponse}; +use crate::steel::handlers::execution::{self, ScriptOperation}; use std::collections::HashMap; pub async fn post_table_data( @@ -100,6 +101,56 @@ pub async fn post_table_data( } } + + // Validate Steel scripts + let scripts = sqlx::query!( + "SELECT target_column, script FROM table_scripts WHERE table_definitions_id = $1", + table_def.id + ) + .fetch_all(db_pool) + .await + .map_err(|e| Status::internal(format!("Failed to fetch scripts: {}", e)))?; + + for script_record in scripts { + let target_column = script_record.target_column; + + // Check if target column is present in data + if !data.contains_key(&target_column) { + return Err(Status::invalid_argument( + format!("Column '{}' is required due to an associated script", target_column) + )); + } + + // Parse the script + let operation = execution::parse_script(&script_record.script, &target_column) + .map_err(|e| Status::invalid_argument(e.to_string()))?; + + // Get source column from operation + let source_column = match operation { + ScriptOperation::SetToColumn { source } => source, + }; + + // Check source column presence + let source_value = data.get(&source_column) + .ok_or_else(|| Status::invalid_argument( + format!("Source column '{}' required by script for '{}' is missing", source_column, target_column) + ))?; + + // Get target value + let target_value = data.get(&target_column) + .ok_or_else(|| Status::invalid_argument( + format!("Target column '{}' is missing in data", target_column) + ))?; + + // Validate value match + if target_value != source_value { + return Err(Status::invalid_argument( + format!("Value for '{}' must match '{}' as per script. Expected '{}', got '{}'", + target_column, source_column, source_value, target_value) + )); + } + } + // Prepare SQL parameters let mut params = PgArguments::default(); let mut columns_list = Vec::new();