diff --git a/src/main.rs b/src/main.rs index 9b63de3..526d7db 100644 --- a/src/main.rs +++ b/src/main.rs @@ -48,6 +48,7 @@ async fn main() -> std::io::Result<()> { // Start HTTP server HttpServer::new(move || { + let cors = Cors::default() .send_wildcard() .allowed_origin("*") @@ -55,6 +56,7 @@ async fn main() -> std::io::Result<()> { .allowed_headers(vec![header::AUTHORIZATION, header::ACCEPT]) .allowed_header(header::CONTENT_TYPE) .max_age(3600); + App::new() .wrap(cors) .app_data(app_state.clone()) diff --git a/src/services.rs b/src/services.rs index 8eb5178..79d2957 100644 --- a/src/services.rs +++ b/src/services.rs @@ -1,5 +1,5 @@ pub mod config; - +pub mod utils; pub mod state; pub mod email; pub mod keywords; diff --git a/src/services/keywords/create_draft.rs b/src/services/keywords/create_draft.rs new file mode 100644 index 0000000..106e482 --- /dev/null +++ b/src/services/keywords/create_draft.rs @@ -0,0 +1,32 @@ +use rhai::Dynamic; +use rhai::Engine; +use serde_json::json; + +use crate::services::state::AppState; + +pub fn create_draft_keyword(_state: &AppState, engine: &mut Engine) { + engine + .register_custom_syntax( + &["CREATE", "DRAFT", "$expr$", ",", "$expr$", ",", "$expr$"], + true, // Statement + |context, inputs| { + if inputs.len() < 3 { + return Err("Not enough arguments for CREATE DRAFT".into()); + } + + let to = context.eval_expression_tree(&inputs[0])?; + let subject = context.eval_expression_tree(&inputs[1])?; + let body = context.eval_expression_tree(&inputs[2])?; + + let result = json!({ + "command": "create_draft", + "to": to.to_string(), + "subject": subject.to_string(), + "body": body.to_string() + }); + println!("CREATE DRAFT executed: {}", result.to_string()); + Ok(Dynamic::UNIT) + }, + ) + .unwrap(); +} diff --git a/src/services/keywords/create_site.rs b/src/services/keywords/create_site.rs new file mode 100644 index 0000000..826a9d7 --- /dev/null +++ b/src/services/keywords/create_site.rs @@ -0,0 +1,40 @@ +use rhai::Dynamic; +use rhai::Engine; +use serde_json::json; + +use crate::services::state::AppState; + +pub fn create_site_keyword(_state: &AppState, engine: &mut Engine) { + + engine + .register_custom_syntax( + &[ + "CREATE", "SITE", "$expr$", ",", "$expr$", ",", "$expr$", ",", "$expr$", ",", + "$expr$", + ], + true, // Statement + |context, inputs| { + if inputs.len() < 5 { + return Err("Not enough arguments for CREATE SITE".into()); + } + + let name = context.eval_expression_tree(&inputs[0])?; + let company = context.eval_expression_tree(&inputs[1])?; + let website = context.eval_expression_tree(&inputs[2])?; + let template = context.eval_expression_tree(&inputs[3])?; + let prompt = context.eval_expression_tree(&inputs[4])?; + + let result = json!({ + "command": "create_site", + "name": name.to_string(), + "company": company.to_string(), + "website": website.to_string(), + "template": template.to_string(), + "prompt": prompt.to_string() + }); + println!("CREATE SITE executed: {}", result.to_string()); + Ok(Dynamic::UNIT) + }, + ) + .unwrap(); +} diff --git a/src/services/keywords/find.rs b/src/services/keywords/find.rs index 53528c0..925c9af 100644 --- a/src/services/keywords/find.rs +++ b/src/services/keywords/find.rs @@ -1,35 +1,79 @@ +use rhai::Dynamic; +use rhai::Engine; use serde_json::{json, Value}; -use sqlx::Column; // Required for .name() method -use sqlx::TypeInfo; // Required for .type_info() method -use sqlx::{postgres::PgRow, PgPool, Row}; +use sqlx::{PgPool}; use std::error::Error; +use crate::services::state::AppState; +use crate::services::utils; +use crate::services::utils::row_to_json; +use crate::services::utils::to_array; + + +pub fn find_keyword(state: &AppState, engine: &mut Engine) { + let db = state.db_custom.clone(); + + engine + .register_custom_syntax(&["FIND", "$expr$", ",", "$expr$"], false, { + let db = db.clone(); + + move |context, inputs| { + let table_name = context.eval_expression_tree(&inputs[0])?; + let filter = context.eval_expression_tree(&inputs[1])?; + let binding = db.as_ref().unwrap(); + + // Use the current async context instead of creating a new runtime + let binding2 = table_name.to_string(); + let binding3 = filter.to_string(); + let fut = execute_find(binding, &binding2, &binding3); + + // Use tokio::task::block_in_place + tokio::runtime::Handle::current().block_on + let result = + tokio::task::block_in_place(|| tokio::runtime::Handle::current().block_on(fut)) + .map_err(|e| format!("DB error: {}", e))?; + + if let Some(results) = result.get("results") { + let array = to_array(utils::json_value_to_dynamic(results)); + Ok(Dynamic::from(array)) + } else { + Err("No results".into()) + } + } + }) + .unwrap(); +} pub async fn execute_find( pool: &PgPool, table_str: &str, filter_str: &str, -) -> Result { // Changed to String error like your Actix code - println!("Starting execute_find with table: {}, filter: {}", table_str, filter_str); - - let (where_clause, params) = parse_filter(filter_str) - .map_err(|e| e.to_string())?; - - let query = format!("SELECT * FROM {} WHERE {} LIMIT 10", table_str, where_clause); +) -> Result { + // Changed to String error like your Actix code + println!( + "Starting execute_find with table: {}, filter: {}", + table_str, filter_str + ); + + let (where_clause, params) = parse_filter(filter_str).map_err(|e| e.to_string())?; + + let query = format!( + "SELECT * FROM {} WHERE {} LIMIT 10", + table_str, where_clause + ); println!("Executing query: {}", query); - + // Use the same simple pattern as your Actix code - no timeout wrapper let rows = sqlx::query(&query) - .bind(¶ms[0]) // Simplified like your working code + .bind(¶ms[0]) // Simplified like your working code .fetch_all(pool) .await .map_err(|e| { eprintln!("SQL execution error: {}", e); e.to_string() })?; - + println!("Query successful, got {} rows", rows.len()); - + let mut results = Vec::new(); for row in rows { results.push(row_to_json(row).map_err(|e| e.to_string())?); @@ -43,96 +87,6 @@ pub async fn execute_find( })) } -fn row_to_json(row: PgRow) -> Result> { - let mut result = serde_json::Map::new(); - let columns = row.columns(); - println!("Processing {} columns", columns.len()); - - for (i, column) in columns.iter().enumerate() { - let column_name = column.name(); - let type_name = column.type_info().name(); - println!( - "Processing column {}: {} (type: {})", - i, column_name, type_name - ); - - let value: Value = match type_name { - "INT4" | "INT8" | "int4" | "int8" => match row.try_get::(i) { - Ok(v) => { - println!("Got int64 value: {}", v); - json!(v) - } - Err(e) => { - println!("Failed to get int64, trying i32: {}", e); - match row.try_get::(i) { - Ok(v) => json!(v as i64), - Err(_) => Value::Null, - } - } - }, - "FLOAT4" | "FLOAT8" | "float4" | "float8" => match row.try_get::(i) { - Ok(v) => { - println!("Got float64 value: {}", v); - json!(v) - } - Err(e) => { - println!("Failed to get float64, trying f32: {}", e); - match row.try_get::(i) { - Ok(v) => json!(v as f64), - Err(_) => Value::Null, - } - } - }, - "TEXT" | "VARCHAR" | "text" | "varchar" => match row.try_get::(i) { - Ok(v) => { - println!("Got string value: {}", v); - json!(v) - } - Err(e) => { - println!("Failed to get string: {}", e); - Value::Null - } - }, - "BOOL" | "bool" => match row.try_get::(i) { - Ok(v) => { - println!("Got bool value: {}", v); - json!(v) - } - Err(e) => { - println!("Failed to get bool: {}", e); - Value::Null - } - }, - "JSON" | "JSONB" | "json" | "jsonb" => match row.try_get::(i) { - Ok(v) => { - println!("Got JSON value: {:?}", v); - v - } - Err(e) => { - println!("Failed to get JSON, trying as string: {}", e); - match row.try_get::(i) { - Ok(s) => match serde_json::from_str(&s) { - Ok(v) => v, - Err(_) => json!(s), - }, - Err(_) => Value::Null, - } - } - }, - _ => { - println!("Unknown type {}, trying as string", type_name); - match row.try_get::(i) { - Ok(v) => json!(v), - Err(_) => Value::Null, - } - } - }; - result.insert(column_name.to_string(), value); - } - println!("Finished processing row, got {} fields", result.len()); - Ok(Value::Object(result)) -} - // Helper function to parse the filter string into SQL WHERE clause and parameters fn parse_filter(filter_str: &str) -> Result<(String, Vec), Box> { let parts: Vec<&str> = filter_str.split('=').collect(); diff --git a/src/services/keywords/for_next.rs b/src/services/keywords/for_next.rs new file mode 100644 index 0000000..5824f17 --- /dev/null +++ b/src/services/keywords/for_next.rs @@ -0,0 +1,83 @@ +use rhai::Dynamic; +use rhai::Engine; +use crate::services::state::AppState; + +pub fn for_keyword(_state: &AppState, engine: &mut Engine) { + + engine + .register_custom_syntax(&["EXIT", "FOR"], false, |_context, _inputs| { + Err("EXIT FOR".into()) + }) + .unwrap(); + + engine + .register_custom_syntax( + &[ + "FOR", "EACH", "$ident$", "IN", "$expr$", "$block$", "NEXT", "$ident$", + ], + true, // We're modifying the scope by adding the loop variable + |context, inputs| { + // Get the iterator variable names + let loop_var = inputs[0].get_string_value().unwrap(); + let next_var = inputs[3].get_string_value().unwrap(); + + // Verify variable names match + if loop_var != next_var { + return Err(format!( + "NEXT variable '{}' doesn't match FOR EACH variable '{}'", + next_var, loop_var + ) + .into()); + } + + // Evaluate the collection expression + let collection = context.eval_expression_tree(&inputs[1])?; + + // Debug: Print the collection type + println!("Collection type: {}", collection.type_name()); + let ccc = collection.clone(); + // Convert to array - with proper error handling + let array = match collection.into_array() { + Ok(arr) => arr, + Err(err) => { + return Err(format!( + "foreach expected array, got {}: {}", + ccc.type_name(), + err + ) + .into()); + } + }; + // Get the block as an expression tree + let block = &inputs[2]; + + // Remember original scope length + let orig_len = context.scope().len(); + + for item in array { + // Push the loop variable into the scope + context.scope_mut().push(loop_var, item); + + // Evaluate the block with the current scope + match context.eval_expression_tree(block) { + Ok(_) => (), + Err(e) if e.to_string() == "EXIT FOR" => { + context.scope_mut().rewind(orig_len); + break; + } + Err(e) => { + // Rewind the scope before returning error + context.scope_mut().rewind(orig_len); + return Err(e); + } + } + + // Remove the loop variable for next iteration + context.scope_mut().rewind(orig_len); + } + + Ok(Dynamic::UNIT) + }, + ) + .unwrap(); +} diff --git a/src/services/keywords/get.rs b/src/services/keywords/get.rs new file mode 100644 index 0000000..1f981a9 --- /dev/null +++ b/src/services/keywords/get.rs @@ -0,0 +1,20 @@ +use rhai::Dynamic; +use rhai::Engine; + +use crate::services::state::AppState; + +pub fn get_keyword(_state: &AppState, engine: &mut Engine) { + engine + .register_custom_syntax( + &["GET", "$expr$"], + false, // Expression, not statement + |context, inputs| { + let url = context.eval_expression_tree(&inputs[0])?; + let url_str = url.to_string(); + + println!("GET executed: {}", url_str.to_string()); + Ok(format!("Content from {}", url_str).into()) + }, + ) + .unwrap(); +} diff --git a/src/services/keywords/mod.rs b/src/services/keywords/mod.rs index f4dab31..d2e5b10 100644 --- a/src/services/keywords/mod.rs +++ b/src/services/keywords/mod.rs @@ -1 +1,7 @@ -pub mod find; \ No newline at end of file +pub mod create_draft; +pub mod create_site; +pub mod find; +pub mod for_next; +pub mod get; +pub mod print; +pub mod set; \ No newline at end of file diff --git a/src/services/keywords/print.rs b/src/services/keywords/print.rs new file mode 100644 index 0000000..2d0aa5f --- /dev/null +++ b/src/services/keywords/print.rs @@ -0,0 +1,20 @@ +use rhai::Dynamic; +use rhai::Engine; + +use crate::services::state::AppState; + +pub fn print_keyword(_state: &AppState, engine: &mut Engine) { + + // PRINT command + engine + .register_custom_syntax( + &["PRINT", "$expr$"], + true, // Statement + |context, inputs| { + let value = context.eval_expression_tree(&inputs[0])?; + println!("{}", value); + Ok(Dynamic::UNIT) + }, + ) + .unwrap(); +} diff --git a/src/services/keywords/set.rs b/src/services/keywords/set.rs new file mode 100644 index 0000000..0b9f235 --- /dev/null +++ b/src/services/keywords/set.rs @@ -0,0 +1,36 @@ +use rhai::Dynamic; +use rhai::Engine; +use serde_json::json; + +use crate::services::state::AppState; + + +pub fn set_keyword(_state: &AppState, engine: &mut Engine) { + + engine + .register_custom_syntax( + &["SET", "$expr$", ",", "$expr$", ",", "$expr$"], + true, // Statement + |context, inputs| { + let table_name = context.eval_expression_tree(&inputs[0])?; + let key_value = context.eval_expression_tree(&inputs[1])?; + let value = context.eval_expression_tree(&inputs[2])?; + + let table_str = table_name.to_string(); + let key_str = key_value.to_string(); + let value_str = value.to_string(); + + let result = json!({ + "command": "set", + "status": "success", + "table": table_str, + "key": key_str, + "value": value_str + }); + println!("SET executed: {}", result.to_string()); + Ok(Dynamic::UNIT) + }, + ) + .unwrap(); + +} diff --git a/src/services/script.rs b/src/services/script.rs index ab77f68..215b879 100644 --- a/src/services/script.rs +++ b/src/services/script.rs @@ -1,301 +1,33 @@ -use rhai::{Array, Dynamic, Engine}; -use rhai::{EvalAltResult}; -use serde_json::{json, Value}; -use smartstring::SmartString; - -use crate::services::keywords::find::execute_find; +use rhai::{ Dynamic, Engine, EvalAltResult}; +use crate::services::keywords::create_draft::{create_draft_keyword}; +use crate::services::keywords::create_site::create_site_keyword; +use crate::services::keywords::find::{find_keyword}; +use crate::services::keywords::for_next::for_keyword; +use crate::services::keywords::get::get_keyword; +use crate::services::keywords::print::print_keyword; +use crate::services::keywords::set::set_keyword; use crate::services::state::AppState; pub struct ScriptService { engine: Engine, - -} - -fn json_value_to_dynamic(value: &Value) -> Dynamic { - match value { - Value::Null => Dynamic::UNIT, - Value::Bool(b) => Dynamic::from(*b), - Value::Number(n) => { - if let Some(i) = n.as_i64() { - Dynamic::from(i) - } else if let Some(f) = n.as_f64() { - Dynamic::from(f) - } else { - Dynamic::UNIT - } - } - Value::String(s) => Dynamic::from(s.clone()), - Value::Array(arr) => Dynamic::from( - arr.iter() - .map(json_value_to_dynamic) - .collect::(), - ), - Value::Object(obj) => Dynamic::from( - obj.iter() - .map(|(k, v)| (SmartString::from(k), json_value_to_dynamic(v))) - .collect::(), - ), - } -} - -/// Converts any value to an array - single values become single-element arrays -fn to_array(value: Dynamic) -> Array { - if value.is_array() { - // Already an array - return as-is - value.cast::() - } else if value.is_unit() || value.is::<()>() { - // Handle empty/unit case - Array::new() - } else { - // Convert single value to single-element array - Array::from([value]) - } } impl ScriptService { pub fn new(state: &AppState) -> Self { let mut engine = Engine::new(); - + // Configure engine for BASIC-like syntax engine.set_allow_anonymous_fn(true); engine.set_allow_looping(true); - - engine - .register_custom_syntax( - &[ - "FOR", "EACH", "$ident$", "IN", "$expr$", "$block$", "NEXT", "$ident$", - ], - true, // We're modifying the scope by adding the loop variable - |context, inputs| { - // Get the iterator variable names - let loop_var = inputs[0].get_string_value().unwrap(); - let next_var = inputs[3].get_string_value().unwrap(); - - // Verify variable names match - if loop_var != next_var { - return Err(format!( - "NEXT variable '{}' doesn't match FOR EACH variable '{}'", - next_var, loop_var - ) - .into()); - } - - // Evaluate the collection expression - let collection = context.eval_expression_tree(&inputs[1])?; - - // Debug: Print the collection type - println!("Collection type: {}", collection.type_name()); - let ccc = collection.clone(); - // Convert to array - with proper error handling - let array = match collection.into_array() { - Ok(arr) => arr, - Err(err) => { - return Err(format!( - "foreach expected array, got {}: {}", - ccc.type_name(), - err - ) - .into()); - } - }; - // Get the block as an expression tree - let block = &inputs[2]; - - // Remember original scope length - let orig_len = context.scope().len(); - - for item in array { - // Push the loop variable into the scope - context.scope_mut().push(loop_var, item); - - // Evaluate the block with the current scope - match context.eval_expression_tree(block) { - Ok(_) => (), - Err(e) if e.to_string() == "EXIT FOR" => { - context.scope_mut().rewind(orig_len); - break; - } - Err(e) => { - // Rewind the scope before returning error - context.scope_mut().rewind(orig_len); - return Err(e); - } - } - - // Remove the loop variable for next iteration - context.scope_mut().rewind(orig_len); - } - - Ok(Dynamic::UNIT) - }, - ) - .unwrap(); - - // Register EXIT FOR - engine - .register_custom_syntax(&["EXIT", "FOR"], false, |_context, _inputs| { - Err("EXIT FOR".into()) - }) - .unwrap(); - - // FIND command: FIND "table", "filter" - // Clone the database reference outside the closure to avoid lifetime issues - let db = state.db_custom.clone(); - - engine - .register_custom_syntax(&["FIND", "$expr$", ",", "$expr$"], false, { - let db = db.clone(); - - move |context, inputs| { - let table_name = context.eval_expression_tree(&inputs[0])?; - let filter = context.eval_expression_tree(&inputs[1])?; - let binding = db.as_ref().unwrap(); - - // Use the current async context instead of creating a new runtime - let binding2 = table_name.to_string(); - let binding3 = filter.to_string(); - let fut = execute_find( - binding, - &binding2, - &binding3, - ); - - // Use tokio::task::block_in_place + tokio::runtime::Handle::current().block_on - let result = tokio::task::block_in_place(|| { - tokio::runtime::Handle::current().block_on(fut) - }) - .map_err(|e| format!("DB error: {}", e))?; - - if let Some(results) = result.get("results") { - let array = to_array(json_value_to_dynamic(results)); - Ok(Dynamic::from(array)) - } else { - Err("No results".into()) - } - } - }) - .unwrap(); - - // SET command: SET "table", "key", "value" - engine - .register_custom_syntax( - &["SET", "$expr$", ",", "$expr$", ",", "$expr$"], - true, // Statement - |context, inputs| { - let table_name = context.eval_expression_tree(&inputs[0])?; - let key_value = context.eval_expression_tree(&inputs[1])?; - let value = context.eval_expression_tree(&inputs[2])?; - - let table_str = table_name.to_string(); - let key_str = key_value.to_string(); - let value_str = value.to_string(); - - let result = json!({ - "command": "set", - "status": "success", - "table": table_str, - "key": key_str, - "value": value_str - }); - println!("SET executed: {}", result.to_string()); - Ok(Dynamic::UNIT) - }, - ) - .unwrap(); - - // GET command: GET "url" - engine - .register_custom_syntax( - &["GET", "$expr$"], - false, // Expression, not statement - |context, inputs| { - let url = context.eval_expression_tree(&inputs[0])?; - let url_str = url.to_string(); - - println!("GET executed: {}", url_str.to_string()); - Ok(format!("Content from {}", url_str).into()) - }, - ) - .unwrap(); - - // CREATE SITE command: CREATE SITE "name", "company", "website", "template", "prompt" - engine - .register_custom_syntax( - &[ - "CREATE", "SITE", "$expr$", ",", "$expr$", ",", "$expr$", ",", "$expr$", ",", - "$expr$", - ], - true, // Statement - |context, inputs| { - if inputs.len() < 5 { - return Err("Not enough arguments for CREATE SITE".into()); - } - - let name = context.eval_expression_tree(&inputs[0])?; - let company = context.eval_expression_tree(&inputs[1])?; - let website = context.eval_expression_tree(&inputs[2])?; - let template = context.eval_expression_tree(&inputs[3])?; - let prompt = context.eval_expression_tree(&inputs[4])?; - - let result = json!({ - "command": "create_site", - "name": name.to_string(), - "company": company.to_string(), - "website": website.to_string(), - "template": template.to_string(), - "prompt": prompt.to_string() - }); - println!("CREATE SITE executed: {}", result.to_string()); - Ok(Dynamic::UNIT) - }, - ) - .unwrap(); - - // CREATE DRAFT command: CREATE DRAFT "to", "subject", "body" - engine - .register_custom_syntax( - &["CREATE", "DRAFT", "$expr$", ",", "$expr$", ",", "$expr$"], - true, // Statement - |context, inputs| { - if inputs.len() < 3 { - return Err("Not enough arguments for CREATE DRAFT".into()); - } - - let to = context.eval_expression_tree(&inputs[0])?; - let subject = context.eval_expression_tree(&inputs[1])?; - let body = context.eval_expression_tree(&inputs[2])?; - - let result = json!({ - "command": "create_draft", - "to": to.to_string(), - "subject": subject.to_string(), - "body": body.to_string() - }); - println!("CREATE DRAFT executed: {}", result.to_string()); - Ok(Dynamic::UNIT) - }, - ) - .unwrap(); - - // PRINT command - engine - .register_custom_syntax( - &["PRINT", "$expr$"], - true, // Statement - |context, inputs| { - let value = context.eval_expression_tree(&inputs[0])?; - println!("{}", value); - Ok(Dynamic::UNIT) - }, - ) - .unwrap(); - - // Register web service functions - engine.register_fn("web_get", |url: &str| format!("Response from {}", url)); - - ScriptService { - engine, - } + + create_draft_keyword(state, &mut engine); + create_site_keyword(state, &mut engine); + find_keyword(state, &mut engine); + for_keyword(state, &mut engine); + get_keyword(state, &mut engine); + set_keyword(state, &mut engine); + print_keyword(state, &mut engine); + ScriptService { engine } } fn preprocess_basic_script(&self, script: &str) -> String { @@ -370,7 +102,7 @@ impl ScriptService { result.push_str(trimmed); result.push(';'); } else { - // Add semicolons only for non-BASIC statements + // Add semicolons only for BASIC statements result.push_str(trimmed); if !trimmed.ends_with(';') && !trimmed.ends_with('{') && !trimmed.ends_with('}') { result.push(';'); diff --git a/src/services/state.rs b/src/services/state.rs index 4ab156a..0b6ae8c 100644 --- a/src/services/state.rs +++ b/src/services/state.rs @@ -8,6 +8,6 @@ pub struct AppState { pub minio_client: Option, pub config: Option, pub db: Option, -pub db_custom: Option, + pub db_custom: Option, } diff --git a/src/services/utils.rs b/src/services/utils.rs new file mode 100644 index 0000000..10fe0b3 --- /dev/null +++ b/src/services/utils.rs @@ -0,0 +1,141 @@ +use smartstring::SmartString; +use rhai::{Array, Dynamic}; +use serde_json::{json, Value}; +use sqlx::Column; // Required for .name() method +use sqlx::TypeInfo; // Required for .type_info() method +use sqlx::{postgres::PgRow, Row}; + +use std::error::Error; + +pub fn row_to_json(row: PgRow) -> Result> { + let mut result = serde_json::Map::new(); + let columns = row.columns(); + println!("Processing {} columns", columns.len()); + + for (i, column) in columns.iter().enumerate() { + let column_name = column.name(); + let type_name = column.type_info().name(); + println!( + "Processing column {}: {} (type: {})", + i, column_name, type_name + ); + + let value: Value = match type_name { + "INT4" | "INT8" | "int4" | "int8" => match row.try_get::(i) { + Ok(v) => { + println!("Got int64 value: {}", v); + json!(v) + } + Err(e) => { + println!("Failed to get int64, trying i32: {}", e); + match row.try_get::(i) { + Ok(v) => json!(v as i64), + Err(_) => Value::Null, + } + } + }, + "FLOAT4" | "FLOAT8" | "float4" | "float8" => match row.try_get::(i) { + Ok(v) => { + println!("Got float64 value: {}", v); + json!(v) + } + Err(e) => { + println!("Failed to get float64, trying f32: {}", e); + match row.try_get::(i) { + Ok(v) => json!(v as f64), + Err(_) => Value::Null, + } + } + }, + "TEXT" | "VARCHAR" | "text" | "varchar" => match row.try_get::(i) { + Ok(v) => { + println!("Got string value: {}", v); + json!(v) + } + Err(e) => { + println!("Failed to get string: {}", e); + Value::Null + } + }, + "BOOL" | "bool" => match row.try_get::(i) { + Ok(v) => { + println!("Got bool value: {}", v); + json!(v) + } + Err(e) => { + println!("Failed to get bool: {}", e); + Value::Null + } + }, + "JSON" | "JSONB" | "json" | "jsonb" => match row.try_get::(i) { + Ok(v) => { + println!("Got JSON value: {:?}", v); + v + } + Err(e) => { + println!("Failed to get JSON, trying as string: {}", e); + match row.try_get::(i) { + Ok(s) => match serde_json::from_str(&s) { + Ok(v) => v, + Err(_) => json!(s), + }, + Err(_) => Value::Null, + } + } + }, + _ => { + println!("Unknown type {}, trying as string", type_name); + match row.try_get::(i) { + Ok(v) => json!(v), + Err(_) => Value::Null, + } + } + }; + result.insert(column_name.to_string(), value); + } + println!("Finished processing row, got {} fields", result.len()); + Ok(Value::Object(result)) +} + + + +pub fn json_value_to_dynamic(value: &Value) -> Dynamic { + match value { + Value::Null => Dynamic::UNIT, + Value::Bool(b) => Dynamic::from(*b), + Value::Number(n) => { + if let Some(i) = n.as_i64() { + Dynamic::from(i) + } else if let Some(f) = n.as_f64() { + Dynamic::from(f) + } else { + Dynamic::UNIT + } + } + Value::String(s) => Dynamic::from(s.clone()), + Value::Array(arr) => Dynamic::from( + arr.iter() + .map(json_value_to_dynamic) + .collect::(), + ), + Value::Object(obj) => Dynamic::from( + obj.iter() + .map(|(k, v)| (SmartString::from(k), json_value_to_dynamic(v))) + .collect::(), + ), + } +} + +/// Converts any value to an array - single values become single-element arrays +pub fn to_array(value: Dynamic) -> Array { + if value.is_array() { + // Already an array - return as-is + value.cast::() + } else if value.is_unit() || value.is::<()>() { + // Handle empty/unit case + Array::new() + } else { + // Convert single value to single-element array + Array::from([value]) + } +}