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 whether a struct is eligible for a manual `impl Default` with schema values.
62///
63/// Returns true when every required field has a schema default value and all other
64/// fields are optional (which naturally default to `None` or `Some(schema_default)`).
65pub fn eligible_for_schema_default(obj: &LexObject<'static>) -> bool {
66 let required = obj.required.as_ref().map(|r| r.as_slice()).unwrap_or(&[]);
67 let nullable = obj.nullable.as_ref().map(|n| n.as_slice()).unwrap_or(&[]);
68
69 required.iter().all(|field_name| {
70 // Nullable required fields are treated as optional — they default to None.
71 if nullable.contains(field_name) {
72 return true;
73 }
74 let field_name_str: &str = field_name.as_ref();
75 obj.properties
76 .get(field_name_str)
77 .map(has_schema_default)
78 .unwrap_or(false)
79 })
80}
81
82/// Check if a field property has a schema default value.
83pub fn has_schema_default(prop: &LexObjectProperty<'static>) -> bool {
84 match prop {
85 LexObjectProperty::Boolean(b) => b.default.is_some(),
86 LexObjectProperty::Integer(i) => i.default.is_some(),
87 LexObjectProperty::String(s) => s.default.is_some() && s.known_values.is_none(),
88 _ => false,
89 }
90}
91
92/// Check if all required fields in an object are defaultable strings.
93fn all_required_are_defaultable_strings(obj: &LexObject<'static>) -> bool {
94 let required = obj.required.as_ref().map(|r| r.as_slice()).unwrap_or(&[]);
95
96 if required.is_empty() {
97 return false; // Handled separately by count check
98 }
99
100 required.iter().all(|field_name| {
101 let field_name_str: &str = field_name.as_ref();
102 obj.properties
103 .get(field_name_str)
104 .map(is_defaultable_string)
105 .unwrap_or(false)
106 })
107}