convex_typegen/
convex.rs

1use std::collections::{BTreeMap, HashMap};
2use std::path::PathBuf;
3
4use convex::Value as ConvexValue;
5use oxc::allocator::Allocator;
6use oxc::diagnostics::OxcDiagnostic;
7use oxc::parser::Parser;
8use oxc::semantic::SemanticBuilder;
9use oxc::span::SourceType;
10use serde::{Deserialize, Serialize};
11use serde_json::{json, Value as JsonValue};
12
13use crate::errors::ConvexTypeGeneratorError;
14
15/// The convex schema.
16///
17/// A schema can contain many tables. https://docs.convex.dev/database/schemas
18#[derive(Debug, Serialize, Deserialize)]
19pub(crate) struct ConvexSchema
20{
21    pub(crate) tables: Vec<ConvexTable>,
22}
23
24/// A table in the convex schema.
25///
26/// A table can contain many columns.
27#[derive(Debug, Serialize, Deserialize)]
28pub(crate) struct ConvexTable
29{
30    /// The name of the table.
31    pub(crate) name: String,
32    /// The columns in the table.
33    pub(crate) columns: Vec<ConvexColumn>,
34}
35
36/// A column in the convex schema.
37#[derive(Debug, Serialize, Deserialize)]
38pub(crate) struct ConvexColumn
39{
40    /// The name of the column.
41    pub(crate) name: String,
42    /// The data type of the column.
43    /// https://docs.rs/convex/latest/convex/enum.Value.html
44    pub(crate) data_type: JsonValue,
45}
46
47/// A collection of all convex functions.
48pub(crate) type ConvexFunctions = Vec<ConvexFunction>;
49
50/// Convex functions (Queries, Mutations, and Actions)
51///
52/// https://docs.convex.dev/functions
53#[derive(Debug, Serialize, Deserialize)]
54pub(crate) struct ConvexFunction
55{
56    pub(crate) name: String,
57    pub(crate) params: Vec<ConvexFunctionParam>,
58    pub(crate) type_: String,
59    pub(crate) file_name: String,
60}
61
62/// A parameter in a convex function.
63#[derive(Debug, Serialize, Deserialize)]
64pub(crate) struct ConvexFunctionParam
65{
66    pub(crate) name: String,
67    pub(crate) data_type: JsonValue,
68}
69
70/// Creates an AST from a schema file.
71///
72/// # Arguments
73/// * `path` - Path to the schema file
74///
75/// # Errors
76/// Returns an error if:
77/// * The file cannot be read
78/// * The file contains invalid syntax
79/// * The AST cannot be generated
80pub(crate) fn create_schema_ast(path: PathBuf) -> Result<JsonValue, ConvexTypeGeneratorError>
81{
82    // Validate path exists before processing
83    if !path.exists() {
84        return Err(ConvexTypeGeneratorError::MissingSchemaFile);
85    }
86
87    generate_ast(&path)
88}
89
90/// Creates a map of all convex functions from a list of function paths.
91pub(crate) fn create_functions_ast(paths: Vec<PathBuf>) -> Result<HashMap<String, JsonValue>, ConvexTypeGeneratorError>
92{
93    let mut functions = HashMap::new();
94
95    for path in paths {
96        let function_ast = generate_ast(&path)?;
97        let path_str = path.to_string_lossy().to_string();
98        let file_name = path
99            .file_name()
100            .ok_or_else(|| ConvexTypeGeneratorError::InvalidPath(path_str.clone()))?
101            .to_str()
102            .ok_or(ConvexTypeGeneratorError::InvalidUnicode(path_str))?;
103
104        functions.insert(file_name.to_string(), function_ast);
105    }
106
107    Ok(functions)
108}
109
110pub(crate) fn parse_schema_ast(ast: JsonValue) -> Result<ConvexSchema, ConvexTypeGeneratorError>
111{
112    let context = "root";
113    // Get the body array
114    let body = ast["body"]
115        .as_array()
116        .ok_or_else(|| ConvexTypeGeneratorError::InvalidSchema {
117            context: context.to_string(),
118            details: "Missing body array".to_string(),
119        })?;
120
121    // Find the defineSchema call
122    let define_schema = find_define_schema(body).ok_or_else(|| ConvexTypeGeneratorError::InvalidSchema {
123        context: context.to_string(),
124        details: "Could not find defineSchema call".to_string(),
125    })?;
126
127    // Get the arguments array of defineSchema
128    let schema_args = define_schema["arguments"]
129        .as_array()
130        .ok_or_else(|| ConvexTypeGeneratorError::InvalidSchema {
131            context: context.to_string(),
132            details: "Missing schema arguments".to_string(),
133        })?;
134
135    // Get the first argument which is an object containing table definitions
136    let tables_obj = schema_args
137        .first()
138        .and_then(|arg| arg["properties"].as_array())
139        .ok_or_else(|| ConvexTypeGeneratorError::InvalidSchema {
140            context: context.to_string(),
141            details: "Missing table definitions".to_string(),
142        })?;
143
144    let mut tables = Vec::new();
145
146    // Iterate through each table definition
147    for table_prop in tables_obj {
148        // Get the table name
149        let table_name = table_prop["key"]["name"]
150            .as_str()
151            .ok_or_else(|| ConvexTypeGeneratorError::InvalidSchema {
152                context: context.to_string(),
153                details: "Invalid table name".to_string(),
154            })?;
155
156        // Get the defineTable call arguments
157        let define_table_args =
158            table_prop["value"]["arguments"]
159                .as_array()
160                .ok_or_else(|| ConvexTypeGeneratorError::InvalidSchema {
161                    context: context.to_string(),
162                    details: "Invalid table definition".to_string(),
163                })?;
164
165        // Get the first argument which contains column definitions
166        let columns_obj = define_table_args
167            .first()
168            .and_then(|arg| arg["properties"].as_array())
169            .ok_or_else(|| ConvexTypeGeneratorError::InvalidSchema {
170                context: context.to_string(),
171                details: "Missing column definitions".to_string(),
172            })?;
173
174        let mut columns = Vec::new();
175
176        // Iterate through each column definition
177        for column_prop in columns_obj {
178            // Get column name
179            let column_name =
180                column_prop["key"]["name"]
181                    .as_str()
182                    .ok_or_else(|| ConvexTypeGeneratorError::InvalidSchema {
183                        context: context.to_string(),
184                        details: "Invalid column name".to_string(),
185                    })?;
186
187            // Get column type by looking at the property chain
188            let mut context = TypeContext::new(context.to_string());
189            let column_type = extract_column_type(column_prop, &mut context)?;
190
191            columns.push(ConvexColumn {
192                name: column_name.to_string(),
193                data_type: column_type,
194            });
195        }
196
197        tables.push(ConvexTable {
198            name: table_name.to_string(),
199            columns,
200        });
201    }
202
203    Ok(ConvexSchema { tables })
204}
205
206/// Helper function to find the defineSchema call in the AST
207fn find_define_schema(body: &[JsonValue]) -> Option<&JsonValue>
208{
209    for node in body {
210        // Check if this is an export default declaration
211        if let Some(declaration) = node.get("declaration") {
212            // Check if this is a call expression
213            if declaration["type"].as_str() == Some("CallExpression") {
214                // Check if the callee is defineSchema
215                if let Some(callee) = declaration.get("callee") {
216                    if callee["type"].as_str() == Some("Identifier") && callee["name"].as_str() == Some("defineSchema") {
217                        return Some(declaration);
218                    }
219                }
220            }
221        }
222
223        // Could also be a regular variable declaration or expression
224        // that calls defineSchema
225        if node["type"].as_str() == Some("CallExpression") {
226            if let Some(callee) = node.get("callee") {
227                if callee["type"].as_str() == Some("Identifier") && callee["name"].as_str() == Some("defineSchema") {
228                    return Some(node);
229                }
230            }
231        }
232    }
233    None
234}
235
236/// Helper function to extract the column type from a column property
237fn extract_column_type(column_prop: &JsonValue, context: &mut TypeContext) -> Result<JsonValue, ConvexTypeGeneratorError>
238{
239    let value = &column_prop["value"];
240    let callee = &value["callee"];
241
242    let type_name = callee["property"]["name"]
243        .as_str()
244        .ok_or_else(|| ConvexTypeGeneratorError::InvalidSchema {
245            context: context.get_error_context(),
246            details: "Invalid column type".to_string(),
247        })?;
248
249    // Validate the type name
250    validate_type_name(type_name)?;
251
252    let binding = Vec::new();
253    let args = value["arguments"].as_array().unwrap_or(&binding);
254
255    let mut type_obj = serde_json::Map::new();
256    type_obj.insert("type".to_string(), JsonValue::String(type_name.to_string()));
257
258    // Handle nested types
259    match type_name {
260        "optional" => {
261            // For optional types, recursively parse the inner type
262            if let Some(inner_type) = args.first() {
263                let inner_type_prop = json!({
264                    "key": { "name": "inner" },
265                    "value": inner_type
266                });
267                context.type_path.push("inner".to_string());
268                let parsed_inner_type = extract_column_type(&inner_type_prop, context)?;
269                context.type_path.pop();
270                type_obj.insert("inner".to_string(), parsed_inner_type);
271            } else {
272                return Err(ConvexTypeGeneratorError::InvalidSchema {
273                    context: context.type_path.join("."),
274                    details: "Optional type must have an inner type".to_string(),
275                });
276            }
277        }
278        "array" => {
279            // For arrays, recursively parse the element type
280            if let Some(element_type) = args.first() {
281                let element_type_prop = json!({
282                    "key": { "name": "element" },
283                    "value": element_type
284                });
285                context.type_path.push("elements".to_string());
286                let parsed_element_type = extract_column_type(&element_type_prop, context)?;
287                context.type_path.pop();
288                type_obj.insert("elements".to_string(), parsed_element_type);
289            }
290        }
291        "object" => {
292            // For objects, parse each property type
293            if let Some(obj_def) = args.first() {
294                if let Some(properties) = obj_def["properties"].as_array() {
295                    let mut prop_types = serde_json::Map::new();
296
297                    for prop in properties {
298                        let prop_name =
299                            prop["key"]["name"]
300                                .as_str()
301                                .ok_or_else(|| ConvexTypeGeneratorError::InvalidSchema {
302                                    context: context.type_path.join("."),
303                                    details: "Invalid object property name".to_string(),
304                                })?;
305
306                        let prop_type = extract_column_type(prop, context)?;
307                        prop_types.insert(prop_name.to_string(), prop_type);
308                    }
309
310                    type_obj.insert("properties".to_string(), JsonValue::Object(prop_types));
311                }
312            }
313        }
314        "record" => {
315            // For records, parse both key and value types
316            if args.len() >= 2 {
317                // First argument is the key type
318                let key_type_prop = json!({
319                    "key": { "name": "key" },
320                    "value": args[0]
321                });
322                let key_type = extract_column_type(&key_type_prop, context)?;
323                type_obj.insert("keyType".to_string(), key_type);
324
325                // Second argument is the value type
326                let value_type_prop = json!({
327                    "key": { "name": "value" },
328                    "value": args[1]
329                });
330                let value_type = extract_column_type(&value_type_prop, context)?;
331                type_obj.insert("valueType".to_string(), value_type);
332            }
333        }
334        "union" => {
335            // For unions, parse all variant types
336            let mut variants = Vec::new();
337            for variant in args {
338                let variant_prop = json!({
339                    "key": { "name": "variant" },
340                    "value": variant
341                });
342                let variant_type = extract_column_type(&variant_prop, context)?;
343                variants.push(variant_type);
344            }
345            type_obj.insert("variants".to_string(), JsonValue::Array(variants));
346        }
347        "literal" => {
348            // For literals, store the literal value
349            if let Some(literal_value) = args.first() {
350                type_obj.insert("value".to_string(), literal_value.clone());
351            }
352        }
353        // For other types, just include their arguments if any
354        _ => {
355            if !args.is_empty() {
356                type_obj.insert("arguments".to_string(), JsonValue::Array(args.to_vec()));
357            }
358        }
359    }
360
361    // Build the type object as before...
362    let type_value = JsonValue::Object(type_obj);
363
364    // Check for circular references
365    check_circular_references(&type_value, context)?;
366
367    Ok(type_value)
368}
369
370pub(crate) fn parse_function_ast(ast_map: HashMap<String, JsonValue>) -> Result<ConvexFunctions, ConvexTypeGeneratorError>
371{
372    let mut functions = Vec::new();
373
374    for (file_name, ast) in ast_map {
375        // Strip the .ts extension from the file name
376        let file_name = file_name.strip_suffix(".ts").unwrap_or(&file_name).to_string();
377
378        // Get the body array
379        let body = ast["body"]
380            .as_array()
381            .ok_or_else(|| ConvexTypeGeneratorError::InvalidSchema {
382                context: format!("file_{}", file_name),
383                details: "Missing body array".to_string(),
384            })?;
385
386        for node in body {
387            // Look for export declarations
388            if node["type"].as_str() == Some("ExportNamedDeclaration") {
389                if let Some(declaration) = node.get("declaration") {
390                    // Handle variable declarations (const testQuery = query({...}))
391                    if declaration["type"].as_str() == Some("VariableDeclaration") {
392                        if let Some(declarators) = declaration["declarations"].as_array() {
393                            for declarator in declarators {
394                                // Get function name
395                                let name = declarator["id"]["name"].as_str().ok_or_else(|| {
396                                    ConvexTypeGeneratorError::InvalidSchema {
397                                        context: format!("file_{}", file_name),
398                                        details: "Missing function name".to_string(),
399                                    }
400                                })?;
401
402                                // Get the function call (query/mutation/action)
403                                let init = &declarator["init"];
404                                if init["type"].as_str() == Some("CallExpression") {
405                                    // Get the callee to determine function type
406                                    let fn_type = init["callee"]["name"].as_str().ok_or_else(|| {
407                                        ConvexTypeGeneratorError::InvalidSchema {
408                                            context: format!("function_{}", name),
409                                            details: "Missing function type".to_string(),
410                                        }
411                                    })?;
412
413                                    // Get the first argument which contains the function config
414                                    if let Some(args) = init["arguments"].as_array() {
415                                        if let Some(config) = args.first() {
416                                            // Extract function parameters from the args property
417                                            let params = extract_function_params(config, &file_name)?;
418
419                                            functions.push(ConvexFunction {
420                                                name: name.to_string(),
421                                                params,
422                                                type_: fn_type.to_string(),
423                                                file_name: file_name.to_string(),
424                                            });
425                                        }
426                                    }
427                                }
428                            }
429                        }
430                    }
431                }
432            }
433        }
434    }
435
436    Ok(functions)
437}
438
439/// Helper function to extract function parameters from the function configuration
440fn extract_function_params(config: &JsonValue, file_name: &str)
441    -> Result<Vec<ConvexFunctionParam>, ConvexTypeGeneratorError>
442{
443    let mut params = Vec::new();
444
445    // Get the args object from the function config
446    if let Some(properties) = config["properties"].as_array() {
447        for prop in properties {
448            if prop["key"]["name"].as_str() == Some("args") {
449                // Ensure args is an object
450                if prop["value"]["type"].as_str() != Some("ObjectExpression") {
451                    return Err(ConvexTypeGeneratorError::InvalidSchema {
452                        context: format!("file_{}", file_name),
453                        details: "Function args must be an object".to_string(),
454                    });
455                }
456
457                // Get the args object value
458                if let Some(args_props) = prop["value"]["properties"].as_array() {
459                    for arg_prop in args_props {
460                        // Validate argument property structure
461                        if !arg_prop["type"].as_str().map_or(false, |t| t == "ObjectProperty") {
462                            return Err(ConvexTypeGeneratorError::InvalidSchema {
463                                context: format!("file_{}", file_name),
464                                details: "Invalid argument property structure".to_string(),
465                            });
466                        }
467
468                        // Get parameter name
469                        let param_name =
470                            arg_prop["key"]["name"]
471                                .as_str()
472                                .ok_or_else(|| ConvexTypeGeneratorError::InvalidSchema {
473                                    context: format!("file_{}", file_name),
474                                    details: "Invalid parameter name".to_string(),
475                                })?;
476
477                        // Get parameter type using the same extraction logic as schema
478                        let mut context = TypeContext::new(format!("function_{}", param_name));
479                        let param_type = extract_column_type(arg_prop, &mut context)?;
480
481                        params.push(ConvexFunctionParam {
482                            name: param_name.to_string(),
483                            data_type: param_type,
484                        });
485                    }
486                }
487                break; // Found args object, no need to continue
488            }
489        }
490    }
491
492    Ok(params)
493}
494
495/// Generates an AST from a source file.
496///
497/// # Arguments
498/// * `path` - Path to the source file
499///
500/// # Errors
501/// Returns an error if the file cannot be parsed or contains invalid syntax
502fn generate_ast(path: &PathBuf) -> Result<JsonValue, ConvexTypeGeneratorError>
503{
504    let path_str = path.to_string_lossy().to_string();
505    let allocator = Allocator::default();
506
507    // Read file contents
508    let source_text = std::fs::read_to_string(path).map_err(|error| ConvexTypeGeneratorError::IOError {
509        file: path_str.clone(),
510        error,
511    })?;
512
513    if source_text.trim().is_empty() {
514        return Err(ConvexTypeGeneratorError::EmptySchemaFile { file: path_str });
515    }
516
517    let source_type = SourceType::from_path(path).map_err(|_| ConvexTypeGeneratorError::ParsingFailed {
518        file: path_str.clone(),
519        details: "Failed to determine source type".to_string(),
520    })?;
521
522    let mut errors: Vec<OxcDiagnostic> = Vec::new();
523
524    let ret = Parser::new(&allocator, &source_text, source_type).parse();
525    errors.extend(ret.errors);
526
527    if ret.panicked {
528        for error in &errors {
529            eprintln!("{error:?}");
530        }
531        return Err(ConvexTypeGeneratorError::ParsingFailed {
532            file: path_str.clone(),
533            details: "Parser panicked".to_string(),
534        });
535    }
536
537    if ret.program.is_empty() {
538        return Err(ConvexTypeGeneratorError::EmptySchemaFile { file: path_str });
539    }
540
541    let semantics = SemanticBuilder::new().with_check_syntax_error(true).build(&ret.program);
542    errors.extend(semantics.errors);
543
544    if !errors.is_empty() {
545        for error in &errors {
546            eprintln!("{error:?}");
547        }
548        return Err(ConvexTypeGeneratorError::ParsingFailed {
549            file: path_str,
550            details: "Semantic analysis failed".to_string(),
551        });
552    }
553
554    serde_json::to_value(&ret.program).map_err(ConvexTypeGeneratorError::SerializationFailed)
555}
556
557const VALID_CONVEX_TYPES: &[&str] = &[
558    "id", "null", "int64", "number", "boolean", "string", "bytes", "array", "object", "record", "union", "literal",
559    "optional", "any",
560];
561
562fn validate_type_name(type_name: &str) -> Result<(), ConvexTypeGeneratorError>
563{
564    if !VALID_CONVEX_TYPES.contains(&type_name) {
565        return Err(ConvexTypeGeneratorError::InvalidType {
566            found: type_name.to_string(),
567            valid_types: VALID_CONVEX_TYPES.iter().map(|&s| s.to_string()).collect(),
568        });
569    }
570    Ok(())
571}
572
573#[derive(Debug, Default)]
574struct TypeContext
575{
576    /// Stack of type paths being processed (includes type name and path)
577    type_stack: Vec<(String, String)>, // (type_name, full_path)
578    /// Current file being processed - used for error context
579    file_name: String,
580    /// Current path in the type structure
581    type_path: Vec<String>,
582}
583
584impl TypeContext
585{
586    fn new(file_name: String) -> Self
587    {
588        Self {
589            file_name,
590            type_stack: Vec::new(),
591            type_path: Vec::new(),
592        }
593    }
594
595    fn push_type(&mut self, type_name: &str) -> Result<(), ConvexTypeGeneratorError>
596    {
597        let current_path = self.type_path.join(".");
598
599        // Only check for circular references in object types
600        // Arrays and other container types can be nested
601        if type_name == "object" {
602            let full_path = if current_path.is_empty() {
603                type_name.to_string()
604            } else {
605                format!("{}.{}", current_path, type_name)
606            };
607
608            // Check if this exact path has been seen before
609            if self.type_stack.iter().any(|(_, path)| path == &full_path) {
610                return Err(ConvexTypeGeneratorError::CircularReference {
611                    path: self.type_stack.iter().map(|(_, path)| path.clone()).collect(),
612                });
613            }
614            self.type_stack.push((type_name.to_string(), full_path));
615        }
616        Ok(())
617    }
618
619    /// Get the current context for error messages
620    fn get_error_context(&self) -> String
621    {
622        format!("{}:{}", self.file_name, self.type_path.join("."))
623    }
624
625    /// Removes the most recently pushed type from the stack
626    fn pop_type(&mut self)
627    {
628        // Only pop if the last type was an object (matches push_type behavior)
629        if let Some((type_name, _)) = self.type_stack.last() {
630            if type_name == "object" {
631                self.type_stack.pop();
632            }
633        }
634    }
635}
636
637fn check_circular_references(type_obj: &JsonValue, context: &mut TypeContext) -> Result<(), ConvexTypeGeneratorError>
638{
639    let type_name = type_obj["type"]
640        .as_str()
641        .ok_or_else(|| ConvexTypeGeneratorError::InvalidSchema {
642            context: context.type_path.join("."),
643            details: "Missing type name".to_string(),
644        })?;
645
646    context.push_type(type_name)?;
647
648    match type_name {
649        "optional" => {
650            if let Some(inner) = type_obj.get("inner") {
651                context.type_path.push("inner".to_string());
652                check_circular_references(inner, context)?;
653                context.type_path.pop();
654            }
655        }
656        "array" => {
657            if let Some(elements) = type_obj.get("elements") {
658                context.type_path.push("elements".to_string());
659                check_circular_references(elements, context)?;
660                context.type_path.pop();
661            }
662        }
663        "object" => {
664            if let Some(properties) = type_obj.get("properties") {
665                if let Some(props) = properties.as_object() {
666                    for (prop_name, prop_type) in props {
667                        context.type_path.push(prop_name.to_string());
668                        check_circular_references(prop_type, context)?;
669                        context.type_path.pop();
670                    }
671                }
672            }
673        }
674        "record" => {
675            // Check key type
676            if let Some(key_type) = type_obj.get("keyType") {
677                context.type_path.push("keyType".to_string());
678                check_circular_references(key_type, context)?;
679                context.type_path.pop();
680            }
681            // Check value type
682            if let Some(value_type) = type_obj.get("valueType") {
683                context.type_path.push("valueType".to_string());
684                check_circular_references(value_type, context)?;
685                context.type_path.pop();
686            }
687        }
688        "union" | "intersection" => {
689            if let Some(variants) = type_obj["variants"].as_array() {
690                for (i, variant) in variants.iter().enumerate() {
691                    context.type_path.push(format!("variant_{}", i));
692                    check_circular_references(variant, context)?;
693                    context.type_path.pop();
694                }
695            }
696        }
697        _ => {} // Other types don't have nested types
698    }
699
700    context.pop_type();
701    Ok(())
702}
703
704/// Trait for converting types into Convex-compatible arguments
705pub trait IntoConvexValue
706{
707    /// Convert the type into a Convex Value
708    fn into_convex_value(self) -> ConvexValue;
709}
710
711impl IntoConvexValue for JsonValue
712{
713    fn into_convex_value(self) -> ConvexValue
714    {
715        match self {
716            JsonValue::Null => ConvexValue::Null,
717            JsonValue::Bool(b) => ConvexValue::Boolean(b),
718            JsonValue::Number(n) => {
719                if let Some(i) = n.as_i64() {
720                    ConvexValue::Int64(i)
721                } else if let Some(f) = n.as_f64() {
722                    ConvexValue::Float64(f)
723                } else {
724                    ConvexValue::Null
725                }
726            }
727            JsonValue::String(s) => ConvexValue::String(s),
728            JsonValue::Array(arr) => ConvexValue::Array(arr.into_iter().map(|v| v.into_convex_value()).collect()),
729            JsonValue::Object(map) => {
730                let converted: BTreeMap<String, ConvexValue> =
731                    map.into_iter().map(|(k, v)| (k, v.into_convex_value())).collect();
732                ConvexValue::Object(converted)
733            }
734        }
735    }
736}
737
738pub trait ConvexValueExt
739{
740    /// Convert a convex value into a serde value
741    fn into_serde_value(self) -> JsonValue;
742}
743
744impl ConvexValueExt for ConvexValue
745{
746    fn into_serde_value(self) -> JsonValue
747    {
748        match self {
749            ConvexValue::Null => JsonValue::Null,
750            ConvexValue::Boolean(b) => JsonValue::Bool(b),
751            ConvexValue::Int64(i) => JsonValue::Number(i.into()),
752            ConvexValue::Float64(f) => {
753                if let Some(n) = serde_json::Number::from_f64(f) {
754                    JsonValue::Number(n)
755                } else {
756                    JsonValue::Null
757                }
758            }
759            ConvexValue::String(s) => JsonValue::String(s),
760            ConvexValue::Array(arr) => JsonValue::Array(arr.into_iter().map(|v| v.into_serde_value()).collect()),
761            ConvexValue::Object(map) => JsonValue::Object(map.into_iter().map(|(k, v)| (k, v.into_serde_value())).collect()),
762            ConvexValue::Bytes(b) => JsonValue::Array(b.into_iter().map(|byte| JsonValue::Number(byte.into())).collect()),
763        }
764    }
765}
766
767/// Extension trait for ConvexClient to provide a more ergonomic API
768pub trait ConvexClientExt
769{
770    /// Convert function arguments into Convex-compatible format
771    fn prepare_args<T: Into<BTreeMap<String, JsonValue>>>(args: T) -> BTreeMap<String, ConvexValue>
772    {
773        args.into().into_iter().map(|(k, v)| (k, v.into_convex_value())).collect()
774    }
775}
776
777// Implement the trait for ConvexClient reference
778impl ConvexClientExt for convex::ConvexClient {}