hemmer_provider_generator_parser/
rustdoc_loader.rs

1//! Rustdoc JSON loader
2//!
3//! Loads and parses rustdoc JSON output from AWS SDK crates.
4
5use hemmer_provider_generator_common::{FieldDefinition, GeneratorError, Result};
6use rustdoc_types::{Crate, Id, ItemEnum, Type};
7use std::path::Path;
8
9/// Loads rustdoc JSON from a file path
10///
11/// The JSON file should be generated with:
12/// ```bash
13/// cargo +nightly rustdoc --package aws-sdk-s3 -- -Z unstable-options --output-format json
14/// ```
15pub struct RustdocLoader;
16
17impl RustdocLoader {
18    /// Load rustdoc JSON from a file
19    pub fn load_from_file(path: &Path) -> Result<Crate> {
20        let content = std::fs::read_to_string(path).map_err(|e| {
21            GeneratorError::Parse(format!("Failed to read rustdoc JSON file: {}", e))
22        })?;
23
24        let crate_data: Crate = serde_json::from_str(&content)
25            .map_err(|e| GeneratorError::Parse(format!("Failed to parse rustdoc JSON: {}", e)))?;
26
27        Ok(crate_data)
28    }
29
30    /// Extract operation module names from the crate
31    ///
32    /// In AWS SDK crates, operations are in the `operation` module
33    pub fn find_operation_modules(crate_data: &Crate) -> Vec<String> {
34        let mut operations = Vec::new();
35
36        // Find the root module
37        let root_id = &crate_data.root;
38        if let Some(root_item) = crate_data.index.get(root_id) {
39            // Look for the "operation" module
40            if let rustdoc_types::ItemEnum::Module(module) = &root_item.inner {
41                for item_id in &module.items {
42                    if let Some(item) = crate_data.index.get(item_id) {
43                        if let Some(name) = &item.name {
44                            if name == "operation" {
45                                // Found operation module, extract its submodules
46                                if let rustdoc_types::ItemEnum::Module(op_module) = &item.inner {
47                                    for op_id in &op_module.items {
48                                        if let Some(op_item) = crate_data.index.get(op_id) {
49                                            if let Some(op_name) = &op_item.name {
50                                                operations.push(op_name.clone());
51                                            }
52                                        }
53                                    }
54                                }
55                            }
56                        }
57                    }
58                }
59            }
60        }
61
62        operations
63    }
64
65    /// Extract type names from the types module
66    pub fn find_type_modules(crate_data: &Crate) -> Vec<String> {
67        let mut types = Vec::new();
68
69        let root_id = &crate_data.root;
70        if let Some(root_item) = crate_data.index.get(root_id) {
71            if let rustdoc_types::ItemEnum::Module(module) = &root_item.inner {
72                for item_id in &module.items {
73                    if let Some(item) = crate_data.index.get(item_id) {
74                        if let Some(name) = &item.name {
75                            if name == "types" {
76                                if let rustdoc_types::ItemEnum::Module(types_module) = &item.inner {
77                                    for type_id in &types_module.items {
78                                        if let Some(type_item) = crate_data.index.get(type_id) {
79                                            if let Some(type_name) = &type_item.name {
80                                                types.push(type_name.clone());
81                                            }
82                                        }
83                                    }
84                                }
85                            }
86                        }
87                    }
88                }
89            }
90        }
91
92        types
93    }
94
95    /// Extract fields from a struct by name
96    ///
97    /// Searches for a struct with the given name and extracts its field definitions.
98    /// Returns empty vector if struct not found or has no fields.
99    pub fn extract_struct_fields(crate_data: &Crate, struct_name: &str) -> Vec<FieldDefinition> {
100        // Find the struct item
101        let struct_item = crate_data
102            .index
103            .values()
104            .find(|item| item.name.as_deref() == Some(struct_name));
105
106        let struct_item = match struct_item {
107            Some(item) => item,
108            None => return vec![],
109        };
110
111        // Extract fields from the struct
112        if let ItemEnum::Struct(struct_data) = &struct_item.inner {
113            match &struct_data.kind {
114                rustdoc_types::StructKind::Plain { fields, .. } => {
115                    return fields
116                        .iter()
117                        .filter_map(|field_id| Self::extract_field_definition(crate_data, field_id))
118                        .collect();
119                },
120                _ => return vec![],
121            }
122        }
123
124        vec![]
125    }
126
127    /// Extract a single field definition from a field ID
128    fn extract_field_definition(crate_data: &Crate, field_id: &Id) -> Option<FieldDefinition> {
129        let field_item = crate_data.index.get(field_id)?;
130        let field_name = field_item.name.as_ref()?.clone();
131
132        // Extract field type
133        if let ItemEnum::StructField(field_type) = &field_item.inner {
134            let (field_type_mapped, required) =
135                Self::map_rustdoc_type_to_field_type(crate_data, field_type);
136
137            Some(FieldDefinition {
138                name: field_name.clone(),
139                field_type: field_type_mapped,
140                required,
141                sensitive: crate::TypeMapper::is_sensitive(&field_name),
142                immutable: crate::TypeMapper::is_immutable(&field_name),
143                description: field_item.docs.clone(),
144                // For rustdoc-parsed fields, the accessor is the field name itself
145                response_accessor: Some(field_name),
146            })
147        } else {
148            None
149        }
150    }
151
152    /// Map rustdoc Type to our FieldType
153    ///
154    /// Returns (FieldType, required: bool)
155    #[allow(clippy::only_used_in_recursion)]
156    fn map_rustdoc_type_to_field_type(
157        crate_data: &Crate,
158        rustdoc_type: &Type,
159    ) -> (hemmer_provider_generator_common::FieldType, bool) {
160        use hemmer_provider_generator_common::FieldType;
161
162        match rustdoc_type {
163            Type::ResolvedPath(path) => {
164                // Extract the last segment of the path as the type name
165                let type_name = path.path.rsplit("::").next().unwrap_or(&path.path);
166
167                // Check if it's Option<T>
168                if type_name == "Option" {
169                    if let Some(generic_args) = &path.args {
170                        if let rustdoc_types::GenericArgs::AngleBracketed { args, .. } =
171                            generic_args.as_ref()
172                        {
173                            if let Some(rustdoc_types::GenericArg::Type(inner_type)) = args.first()
174                            {
175                                let (inner_field_type, _) =
176                                    Self::map_rustdoc_type_to_field_type(crate_data, inner_type);
177                                return (inner_field_type, false); // Option means not required
178                            }
179                        }
180                    }
181                    return (FieldType::String, false);
182                }
183
184                // Check if it's Vec<T>
185                if type_name == "Vec" {
186                    if let Some(generic_args) = &path.args {
187                        if let rustdoc_types::GenericArgs::AngleBracketed { args, .. } =
188                            generic_args.as_ref()
189                        {
190                            if let Some(rustdoc_types::GenericArg::Type(inner_type)) = args.first()
191                            {
192                                let (inner_field_type, _) =
193                                    Self::map_rustdoc_type_to_field_type(crate_data, inner_type);
194                                return (FieldType::List(Box::new(inner_field_type)), true);
195                            }
196                        }
197                    }
198                    return (FieldType::List(Box::new(FieldType::String)), true);
199                }
200
201                // Check if it's HashMap<K, V>
202                if type_name == "HashMap" {
203                    if let Some(generic_args) = &path.args {
204                        if let rustdoc_types::GenericArgs::AngleBracketed { args, .. } =
205                            generic_args.as_ref()
206                        {
207                            if args.len() >= 2 {
208                                let key_type = if let Some(rustdoc_types::GenericArg::Type(k)) =
209                                    args.first()
210                                {
211                                    let (kt, _) =
212                                        Self::map_rustdoc_type_to_field_type(crate_data, k);
213                                    kt
214                                } else {
215                                    FieldType::String
216                                };
217
218                                let value_type =
219                                    if let Some(rustdoc_types::GenericArg::Type(v)) = args.get(1) {
220                                        let (vt, _) =
221                                            Self::map_rustdoc_type_to_field_type(crate_data, v);
222                                        vt
223                                    } else {
224                                        FieldType::String
225                                    };
226
227                                return (
228                                    FieldType::Map(Box::new(key_type), Box::new(value_type)),
229                                    true,
230                                );
231                            }
232                        }
233                    }
234                    return (
235                        FieldType::Map(Box::new(FieldType::String), Box::new(FieldType::String)),
236                        true,
237                    );
238                }
239
240                // Map basic types
241                (crate::TypeMapper::map_type(type_name), true)
242            },
243            Type::Primitive(prim) => (crate::TypeMapper::map_type(prim), true),
244            _ => (FieldType::String, true), // Default fallback
245        }
246    }
247}
248
249#[cfg(test)]
250mod tests {
251    #[test]
252    fn test_rustdoc_loader_api() {
253        // This is a placeholder test - real tests would need actual rustdoc JSON files
254        // We'll test with integration tests that generate rustdoc JSON
255    }
256}