use proc_macro2::TokenStream;
use quote::{format_ident, quote};
use crate::ast::{EntitySection, FieldTypeInfo, ResolvedField};
pub fn generate_field_accessors(sections: &[EntitySection]) -> TokenStream {
let section_modules: Vec<TokenStream> = sections
.iter()
.filter(|s| s.name != "root") .map(|section| generate_section_module(§ion.name, §ion.fields))
.collect();
if section_modules.is_empty() {
return quote! {};
}
quote! {
pub mod fields {
use hyperstack::runtime::hyperstack_interpreter::ast::FieldPath;
#(#section_modules)*
}
}
}
fn generate_section_module(section_name: &str, fields: &[FieldTypeInfo]) -> TokenStream {
let section_ident = format_ident!("{}", section_name);
let field_constants: Vec<TokenStream> = fields
.iter()
.filter(|field| field.emit)
.map(|field| generate_field_constant(section_name, &field.field_name))
.collect();
let nested_modules: Vec<TokenStream> = fields
.iter()
.filter(|field| field.emit)
.filter_map(|field| {
field.resolved_type.as_ref().and_then(|rt| {
if !rt.is_enum && !rt.fields.is_empty() {
Some(generate_nested_accessors(
section_name,
&field.field_name,
&rt.fields,
))
} else {
None
}
})
})
.collect();
quote! {
pub mod #section_ident {
use super::*;
#(#field_constants)*
#(#nested_modules)*
}
}
}
fn generate_field_constant(section_name: &str, field_name: &str) -> TokenStream {
let field_ident = format_ident!("{}", field_name);
quote! {
#[allow(non_upper_case_globals)]
pub fn #field_ident() -> FieldPath {
FieldPath::new(&[#section_name, #field_name])
}
}
}
fn generate_nested_accessors(
parent_section: &str,
field_name: &str,
nested_fields: &[ResolvedField],
) -> TokenStream {
let module_ident = format_ident!("{}", field_name);
let field_constants: Vec<TokenStream> = nested_fields
.iter()
.map(|field| {
let nested_field_ident = format_ident!("{}", field.field_name);
let nested_field_name = &field.field_name;
quote! {
#[allow(non_upper_case_globals)]
pub fn #nested_field_ident() -> FieldPath {
FieldPath::new(&[#parent_section, #field_name, #nested_field_name])
}
}
})
.collect();
quote! {
pub mod #module_ident {
use super::*;
#(#field_constants)*
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ast::{BaseType, ResolvedStructType};
#[test]
fn test_generate_empty_sections() {
let sections: Vec<EntitySection> = vec![];
let output = generate_field_accessors(§ions);
assert!(output.is_empty());
}
#[test]
fn test_generate_simple_section() {
let sections = vec![EntitySection {
name: "id".to_string(),
fields: vec![
FieldTypeInfo {
field_name: "round_id".to_string(),
rust_type_name: "u64".to_string(),
base_type: BaseType::Integer,
is_optional: false,
is_array: false,
inner_type: None,
source_path: None,
resolved_type: None,
emit: true,
},
FieldTypeInfo {
field_name: "round_address".to_string(),
rust_type_name: "Pubkey".to_string(),
base_type: BaseType::Pubkey,
is_optional: false,
is_array: false,
inner_type: None,
source_path: None,
resolved_type: None,
emit: true,
},
],
is_nested_struct: false,
parent_field: None,
}];
let output = generate_field_accessors(§ions);
let output_str = output.to_string();
assert!(output_str.contains("pub mod fields"));
assert!(output_str.contains("pub mod id"));
assert!(output_str.contains("round_id"));
assert!(output_str.contains("round_address"));
}
#[test]
fn test_generate_nested_accessors() {
let sections = vec![EntitySection {
name: "state".to_string(),
fields: vec![FieldTypeInfo {
field_name: "identity".to_string(),
rust_type_name: "MyIdentity".to_string(),
base_type: BaseType::Object,
is_optional: false,
is_array: false,
inner_type: None,
source_path: None,
resolved_type: Some(ResolvedStructType {
type_name: "MyIdentity".to_string(),
fields: vec![
ResolvedField {
field_name: "owner".to_string(),
field_type: "Pubkey".to_string(),
base_type: BaseType::Pubkey,
is_optional: false,
is_array: false,
},
ResolvedField {
field_name: "created_at".to_string(),
field_type: "i64".to_string(),
base_type: BaseType::Timestamp,
is_optional: false,
is_array: false,
},
],
is_instruction: false,
is_account: false,
is_event: false,
is_enum: false,
enum_variants: vec![],
}),
emit: true,
}],
is_nested_struct: false,
parent_field: None,
}];
let output = generate_field_accessors(§ions);
let output_str = output.to_string();
assert!(output_str.contains("pub mod state"));
assert!(output_str.contains("pub mod identity"));
assert!(output_str.contains("owner"));
assert!(output_str.contains("created_at"));
}
#[test]
fn test_skip_root_section() {
let sections = vec![
EntitySection {
name: "root".to_string(),
fields: vec![FieldTypeInfo {
field_name: "some_root_field".to_string(),
rust_type_name: "u64".to_string(),
base_type: BaseType::Integer,
is_optional: false,
is_array: false,
inner_type: None,
source_path: None,
resolved_type: None,
emit: true,
}],
is_nested_struct: false,
parent_field: None,
},
EntitySection {
name: "id".to_string(),
fields: vec![FieldTypeInfo {
field_name: "key".to_string(),
rust_type_name: "u64".to_string(),
base_type: BaseType::Integer,
is_optional: false,
is_array: false,
inner_type: None,
source_path: None,
resolved_type: None,
emit: true,
}],
is_nested_struct: false,
parent_field: None,
},
];
let output = generate_field_accessors(§ions);
let output_str = output.to_string();
assert!(!output_str.contains("pub mod root"));
assert!(!output_str.contains("some_root_field"));
assert!(output_str.contains("pub mod id"));
assert!(output_str.contains("key"));
}
#[test]
fn test_skip_enum_types() {
let sections = vec![EntitySection {
name: "state".to_string(),
fields: vec![FieldTypeInfo {
field_name: "status".to_string(),
rust_type_name: "MyStatus".to_string(),
base_type: BaseType::Object,
is_optional: false,
is_array: false,
inner_type: None,
source_path: None,
resolved_type: Some(ResolvedStructType {
type_name: "MyStatus".to_string(),
fields: vec![],
is_instruction: false,
is_account: false,
is_event: false,
is_enum: true,
enum_variants: vec!["Active".to_string(), "Inactive".to_string()],
}),
emit: true,
}],
is_nested_struct: false,
parent_field: None,
}];
let output = generate_field_accessors(§ions);
let output_str = output.to_string();
assert!(output_str.contains("status"));
assert!(!output_str.contains("Active"));
assert!(!output_str.contains("Inactive"));
}
}