From e22fa46dc3fce6674d48de7b67ed0a782c991f66 Mon Sep 17 00:00:00 2001 From: Daniel Bulant Date: Wed, 9 Jul 2025 21:28:08 +0200 Subject: [PATCH] array support --- src/model.rs | 49 +++++++++++++++++++++-- tests/array.rs | 104 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 149 insertions(+), 4 deletions(-) create mode 100644 tests/array.rs diff --git a/src/model.rs b/src/model.rs index eaa4ba7..78d4dfe 100644 --- a/src/model.rs +++ b/src/model.rs @@ -3,20 +3,20 @@ use quote::quote; use syn::parse::{Parse, ParseBuffer, ParseStream}; use syn::{Error, Ident, Lit, Result, Token}; -#[derive(Clone)] +#[derive(Clone, Debug)] pub struct ConvexName { pub path: Vec, pub id: String, } -#[derive(Clone)] +#[derive(Clone, Debug)] pub struct ConvexField { pub name: ConvexName, pub t: ConvexType, } // See: https://docs.convex.dev/functions/args-validation -#[derive(Clone)] +#[derive(Clone, Debug)] pub enum ConvexType { // Core types. Id(String), @@ -33,6 +33,7 @@ pub enum ConvexType { BoolLiteral(bool), IntLiteral(i64), // TODO: Any, + Array(Box), Optional(Box), } @@ -58,6 +59,9 @@ impl ConvexType { | ConvexType::Optional(child) => { child.t.print().map(|ts| quote! { Option<#ts> }) }, + | ConvexType::Array(child) => { + child.t.print().map(|ts| quote! { Vec<#ts> }) + } // These depend on field.name to generate a struct name. | ConvexType::Object(_) => None, @@ -288,6 +292,7 @@ impl ConvexField { } }); }, + | ConvexType::Array(_) => panic!("Arrays in unions are not supported yet"), | ConvexType::Optional(_) => panic!("Unions may not contain optional branches"), | ConvexType::Union(_) => panic!("Unions may not directly contain other unions, put other types between them"), @@ -345,7 +350,8 @@ impl ConvexField { } }) }, - | _ => { + | any => { + dbg!(any); panic!( "Internal Error: Should have a complex field like object or union" ) @@ -403,6 +409,14 @@ impl ConvexField { rendered_fields.push(quote! { pub #field_name: #field_type, }); + } else if let ConvexType::Array(child) = &field.t { + let struct_name = field.name.to_struct_name(); + let field_type = quote! { Vec<#struct_name> }; + let mut child_struct = child.print(); + structs.append(&mut child_struct); + rendered_fields.push(quote! { + pub #field_name: #field_type, + }); } else { let field_type = field.name.to_struct_name(); let mut child_struct = field.print(); @@ -585,6 +599,28 @@ impl ConvexField { }; } }, + + | ConvexType::Array(next_t) => { + let next_target = Ident::new("value", Span::call_site()); + let child_match = Self::print_extract_field(next_t, Some(next_target)); + quote! { + let #ident = match #match_target { + | ::core::option::Option::Some(::convex::Value::Array(values)) => { + let mut result = Vec::new(); + for value in values { + let value = ::core::option::Option::Some(value); + #child_match + result.push(#ident); + } + result + }, + | _ => { + return Err(::anyhow::anyhow!("Expected '{}' to be an array", #error_name)); + }, + }; + } + } + | _ => { panic!("Unimplemented print_extract_type") }, @@ -823,6 +859,11 @@ impl ConvexField { }))) }, + | "array" => { + let t = Self::parse_validator_call(name, &inner)?; + Ok(ConvexType::Array(Box::new(ConvexField { name: name.clone(), t }))) + }, + | "object" => { let object_inner; let _ = syn::braced!(object_inner in inner); diff --git a/tests/array.rs b/tests/array.rs new file mode 100644 index 0000000..9ef0e39 --- /dev/null +++ b/tests/array.rs @@ -0,0 +1,104 @@ +use convex::Value; +use maplit::btreemap; +use ragkit_convex_macros::convex_model; +use serde_json::json; + +#[test] +fn basic_string_array() { + convex_model!(Model { a: v.array(v.string()) }); + + let convex_data = Value::Object(btreemap! { + "a".into() => Value::Array(vec![Value::String("apple".into()), Value::String("banana".into())]), + }); + let json_data = json!({ + "a": ["apple", "banana"], + }); + + let model = Model::from_convex_value(&convex_data); + assert!(model.is_ok()); + let model = model.unwrap(); + assert_eq!(vec!["apple".to_string(), "banana".to_string()], model.a); + assert_eq!(json_data, json!(model)); +} + +#[test] +fn union_array() { + convex_model!(Model { a: v.array(v.union(v.string(), v.number())) }); + + let convex_data = Value::Object(btreemap! { + "a".into() => Value::Array(vec![Value::String("apple".into()), Value::Float64(42.)]), + }); + let json_data = json!({ + "a": ["apple", 42.], + }); + + let model = Model::from_convex_value(&convex_data); + assert!(model.is_ok()); + let model = model.unwrap(); + // assert_eq!(vec!["apple".to_string(), 42], model.a); + assert_eq!(json_data, json!(model)); +} + +#[test] +fn union_array_objects() { + convex_model!(Model { + a: v.array( + v.union( + v.object({ + typ: v.literal("http"), + hostname: v.string(), + port: v.number() + }), + v.object({ + typ: v.literal("tcp"), + port: v.number() + }) + ) + ) + }); + + let convex_data = Value::Object(btreemap! { + "a".into() => Value::Array(vec![ + Value::Object(btreemap! { + "typ".into() => Value::String("http".into()), + "hostname".into() => Value::String("example.com".into()), + "port".into() => Value::Float64(80.0), + }), + Value::Object(btreemap! { + "typ".into() => Value::String("tcp".into()), + "port".into() => Value::Float64(8080.0), + }), + ]), + }); + let json_data = json!({ + "a": [ + { + "typ": "http", + "hostname": "example.com", + "port": 80.0, + }, + { + "typ": "tcp", + "port": 8080.0, + }, + ], + }); + let model = Model::from_convex_value(&convex_data); + assert!(model.is_ok()); + let model = model.unwrap(); + assert_eq!( + vec![ + ModelA::Variant1(ModelAVariant1 { + typ: "http".to_string(), + hostname: "example.com".to_string(), + port: 80.0, + }), + ModelA::Variant2(ModelAVariant2 { + typ: "tcp".to_string(), + port: 8080.0, + }), + ], + model.a + ); + assert_eq!(json_data, json!(model)); +} \ No newline at end of file