use std::collections::HashSet;
use proc_macro2::TokenStream;
use quote::quote;
use crate::ir::types::{Cardinality, FieldDef, StructDef, TypeRef};
use super::util::{make_ident, type_ref_tokens};
pub fn emit_builder(def: &StructDef, choice_types: &HashSet<String>) -> TokenStream {
let struct_name = make_ident(&def.name);
let builder_name = make_ident(&format!("{}Builder", def.name));
let builder_fields: TokenStream = def
.fields
.iter()
.map(|f| emit_builder_field(f, choice_types))
.collect();
let setters: TokenStream = def
.fields
.iter()
.map(|f| emit_setter(f, choice_types, &builder_name))
.collect();
let has_required = def
.fields
.iter()
.any(|f| matches!(&f.cardinality, Cardinality::Required));
let missing_checks: TokenStream = def
.fields
.iter()
.filter_map(|f| {
if matches!(&f.cardinality, Cardinality::Required) {
let rust_name = make_ident(&f.rust_name);
let field_name_str = &f.rust_name;
Some(quote! {
if self.#rust_name.is_none() {
missing.push(#field_name_str.to_owned());
}
})
} else {
None
}
})
.collect();
let field_assignments: TokenStream = def.fields.iter().map(emit_build_assignment).collect();
let struct_name_str = &def.name;
let build_body: TokenStream = if has_required {
quote! {
let mut missing: ::std::vec::Vec<::std::string::String> = ::std::vec::Vec::new();
#missing_checks
if !missing.is_empty() {
return ::std::result::Result::Err(crate::common::BuilderError {
type_name: #struct_name_str.to_owned(),
missing_fields: missing,
});
}
::std::result::Result::Ok(#struct_name {
#field_assignments
})
}
} else {
quote! {
::std::result::Result::Ok(#struct_name {
#field_assignments
})
}
};
let builder_doc = format!(
" Builder for [`{struct_name_str}`]. Construct via [`{struct_name_str}::builder()`].",
);
quote! {
#[doc = #builder_doc]
#[allow(clippy::struct_field_names)]
#[derive(Default)]
pub struct #builder_name {
#builder_fields
}
impl #builder_name {
#setters
pub fn build(self) -> ::std::result::Result<#struct_name, crate::common::BuilderError> {
#build_body
}
}
impl #struct_name {
#[must_use]
pub fn builder() -> #builder_name {
#builder_name::default()
}
}
}
}
fn base_type(field: &FieldDef, choice_types: &HashSet<String>) -> TokenStream {
let is_choice = match &field.type_ref {
TypeRef::Named(n) => choice_types.contains(n),
TypeRef::Builtin(_) => false,
};
if is_choice {
let inner = type_ref_tokens(&field.type_ref);
quote! { crate::common::ChoiceWrapper<#inner> }
} else {
type_ref_tokens(&field.type_ref)
}
}
fn emit_builder_field(field: &FieldDef, choice_types: &HashSet<String>) -> TokenStream {
let rust_name = make_ident(&field.rust_name);
let bt = base_type(field, choice_types);
match &field.cardinality {
Cardinality::Required | Cardinality::Optional => quote! {
#rust_name: ::std::option::Option<#bt>,
},
Cardinality::Vec | Cardinality::BoundedVec(_) => quote! {
#rust_name: ::std::vec::Vec<#bt>,
},
}
}
fn emit_setter(
field: &FieldDef,
choice_types: &HashSet<String>,
builder_name: &proc_macro2::Ident,
) -> TokenStream {
let rust_name = make_ident(&field.rust_name);
let bt = base_type(field, choice_types);
let set_doc = format!(" Set the `{}` field.", field.rust_name);
match &field.cardinality {
Cardinality::Required | Cardinality::Optional => {
quote! {
#[doc = #set_doc]
#[must_use]
pub fn #rust_name(mut self, value: #bt) -> #builder_name {
self.#rust_name = ::std::option::Option::Some(value);
self
}
}
}
Cardinality::Vec | Cardinality::BoundedVec(_) => {
let add_name = make_ident(&format!("add_{}", field.rust_name));
let set_doc_vec = format!(
" Set the `{}` field (replaces any previously added items).",
field.rust_name
);
let add_doc = format!(" Append one item to the `{}` field.", field.rust_name);
quote! {
#[doc = #set_doc_vec]
#[must_use]
pub fn #rust_name(mut self, value: ::std::vec::Vec<#bt>) -> #builder_name {
self.#rust_name = value;
self
}
#[doc = #add_doc]
#[must_use]
pub fn #add_name(mut self, value: #bt) -> #builder_name {
self.#rust_name.push(value);
self
}
}
}
}
}
fn emit_build_assignment(field: &FieldDef) -> TokenStream {
let rust_name = make_ident(&field.rust_name);
match &field.cardinality {
Cardinality::Required => quote! {
#rust_name: self.#rust_name.unwrap(),
},
Cardinality::Optional | Cardinality::Vec | Cardinality::BoundedVec(_) => quote! {
#rust_name: self.#rust_name,
},
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ir::types::{Cardinality, FieldDef, RustType, StructDef, TypeRef};
fn field(
xml_name: &str,
rust_name: &str,
type_ref: TypeRef,
cardinality: Cardinality,
) -> FieldDef {
FieldDef {
xml_name: xml_name.to_owned(),
rust_name: rust_name.to_owned(),
type_ref,
cardinality,
}
}
fn no_choices() -> HashSet<String> {
HashSet::new()
}
fn choice_set(names: &[&str]) -> HashSet<String> {
names.iter().map(|&s| s.to_owned()).collect()
}
fn emit_str(def: &StructDef, choices: &HashSet<String>) -> String {
let ts = emit_builder(def, choices);
let file_str = ts.to_string();
let parsed = syn::parse_file(&file_str)
.unwrap_or_else(|e| panic!("builder emit produced invalid Rust: {e}\n{file_str}"));
prettyplease::unparse(&parsed)
}
#[test]
fn builder_for_required_field_is_valid_rust() {
let def = StructDef {
name: "MyMsg".to_owned(),
fields: vec![field(
"Id",
"id",
TypeRef::Builtin(RustType::String),
Cardinality::Required,
)],
};
let src = emit_str(&def, &no_choices());
assert!(src.contains("MyMsgBuilder"), "builder struct: {src}");
assert!(src.contains("pub struct MyMsgBuilder"), "{src}");
assert!(
src.contains("Option<String>") || src.contains("Option < String >"),
"{src}"
);
}
#[test]
fn builder_for_optional_field() {
let def = StructDef {
name: "Foo".to_owned(),
fields: vec![field(
"Val",
"val",
TypeRef::Builtin(RustType::String),
Cardinality::Optional,
)],
};
let src = emit_str(&def, &no_choices());
assert!(src.contains("FooBuilder"), "{src}");
assert!(src.contains("pub fn val"), "{src}");
}
#[test]
fn builder_for_vec_field_has_add_method() {
let def = StructDef {
name: "Container".to_owned(),
fields: vec![field(
"Items",
"items",
TypeRef::Named("ItemType".to_owned()),
Cardinality::Vec,
)],
};
let src = emit_str(&def, &no_choices());
assert!(src.contains("pub fn items"), "{src}");
assert!(src.contains("pub fn add_items"), "{src}");
}
#[test]
fn builder_for_bounded_vec_field_has_add_method() {
let def = StructDef {
name: "Container".to_owned(),
fields: vec![field(
"Items",
"items",
TypeRef::Named("ItemType".to_owned()),
Cardinality::BoundedVec(10),
)],
};
let src = emit_str(&def, &no_choices());
assert!(src.contains("pub fn items"), "{src}");
assert!(src.contains("pub fn add_items"), "{src}");
}
#[test]
fn builder_choice_field_uses_choice_wrapper() {
let def = StructDef {
name: "Hdr".to_owned(),
fields: vec![field(
"Fr",
"fr",
TypeRef::Named("Party51Choice".to_owned()),
Cardinality::Required,
)],
};
let choices = choice_set(&["Party51Choice"]);
let src = emit_str(&def, &choices);
assert!(
src.contains("ChoiceWrapper<Party51Choice>")
|| src.contains("ChoiceWrapper < Party51Choice >"),
"choice field must use ChoiceWrapper: {src}"
);
}
#[test]
fn builder_build_method_present() {
let def = StructDef {
name: "Thing".to_owned(),
fields: vec![
field(
"A",
"a",
TypeRef::Builtin(RustType::String),
Cardinality::Required,
),
field(
"B",
"b",
TypeRef::Builtin(RustType::String),
Cardinality::Optional,
),
],
};
let src = emit_str(&def, &no_choices());
assert!(src.contains("pub fn build"), "{src}");
assert!(src.contains("BuilderError"), "{src}");
}
#[test]
fn struct_builder_associated_fn_present() {
let def = StructDef {
name: "Thing".to_owned(),
fields: vec![field(
"X",
"x",
TypeRef::Builtin(RustType::Bool),
Cardinality::Required,
)],
};
let src = emit_str(&def, &no_choices());
assert!(src.contains("impl Thing"), "{src}");
assert!(src.contains("pub fn builder"), "{src}");
}
#[test]
fn empty_struct_builder_is_valid_rust() {
let def = StructDef {
name: "Empty".to_owned(),
fields: vec![],
};
let src = emit_str(&def, &no_choices());
syn::parse_file(&src).expect("must be valid Rust");
}
#[test]
fn keyword_field_name_in_builder() {
let def = StructDef {
name: "Foo".to_owned(),
fields: vec![field(
"Type",
"type",
TypeRef::Builtin(RustType::String),
Cardinality::Required,
)],
};
let src = emit_str(&def, &no_choices());
assert!(src.contains("r#type"), "keyword field not escaped: {src}");
}
#[test]
fn full_struct_with_all_cardinalities_is_valid_rust() {
let def = StructDef {
name: "Full".to_owned(),
fields: vec![
field(
"Req",
"req",
TypeRef::Builtin(RustType::String),
Cardinality::Required,
),
field(
"Opt",
"opt",
TypeRef::Builtin(RustType::Bool),
Cardinality::Optional,
),
field(
"Many",
"many",
TypeRef::Named("ItemT".to_owned()),
Cardinality::Vec,
),
field(
"Bounded",
"bounded",
TypeRef::Named("ItemT".to_owned()),
Cardinality::BoundedVec(5),
),
],
};
let src = emit_str(&def, &no_choices());
syn::parse_file(&src).expect("must be valid Rust");
assert!(src.contains("pub fn req"), "{src}");
assert!(src.contains("pub fn opt"), "{src}");
assert!(src.contains("pub fn many"), "{src}");
assert!(src.contains("pub fn add_many"), "{src}");
assert!(src.contains("pub fn bounded"), "{src}");
assert!(src.contains("pub fn add_bounded"), "{src}");
}
}