hemmer_provider_generator_parser/
aws.rs

1//! AWS SDK parser implementation
2//!
3//! Parses AWS SDK structure into ServiceDefinition IR.
4//!
5//! ## Usage Modes
6//!
7//! 1. **Hardcoded mode**: Returns predefined resource definitions (S3 bucket example)
8//! 2. **Rustdoc JSON mode**: Parses rustdoc JSON output for automated discovery
9//!
10//! To generate rustdoc JSON:
11//! ```bash
12//! cargo +nightly rustdoc --package aws-sdk-s3 -- -Z unstable-options --output-format json
13//! ```
14
15use hemmer_provider_generator_common::{
16    FieldDefinition, GeneratorError, OperationMapping, Operations, Provider, ResourceDefinition,
17    Result, ServiceDefinition,
18};
19
20use crate::{CrudOperation, OperationClassifier, RustdocLoader};
21use std::collections::HashMap;
22use std::path::{Path, PathBuf};
23
24/// AWS SDK parser
25pub struct AwsParser {
26    service_name: String,
27    sdk_version: String,
28    rustdoc_json_path: Option<PathBuf>,
29}
30
31impl AwsParser {
32    /// Create a new AWS parser in hardcoded mode
33    pub fn new(service_name: &str, sdk_version: &str) -> Self {
34        Self {
35            service_name: service_name.to_string(),
36            sdk_version: sdk_version.to_string(),
37            rustdoc_json_path: None,
38        }
39    }
40
41    /// Create a new AWS parser with rustdoc JSON path for automated parsing
42    pub fn with_rustdoc_json(
43        service_name: &str,
44        sdk_version: &str,
45        rustdoc_json_path: PathBuf,
46    ) -> Self {
47        Self {
48            service_name: service_name.to_string(),
49            sdk_version: sdk_version.to_string(),
50            rustdoc_json_path: Some(rustdoc_json_path),
51        }
52    }
53
54    /// Parse the AWS service into ServiceDefinition
55    ///
56    /// # Modes
57    /// - If rustdoc_json_path is provided: Parse from rustdoc JSON (automated)
58    /// - Otherwise: Use hardcoded resource definitions (S3 bucket example)
59    pub fn parse(&self) -> Result<ServiceDefinition> {
60        if let Some(json_path) = &self.rustdoc_json_path {
61            self.parse_from_rustdoc(json_path)
62        } else {
63            self.parse_hardcoded()
64        }
65    }
66
67    /// Parse from rustdoc JSON (automated mode)
68    fn parse_from_rustdoc(&self, json_path: &Path) -> Result<ServiceDefinition> {
69        // Load rustdoc JSON
70        let crate_data = RustdocLoader::load_from_file(json_path)?;
71
72        // Extract operations
73        let operations = RustdocLoader::find_operation_modules(&crate_data);
74
75        // Group operations by resource
76        let grouped = self.group_operations_by_resource(operations);
77
78        // Build resources from grouped operations
79        let resources = grouped
80            .into_iter()
81            .map(|(resource_name, ops)| {
82                self.build_resource_from_operations(&crate_data, &resource_name, ops)
83            })
84            .collect();
85
86        Ok(ServiceDefinition {
87            provider: Provider::Aws,
88            name: self.service_name.clone(),
89            sdk_version: self.sdk_version.clone(),
90            resources,
91            data_sources: vec![], // Will implement data source detection later
92        })
93    }
94
95    /// Parse using hardcoded resource definitions
96    fn parse_hardcoded(&self) -> Result<ServiceDefinition> {
97        if self.service_name == "s3" {
98            Ok(self.parse_s3_service())
99        } else {
100            Err(GeneratorError::Parse(format!(
101                "Service '{}' not yet supported in hardcoded mode. Use with_rustdoc_json() for automated parsing.",
102                self.service_name
103            )))
104        }
105    }
106
107    /// Parse S3 service (hardcoded example for MVP)
108    fn parse_s3_service(&self) -> ServiceDefinition {
109        ServiceDefinition {
110            provider: Provider::Aws,
111            name: self.service_name.clone(),
112            sdk_version: self.sdk_version.clone(),
113            resources: vec![self.create_s3_bucket_resource()],
114            data_sources: vec![], // Will implement data source detection later
115        }
116    }
117
118    /// Create S3 Bucket resource definition (example)
119    fn create_s3_bucket_resource(&self) -> ResourceDefinition {
120        ResourceDefinition {
121            name: "bucket".to_string(),
122            description: Some("S3 Bucket for object storage".to_string()),
123            fields: vec![
124                FieldDefinition {
125                    name: "bucket".to_string(),
126                    field_type: hemmer_provider_generator_common::FieldType::String,
127                    required: true,
128                    sensitive: false,
129                    immutable: true, // Bucket name is immutable
130                    description: Some("Bucket name (globally unique)".to_string()),
131                    response_accessor: None,
132                },
133                FieldDefinition {
134                    name: "acl".to_string(),
135                    field_type: hemmer_provider_generator_common::FieldType::String,
136                    required: false,
137                    sensitive: false,
138                    immutable: false,
139                    description: Some("Canned ACL to apply to the bucket".to_string()),
140                    response_accessor: None,
141                },
142                FieldDefinition {
143                    name: "tags".to_string(),
144                    field_type: hemmer_provider_generator_common::FieldType::Map(
145                        Box::new(hemmer_provider_generator_common::FieldType::String),
146                        Box::new(hemmer_provider_generator_common::FieldType::String),
147                    ),
148                    required: false,
149                    sensitive: false,
150                    immutable: false,
151                    description: Some("Tags to apply to the bucket".to_string()),
152                    response_accessor: None,
153                },
154            ],
155            outputs: vec![
156                FieldDefinition {
157                    name: "location".to_string(),
158                    field_type: hemmer_provider_generator_common::FieldType::String,
159                    required: false,
160                    sensitive: false,
161                    immutable: false,
162                    description: Some("Bucket location/region".to_string()),
163                    response_accessor: Some("location".to_string()),
164                },
165                FieldDefinition {
166                    name: "arn".to_string(),
167                    field_type: hemmer_provider_generator_common::FieldType::String,
168                    required: true,
169                    sensitive: false,
170                    immutable: true,
171                    description: Some("Amazon Resource Name (ARN) of the bucket".to_string()),
172                    response_accessor: Some("arn".to_string()),
173                },
174            ],
175            // Nested blocks will be detected in future parser enhancements
176            blocks: vec![],
177            id_field: None, // Will implement ID detection later
178            operations: Operations {
179                create: Some(OperationMapping {
180                    sdk_operation: "create_bucket".to_string(),
181                    additional_operations: vec![],
182                }),
183                read: Some(OperationMapping {
184                    sdk_operation: "head_bucket".to_string(),
185                    additional_operations: vec!["get_bucket_location".to_string()],
186                }),
187                update: Some(OperationMapping {
188                    sdk_operation: "put_bucket_tagging".to_string(),
189                    additional_operations: vec!["put_bucket_acl".to_string()],
190                }),
191                delete: Some(OperationMapping {
192                    sdk_operation: "delete_bucket".to_string(),
193                    additional_operations: vec![],
194                }),
195                import: None, // Will implement later
196            },
197        }
198    }
199
200    /// Group operations by resource name
201    ///
202    /// Automatically discovers resources from operation names.
203    fn group_operations_by_resource(
204        &self,
205        operations: Vec<String>,
206    ) -> HashMap<String, Vec<(String, CrudOperation)>> {
207        let mut grouped: HashMap<String, Vec<(String, CrudOperation)>> = HashMap::new();
208
209        for op in operations {
210            if let Some(crud) = OperationClassifier::classify(&op) {
211                let resource = OperationClassifier::extract_resource(&op);
212                grouped.entry(resource).or_default().push((op, crud));
213            }
214        }
215
216        grouped
217    }
218
219    /// Build a resource definition from discovered operations
220    ///
221    /// Extracts field definitions from Input/Output types in rustdoc JSON.
222    fn build_resource_from_operations(
223        &self,
224        crate_data: &rustdoc_types::Crate,
225        resource_name: &str,
226        operations: Vec<(String, CrudOperation)>,
227    ) -> ResourceDefinition {
228        let mut ops = Operations {
229            create: None,
230            read: None,
231            update: None,
232            delete: None,
233            import: None, // Will implement later
234        };
235
236        let mut create_input_struct = None;
237        let mut read_output_struct = None;
238
239        // Map operations to CRUD
240        for (op_name, crud_type) in operations {
241            let mapping = OperationMapping {
242                sdk_operation: op_name.clone(),
243                additional_operations: vec![],
244            };
245
246            match crud_type {
247                CrudOperation::Create => {
248                    if ops.create.is_none() {
249                        // Track the Input struct name for field extraction
250                        // AWS SDK convention: {Operation}Input
251                        let input_name = self.to_pascal_case(&op_name) + "Input";
252                        create_input_struct = Some(input_name);
253                        ops.create = Some(mapping);
254                    }
255                },
256                CrudOperation::Read => {
257                    if ops.read.is_none() {
258                        // Track the Output struct name for output extraction
259                        let output_name = self.to_pascal_case(&op_name) + "Output";
260                        read_output_struct = Some(output_name);
261                        ops.read = Some(mapping);
262                    }
263                },
264                CrudOperation::Update => {
265                    if ops.update.is_none() {
266                        ops.update = Some(mapping);
267                    }
268                },
269                CrudOperation::Delete => {
270                    if ops.delete.is_none() {
271                        ops.delete = Some(mapping);
272                    }
273                },
274            }
275        }
276
277        // Extract fields from Create operation's Input struct
278        let fields = if let Some(input_struct) = create_input_struct {
279            RustdocLoader::extract_struct_fields(crate_data, &input_struct)
280        } else {
281            vec![]
282        };
283
284        // Extract outputs from Read operation's Output struct
285        let outputs = if let Some(output_struct) = read_output_struct {
286            RustdocLoader::extract_struct_fields(crate_data, &output_struct)
287        } else {
288            vec![]
289        };
290
291        ResourceDefinition {
292            name: resource_name.to_string(),
293            description: Some(format!(
294                "{} resource (auto-discovered from SDK)",
295                resource_name
296            )),
297            fields,
298            outputs,
299            // Nested blocks will be detected in future parser enhancements
300            blocks: vec![],
301            id_field: None, // Will implement ID detection later
302            operations: ops,
303        }
304    }
305
306    /// Convert snake_case to PascalCase
307    ///
308    /// AWS SDK operations are in snake_case (create_bucket)
309    /// but struct names are in PascalCase (CreateBucketInput)
310    fn to_pascal_case(&self, s: &str) -> String {
311        s.split('_')
312            .map(|word| {
313                let mut chars = word.chars();
314                match chars.next() {
315                    None => String::new(),
316                    Some(first) => first.to_uppercase().chain(chars).collect(),
317                }
318            })
319            .collect()
320    }
321}
322
323/// Implementation of SdkParser trait for AWS SDK
324impl hemmer_provider_generator_common::SdkParser for AwsParser {
325    fn parse(&self) -> Result<ServiceDefinition> {
326        // Delegate to existing parse method
327        Self::parse(self)
328    }
329
330    fn supported_services(&self) -> Vec<String> {
331        // In hardcoded mode, only S3 is supported
332        // In rustdoc JSON mode, any service can be parsed
333        if self.rustdoc_json_path.is_some() {
334            // With rustdoc JSON, any AWS service is supported
335            vec![self.service_name.clone()]
336        } else {
337            // Hardcoded mode only supports S3
338            vec!["s3".to_string()]
339        }
340    }
341
342    fn metadata(&self) -> hemmer_provider_generator_common::SdkMetadata {
343        hemmer_provider_generator_common::SdkMetadata {
344            provider: Provider::Aws,
345            sdk_version: self.sdk_version.clone(),
346            sdk_name: "aws-sdk-rust".to_string(),
347        }
348    }
349}
350
351#[cfg(test)]
352mod tests {
353    use super::*;
354
355    #[test]
356    fn test_parse_s3_service() {
357        let parser = AwsParser::new("s3", "1.0.0");
358        let result = parser.parse();
359
360        assert!(result.is_ok());
361        let service = result.unwrap();
362
363        assert_eq!(service.provider, Provider::Aws);
364        assert_eq!(service.name, "s3");
365        assert_eq!(service.sdk_version, "1.0.0");
366        assert_eq!(service.resources.len(), 1);
367
368        let bucket = &service.resources[0];
369        assert_eq!(bucket.name, "bucket");
370        assert_eq!(bucket.fields.len(), 3);
371        assert_eq!(bucket.outputs.len(), 2);
372
373        // Check operations are mapped
374        assert!(bucket.operations.create.is_some());
375        assert!(bucket.operations.read.is_some());
376        assert!(bucket.operations.update.is_some());
377        assert!(bucket.operations.delete.is_some());
378    }
379
380    #[test]
381    fn test_parse_unsupported_service() {
382        let parser = AwsParser::new("ec2", "1.0.0");
383        let result = parser.parse();
384
385        assert!(result.is_err());
386    }
387
388    #[test]
389    fn test_group_operations() {
390        let parser = AwsParser::new("s3", "1.0.0");
391        let operations = vec![
392            "create_bucket".to_string(),
393            "delete_bucket".to_string(),
394            "get_bucket_location".to_string(),
395            "put_object".to_string(),
396            "get_object".to_string(),
397        ];
398
399        let grouped = parser.group_operations_by_resource(operations);
400
401        assert_eq!(grouped.len(), 2); // bucket and object
402        assert!(grouped.contains_key("bucket"));
403        assert!(grouped.contains_key("object"));
404        assert_eq!(grouped["bucket"].len(), 3); // create, delete, get
405        assert_eq!(grouped["object"].len(), 2); // put, get
406    }
407
408    #[test]
409    fn test_sdk_parser_trait() {
410        use hemmer_provider_generator_common::SdkParser;
411
412        let parser = AwsParser::new("s3", "1.0.0");
413
414        // Test parse method through trait
415        let result = SdkParser::parse(&parser);
416        assert!(result.is_ok());
417
418        // Test supported_services
419        let services = parser.supported_services();
420        assert_eq!(services, vec!["s3"]);
421
422        // Test metadata
423        let metadata = parser.metadata();
424        assert_eq!(metadata.provider, Provider::Aws);
425        assert_eq!(metadata.sdk_version, "1.0.0");
426        assert_eq!(metadata.sdk_name, "aws-sdk-rust");
427    }
428
429    #[test]
430    fn test_sdk_parser_trait_with_rustdoc() {
431        use hemmer_provider_generator_common::SdkParser;
432        use std::path::PathBuf;
433
434        let parser =
435            AwsParser::with_rustdoc_json("s3", "1.0.0", PathBuf::from("/path/to/rustdoc.json"));
436
437        // Test supported_services with rustdoc JSON path
438        let services = parser.supported_services();
439        assert_eq!(services, vec!["s3"]);
440
441        // Test metadata
442        let metadata = parser.metadata();
443        assert_eq!(metadata.provider, Provider::Aws);
444        assert_eq!(metadata.sdk_name, "aws-sdk-rust");
445    }
446}