Skip to content

Commit 9e41987

Browse files
lutterclaude
andcommitted
gnd: Add support for nested array types in schema codegen
Add support for nested array types (matrices) like [[String]], [[Int]], [[BigInt]], etc. in schema code generation: - types.rs: Add matrix type mappings for asc_type_for_value(), value_to_asc(), and value_from_asc() functions. These now correctly generate Array<Array<T>> types and use toXxxMatrix()/fromXxxMatrix() methods. - typescript.rs: Extend ArrayType to support any TypeExpr as inner type (not just NamedType), enabling nested array types. Add ArrayType::with_depth() helper for creating arrays with arbitrary nesting. - schema.rs: Replace boolean is_list with list_depth counter to track nesting levels. Update value_type_from_field() to wrap base type with correct number of brackets. Update type_from_field() to create properly nested Array<Array<T>> types. Add comprehensive tests for: - Matrix type conversions in types.rs - Nested array field generation in schema.rs - list_depth() function correctness - value_type_from_field() with nested arrays Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 75d6729 commit 9e41987

File tree

3 files changed

+370
-29
lines changed

3 files changed

+370
-29
lines changed

gnd/src/codegen/schema.rs

Lines changed: 176 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,7 @@ fn is_nullable(ty: &Type<'_, String>) -> bool {
156156
}
157157

