1use 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
24pub struct AwsParser {
26 service_name: String,
27 sdk_version: String,
28 rustdoc_json_path: Option<PathBuf>,
29}
30
31impl AwsParser {
32 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 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 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 fn parse_from_rustdoc(&self, json_path: &Path) -> Result<ServiceDefinition> {
69 let crate_data = RustdocLoader::load_from_file(json_path)?;
71
72 let operations = RustdocLoader::find_operation_modules(&crate_data);
74
75 let grouped = self.group_operations_by_resource(operations);
77
78 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![], })
93 }
94
95 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 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![], }
116 }
117
118 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, 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 blocks: vec![],
177 id_field: None, 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, },
197 }
198 }
199
200 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 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, };
235
236 let mut create_input_struct = None;
237 let mut read_output_struct = None;
238
239 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 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 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 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 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 blocks: vec![],
301 id_field: None, operations: ops,
303 }
304 }
305
306 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
323impl hemmer_provider_generator_common::SdkParser for AwsParser {
325 fn parse(&self) -> Result<ServiceDefinition> {
326 Self::parse(self)
328 }
329
330 fn supported_services(&self) -> Vec<String> {
331 if self.rustdoc_json_path.is_some() {
334 vec![self.service_name.clone()]
336 } else {
337 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 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); assert!(grouped.contains_key("bucket"));
403 assert!(grouped.contains_key("object"));
404 assert_eq!(grouped["bucket"].len(), 3); assert_eq!(grouped["object"].len(), 2); }
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 let result = SdkParser::parse(&parser);
416 assert!(result.is_ok());
417
418 let services = parser.supported_services();
420 assert_eq!(services, vec!["s3"]);
421
422 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 let services = parser.supported_services();
439 assert_eq!(services, vec!["s3"]);
440
441 let metadata = parser.metadata();
443 assert_eq!(metadata.provider, Provider::Aws);
444 assert_eq!(metadata.sdk_name, "aws-sdk-rust");
445 }
446}