A better Rust ATProto crate
1use crate::lexicon::{LexObject, LexObjectProperty};
2
3/// Decision about whether to generate builder and/or Default impl for a struct.
4#[derive(Debug, Clone, Copy)]
5pub struct BuilderDecision {
6 /// Whether to generate a bon::Builder derive
7 pub has_builder: bool,
8 /// Whether to generate a Default derive
9 pub has_default: bool,
10}
11
12/// Determine whether a struct should have builder and/or Default based on heuristics.
13///
14/// Rules:
15/// - 0 required fields → Default (no builder)
16/// - All required fields are bare strings → Default (no builder)
17/// - 1+ required fields (not all strings) → Builder (no Default)
18/// - Type name conflicts with bon::Builder → no builder regardless
19///
20/// # Parameters
21/// - `type_name`: Name of the generated struct (to check for conflicts)
22/// - `obj`: Lexicon object schema
23///
24/// # Returns
25/// Decision about builder and Default generation
26pub fn should_generate_builder(type_name: &str, obj: &LexObject<'static>) -> BuilderDecision {
27 let required_count = count_required_fields(obj);
28 let has_default = required_count == 0 || all_required_are_defaultable_strings(obj);
29 let has_builder =
30 required_count >= 1 && !has_default && !conflicts_with_builder_macro(type_name);
31
32 BuilderDecision {
33 has_builder,
34 has_default,
35 }
36}
37
38/// Check if a type name conflicts with types referenced by bon::Builder macro.
39/// bon::Builder generates code that uses unqualified `Option` and `Result`,
40/// so structs with these names cause compilation errors.
41///
42/// This is public for cases where a struct always wants a builder (like records)
43/// but needs to check for conflicts.
44pub fn conflicts_with_builder_macro(type_name: &str) -> bool {
45 matches!(type_name, "Option" | "Result")
46}
47
48/// Count the number of required fields in a lexicon object.
49/// Used to determine whether to generate builders or Default impls.
50fn count_required_fields(obj: &LexObject<'static>) -> usize {
51 let required = obj.required.as_ref().map(|r| r.as_slice()).unwrap_or(&[]);
52 required.len()
53}
54
55/// Check if a field property is a plain string that can default to empty.
56/// Returns true for bare CowStr fields (no format constraints).
57fn is_defaultable_string(prop: &LexObjectProperty<'static>) -> bool {
58 matches!(prop, LexObjectProperty::String(s) if s.format.is_none())
59}
60
61/// Check if all required fields in an object are defaultable strings.
62fn all_required_are_defaultable_strings(obj: &LexObject<'static>) -> bool {
63 let required = obj.required.as_ref().map(|r| r.as_slice()).unwrap_or(&[]);
64
65 if required.is_empty() {
66 return false; // Handled separately by count check
67 }
68
69 required.iter().all(|field_name| {
70 let field_name_str: &str = field_name.as_ref();
71 obj.properties
72 .get(field_name_str)
73 .map(is_defaultable_string)
74 .unwrap_or(false)
75 })
76}