158158
/// Check if a type is a list.
159+
#[allow(dead_code)]
159160
fn is_list(ty: &Type<'_, String>) -> bool {
160161
match ty {
161162
Type::ListType(_) => true,
@@ -164,6 +165,16 @@ fn is_list(ty: &Type<'_, String>) -> bool {
164165
}
165166
}
166167

168+
/// Count the list nesting depth of a type.
169+
/// `String` -> 0, `[String]` -> 1, `[[String]]` -> 2, etc.
170+
fn list_depth(ty: &Type<'_, String>) -> u8 {
171+
match ty {
172+
Type::ListType(inner) => 1 + list_depth(inner),
173+
Type::NonNullType(inner) => list_depth(inner),
174+
Type::NamedType(_) => 0,
175+
}
176+
}
177+
167178
/// Check if a field has the @derivedFrom directive.
168179
fn is_derived_field(field: &Field<'_, String>) -> bool {
169180
field.directives.iter().any(|d| d.name == "derivedFrom")
@@ -187,7 +198,8 @@ struct FieldInfo {
187198
is_derived: bool,
188199
base_type: String,
189200
is_nullable: bool,
190-
is_list: bool,
201+
/// The nesting depth of list wrappers. 0 = scalar, 1 = [T], 2 = [[T]], etc.
202+
list_depth: u8,
191203
}
192204

193205
/// Schema code generator.
@@ -232,7 +244,7 @@ impl SchemaCodeGenerator {
232244
is_derived: is_derived_field(f),
233245
base_type: get_base_type_name(&f.field_type),
234246
is_nullable: is_nullable(&f.field_type),
235-
is_list: is_list(&f.field_type),
247+
list_depth: list_depth(&f.field_type),
236248
})
237249
.collect();
238250

@@ -557,24 +569,33 @@ impl SchemaCodeGenerator {
557569
}
558570

559571
/// Get the value type string for a field.
572+
///
573+
/// Returns the GraphQL-style value type string:
574+
/// - Scalars: `String`, `Int`, `BigInt`, etc.
575+
/// - Arrays: `[String]`, `[Int]`, etc.
576+
/// - Nested arrays: `[[String]]`, `[[Int]]`, etc.
577+
/// - Entity references are converted to `String` (their ID type)
560578
fn value_type_from_field(&self, field: &FieldInfo) -> String {
561-
if field.is_list {
562-
format!(
563-
"[{}]",
564-
if self.entity_names.contains(&field.base_type) {
565-
"String".to_string()
566-
} else {
567-
field.base_type.clone()
568-
}
569-
)
570-
} else if self.entity_names.contains(&field.base_type) {
571-
"String".to_string()
579+
let base = if self.entity_names.contains(&field.base_type) {
580+
"String".to_string() // Entity references are stored as string IDs
572581
} else {
573582
field.base_type.clone()
583+
};
584+
585+
// Wrap with brackets for each level of list nesting
586+
let mut result = base;
587+
for _ in 0..field.list_depth {
588+
result = format!("[{}]", result);
574589
}
590+
result
575591
}
576592

577593
/// Convert field info to an AssemblyScript TypeExpr.
594+
///
595+
/// Creates the correct type expression including nested arrays:
596+
/// - Scalars: `string`, `i32`, `BigInt`, etc.
597+
/// - Arrays: `Array<string>`, `Array<i32>`, etc.
598+
/// - Nested arrays: `Array<Array<string>>`, etc.
578599
fn type_from_field(&self, field: &FieldInfo) -> TypeExpr {
579600
let type_name = if self.entity_names.contains(&field.base_type) {
580601
"string" // Entity references are stored as string IDs
@@ -584,12 +605,13 @@ impl SchemaCodeGenerator {
584605

585606
let named = NamedType::new(type_name);
586607

587-
if field.is_list {
588-
let array = ArrayType::new(named);
608+
if field.list_depth > 0 {
609+
// Use ArrayType::with_depth to create nested array types
610+
let array_type = ArrayType::with_depth(named, field.list_depth);
589611
if field.is_nullable {
590-
NullableType::new(array).into()
612+
NullableType::new(array_type).into()
591613
} else {
592-
array.into()
614+
array_type
593615
}
594616
} else if field.is_nullable && !named.is_primitive() {
595617
NullableType::new(named).into()
@@ -715,4 +737,141 @@ mod tests {
715737
"Array field should use Array<string> type"
716738
);
717739
}
740+
741+
#[test]
742+
fn test_nested_array_field() {
743+
let schema = r#"
744+
type Matrix @entity {
745+
id: ID!
746+
stringMatrix: [[String!]!]!
747+
intMatrix: [[Int!]!]
748+
bigIntMatrix: [[BigInt!]!]!
749+
}
750+
"#;
751+
let doc = parse_schema::<String>(schema).unwrap();
752+
let gen = SchemaCodeGenerator::new(&doc);
753+
754+
let classes = gen.generate_types(true);
755+
assert_eq!(classes.len(), 1);
756+
757+
let matrix = &classes[0];
758+
let output = matrix.to_string();
759+
760+
// Verify nested array field getter/setter are generated
761+
assert!(
762+
output.contains("get stringMatrix()"),
763+
"Should have stringMatrix getter"
764+
);
765+
assert!(
766+
output.contains("set stringMatrix("),
767+
"Should have stringMatrix setter"
768+
);
769+
770+
// Check the type is Array<Array<string>>
771+
assert!(
772+
output.contains("Array<Array<string>>"),
773+
"Nested array field should use Array<Array<string>> type, got: {}",
774+
output
775+
);
776+
777+
// Check that toStringMatrix() and fromStringMatrix() are used
778+
assert!(
779+
output.contains("toStringMatrix()"),
780+
"Should use toStringMatrix() for nested string arrays"
781+
);
782+
assert!(
783+
output.contains("fromStringMatrix("),
784+
"Should use Value.fromStringMatrix() for nested string arrays"
785+
);
786+
787+
// Check BigInt matrix uses correct methods
788+
assert!(
789+
output.contains("Array<Array<BigInt>>"),
790+
"BigInt matrix should use Array<Array<BigInt>> type"
791+
);
792+
assert!(
793+
output.contains("toBigIntMatrix()"),
794+
"Should use toBigIntMatrix() for nested BigInt arrays"
795+
);
796+
assert!(
797+
output.contains("fromBigIntMatrix("),
798+
"Should use Value.fromBigIntMatrix() for nested BigInt arrays"
799+
);
800+
801+
// Check nullable nested array has correct type
802+
assert!(
803+
output.contains("Array<Array<i32>> | null"),
804+
"Nullable nested array should be Array<Array<i32>> | null"
805+
);
806+
}
807+
808+
#[test]
809+
fn test_list_depth() {
810+
use graphql_parser::parse_schema;
811+
812+
// Helper to get list depth from schema field type
813+
fn get_field_list_depth(schema_str: &str) -> u8 {
814+
let doc = parse_schema::<String>(schema_str).unwrap();
815+
for def in &doc.definitions {
816+
if let Definition::TypeDefinition(TypeDefinition::Object(obj)) = def {
817+
for field in &obj.fields {
818+
if field.name == "field" {
819+
return list_depth(&field.field_type);
820+
}
821+
}
822+
}
823+
}
824+
panic!("Field not found");
825+
}
826+
827+
// Scalar
828+
assert_eq!(
829+
get_field_list_depth("type T @entity { id: ID!, field: String! }"),
830+
0
831+
);
832+
833+
// Simple array
834+
assert_eq!(
835+
get_field_list_depth("type T @entity { id: ID!, field: [String!]! }"),
836+
1
837+
);
838+
839+
// Nested array (matrix)
840+
assert_eq!(
841+
get_field_list_depth("type T @entity { id: ID!, field: [[String!]!]! }"),
842+
2
843+
);
844+
845+
// Triple nested array
846+
assert_eq!(
847+
get_field_list_depth("type T @entity { id: ID!, field: [[[String!]!]!]! }"),
848+
3
849+
);
850+
}
851+
852+
#[test]
853+
fn test_value_type_from_field_nested() {
854+
let schema = r#"
855+
type Test @entity {
856+
id: ID!
857+
scalar: String!
858+
array: [String!]!
859+
matrix: [[String!]!]!
860+
}
861+
"#;
862+
let doc = parse_schema::<String>(schema).unwrap();
863+
let gen = SchemaCodeGenerator::new(&doc);
864+
865+
// Find the entity
866+
let entity = &gen.entities[0];
867+
868+
// Find each field and check its value type
869+
let scalar_field = entity.fields.iter().find(|f| f.name == "scalar").unwrap();
870+
let array_field = entity.fields.iter().find(|f| f.name == "array").unwrap();
871+
let matrix_field = entity.fields.iter().find(|f| f.name == "matrix").unwrap();
872+
873+
assert_eq!(gen.value_type_from_field(scalar_field), "String");
874+
assert_eq!(gen.value_type_from_field(array_field), "[String]");
875+
assert_eq!(gen.value_type_from_field(matrix_field), "[[String]]");
876+
}
718877
}

0 commit comments

Comments
 (0)