openapi_nexus_go/
generator.rs

1//! Go HTTP code generator
2
3use std::collections::BTreeMap;
4use std::error::Error;
5
6use heck::{ToKebabCase as _, ToLowerCamelCase as _, ToPascalCase as _, ToSnakeCase as _};
7use minijinja::context;
8use tracing::warn;
9use utoipa::openapi;
10use utoipa::openapi::OpenApi;
11
12use crate::ast::GoStruct;
13use crate::ast::ty::GoTypeDefinition;
14use crate::config::GoHttpConfig;
15use crate::consts::{MAX_LINE_WIDTH, escape_go_keyword};
16use crate::errors::GeneratorError;
17use crate::templating::data::{
18    ApiOperationData, CommonFileHeaderData, GoApiMethodData, GoParameterInfo, MainSdkData,
19    ModelStructData, ModelTypeAliasData, OperationResponse, OperationsData, SubClientInfo,
20};
21use crate::templating::{TemplateName, Templates};
22use crate::type_mapping;
23use openapi_nexus_common::{GeneratorType, Language};
24use openapi_nexus_core::data::{
25    ApiMethodData, HeaderData, ModelData, OperationInfo, ParameterInfo, ReadmeData, RuntimeData,
26};
27use openapi_nexus_core::traits::OpenApiRefExt as _;
28use openapi_nexus_core::traits::ToRcDoc;
29use openapi_nexus_core::traits::code_generator::CodeGenerator;
30use openapi_nexus_core::traits::file_writer::{FileInfo, FileWriter};
31
32/// Go HTTP code generator
33#[derive(Debug, Clone)]
34pub struct GoHttpCodeGenerator {
35    config: GoHttpConfig,
36    templates: Templates,
37}
38
39impl GoHttpCodeGenerator {
40    /// Create a new Go HTTP generator
41    ///
42    /// # Arguments
43    /// * `config` - TOML config table
44    pub fn new(config: toml::value::Table) -> Self {
45        let parsed_config = GoHttpConfig::from(config);
46        let templates = Templates::new();
47        Self {
48            config: parsed_config,
49            templates,
50        }
51    }
52
53    /// Generate filename based on naming convention
54    fn generate_filename(&self, name: &str) -> String {
55        let base_name = match self.config.file_naming_convention {
56            openapi_nexus_core::NamingConvention::CamelCase => name.to_lower_camel_case(),
57            openapi_nexus_core::NamingConvention::KebabCase => name.to_kebab_case(),
58            openapi_nexus_core::NamingConvention::SnakeCase => name.to_snake_case(),
59            openapi_nexus_core::NamingConvention::PascalCase => name.to_pascal_case(),
60        };
61        // Escape reserved keywords in filename to avoid issues
62        let escaped_name = escape_go_keyword(&base_name);
63
64        format!("{}.go", escaped_name)
65    }
66
67    /// Convert ParameterInfo to GoParameterInfo
68    fn convert_parameter(
69        &self,
70        param: &ParameterInfo,
71        components: Option<&utoipa::openapi::Components>,
72    ) -> Result<GoParameterInfo, GeneratorError> {
73        // Convert schema to Go type
74        let go_type = if let Some(schema_ref) = &param.schema {
75            let go_expr = type_mapping::schema_to_go_expression(schema_ref, components)?;
76            // Convert GoExpression to string
77            let doc = go_expr.to_rcdoc();
78            doc.pretty(MAX_LINE_WIDTH).to_string().trim().to_string()
79        } else {
80            "string".to_string() // Default to string if no schema
81        };
82
83        let param_name_pascal = escape_go_keyword(&param.param_name.to_pascal_case());
84        let param_name_camel = escape_go_keyword(&param.param_name.to_lower_camel_case());
85
86        Ok(GoParameterInfo {
87            original_name: param.original_name.clone(),
88            param_name: param_name_pascal,
89            param_name_camel,
90            go_type,
91            required: param.required,
92            description: param.description.clone(),
93        })
94    }
95
96    /// Get module path from config or return default
97    fn get_module_path(&self) -> String {
98        self.config
99            .module_path
100            .clone()
101            .unwrap_or_else(|| "example.com/sdk".to_string())
102    }
103
104    /// Group APIs by their tags based on matching operations
105    fn group_apis_by_tag<'a>(
106        &self,
107        apis: &'a [ApiMethodData],
108        operations_by_tag: &std::collections::HashMap<String, Vec<OperationInfo>>,
109    ) -> BTreeMap<String, Vec<&'a ApiMethodData>> {
110        let mut apis_by_tag: BTreeMap<String, Vec<&'a ApiMethodData>> = BTreeMap::new();
111        for api in apis {
112            // Extract tag from operation (we'll need to match by path/method)
113            // For now, group by finding matching operations
114            for (tag, operations) in operations_by_tag {
115                for op_info in operations {
116                    if op_info.path == api.path && op_info.method == api.http_method {
117                        apis_by_tag.entry(tag.clone()).or_default().push(api);
118                        break;
119                    }
120                }
121            }
122        }
123        apis_by_tag
124    }
125
126    /// Convert a single ApiMethodData to GoApiMethodData
127    fn convert_api_to_go_method(
128        &self,
129        api: &ApiMethodData,
130        operations: &[OperationInfo],
131        components: Option<&utoipa::openapi::Components>,
132    ) -> Result<GoApiMethodData, GeneratorError> {
133        // Find matching operation for additional details
134        let op_info = operations
135            .iter()
136            .find(|op| op.path == api.path && op.method == api.http_method);
137
138        // Convert method name to PascalCase for Go
139        let method_name = api.method_name.to_pascal_case();
140
141        // Convert parameters
142        let path_params: Result<Vec<GoParameterInfo>, GeneratorError> = api
143            .path_params
144            .iter()
145            .map(|p| self.convert_parameter(p, components))
146            .collect();
147        let query_params: Result<Vec<GoParameterInfo>, GeneratorError> = api
148            .query_params
149            .iter()
150            .map(|p| self.convert_parameter(p, components))
151            .collect();
152        let header_params: Result<Vec<GoParameterInfo>, GeneratorError> = api
153            .header_params
154            .iter()
155            .map(|p| self.convert_parameter(p, components))
156            .collect();
157
158        // Extract request body content type
159        let request_body_content_type = api
160            .request_body
161            .as_ref()
162            .and_then(|rb| {
163                // Prefer application/json, but use first available content type
164                rb.content
165                    .get("application/json")
166                    .map(|_| "application/json")
167                    .or_else(|| rb.content.keys().next().map(|k| k.as_str()))
168            })
169            .unwrap_or("application/json")
170            .to_string();
171
172        // Extract request body type name
173        let request_body_type = api
174            .request_body
175            .as_ref()
176            .and_then(|rb| rb.content.get("application/json"))
177            .and_then(|json_content| json_content.schema.as_ref())
178            .map(|schema_ref| {
179                match schema_ref {
180                    // For inline schemas, generate a new type name
181                    openapi::RefOr::T(_) => format!("{}Request", method_name),
182                    // For references, extract schema name using OpenApiRefExt trait
183                    openapi::RefOr::Ref(reference) => {
184                        // Use schema_name() method for consistent extraction
185                        if let Some(schema_name) = reference.schema_name() {
186                            schema_name.to_pascal_case()
187                        } else {
188                            // Fallback to method name + Request if we can't extract the schema name
189                            format!("{}Request", method_name)
190                        }
191                    }
192                }
193            });
194
195        Ok(GoApiMethodData {
196            name: method_name.clone(),
197            http_method: api.http_method.as_str().to_uppercase(),
198            path: api.path.clone(),
199            operation_id: op_info
200                .and_then(|op| op.operation.operation_id.clone())
201                .unwrap_or_else(|| method_name.clone()),
202            path_params: path_params?,
203            query_params: query_params?,
204            header_params: header_params?,
205            body_param: None, // Request body is handled as a separate parameter
206            has_request_body: api.request_body.is_some(),
207            request_body_content_type,
208            request_body_type,
209            response_type: None, // TODO: Extract from return_type
210            description: op_info.and_then(|op| op.operation.description.clone()),
211        })
212    }
213
214    /// Build import list for API client files
215    fn build_api_imports(&self, go_methods: &[GoApiMethodData], module_path: &str) -> Vec<String> {
216        // Check if any method needs io import (when has_request_body is false)
217        let needs_io_import = go_methods.iter().any(|method| !method.has_request_body);
218
219        // Collect and separate std imports from project imports
220        let mut std_imports = vec![
221            "context".to_string(),
222            "fmt".to_string(),
223            "net/http".to_string(),
224            "net/url".to_string(),
225        ];
226        if needs_io_import {
227            std_imports.push("io".to_string());
228        }
229        std_imports.sort();
230
231        let mut project_imports = vec![
232            format!("{}/internal/config", module_path),
233            format!("{}/internal/hooks", module_path),
234            format!("{}/internal/utils", module_path),
235            format!("{}/models/components", module_path),
236            format!("{}/models/operations", module_path),
237        ];
238
239        project_imports.sort();
240
241        // Combine: std imports, empty string separator, project imports
242        let mut imports = std_imports;
243        imports.push(String::new()); // Empty string as separator for newline
244        imports.extend(project_imports);
245        imports
246    }
247
248    /// Generate API client file for a single tag
249    fn generate_api_client_file(
250        &self,
251        tag: &str,
252        operations: &[OperationInfo],
253        tag_apis: &[&ApiMethodData],
254        openapi: &OpenApi,
255        common_header: &CommonFileHeaderData,
256        module_path: &str,
257    ) -> Result<FileInfo, Box<dyn Error + Send + Sync>> {
258        // Convert core ApiMethodData to Go-specific GoApiMethodData
259        let components = openapi.components.as_ref();
260        let go_methods: Result<Vec<GoApiMethodData>, GeneratorError> = tag_apis
261            .iter()
262            .map(|api| self.convert_api_to_go_method(api, operations, components))
263            .collect();
264
265        let go_methods = go_methods?;
266
267        // Create client struct (escape reserved keywords)
268        let client_struct = GoStruct::new(escape_go_keyword(&tag.to_pascal_case()));
269
270        // Get SDK name from OpenAPI title
271        let sdk_name = openapi.info.title.to_pascal_case();
272
273        // Build imports
274        let imports = self.build_api_imports(&go_methods, module_path);
275
276        let api_data = ApiOperationData::new(
277            client_struct,
278            escape_go_keyword(tag),
279            sdk_name,
280            common_header.clone(),
281        )
282        .with_methods(go_methods)
283        .with_imports(imports);
284
285        // Wrap in context with api_operation key (matching template expectations)
286        let template_context = context! {
287            api_operation => api_data,
288            common_file_header => common_header,
289            module_path => module_path,
290        };
291        let filename = self.generate_filename(tag);
292        let file_info = self.templates.render_template(
293            TemplateName::ApiOperation,
294            &filename,
295            template_context,
296        )?;
297        Ok(file_info)
298    }
299
300    /// Generate operations types file
301    fn generate_operations_file(
302        &self,
303        apis: &[ApiMethodData],
304        common_header: &CommonFileHeaderData,
305        module_path: &str,
306    ) -> Result<FileInfo, Box<dyn Error + Send + Sync>> {
307        let mut responses = Vec::new();
308        for api in apis {
309            let method_name = api.method_name.to_pascal_case();
310            responses.push(OperationResponse {
311                name: format!("{}Response", method_name),
312                operation_name: method_name.clone(),
313                body_type: None, // TODO: Extract from return_type
314            });
315        }
316        // Sort responses by name to ensure stable ordering across generations
317        responses.sort_by(|a, b| a.name.cmp(&b.name));
318        let operations_data =
319            OperationsData::new(responses, common_header.clone(), module_path.to_string());
320        let operations_context = context! {
321            operations => operations_data,
322            common_file_header => common_header,
323            module_path => module_path,
324        };
325        let operations_file = self.templates.render_template(
326            TemplateName::ModelOperations,
327            "operations/operations.go",
328            operations_context,
329        )?;
330        Ok(operations_file)
331    }
332
333    /// Generate main SDK file
334    fn generate_main_sdk_file(
335        &self,
336        openapi: &OpenApi,
337        apis_by_tag: &BTreeMap<String, Vec<&ApiMethodData>>,
338        common_header: &CommonFileHeaderData,
339        module_path: &str,
340    ) -> Result<FileInfo, Box<dyn Error + Send + Sync>> {
341        let sdk_name: String = openapi.info.title.to_pascal_case();
342        let package_name: String = self
343            .config
344            .package_name
345            .as_ref()
346            .map(|s| s.to_snake_case())
347            .unwrap_or_else(|| "sdk".to_string());
348
349        // Collect sub-clients from all tags
350        let mut sub_clients: Vec<SubClientInfo> = Vec::new();
351        for tag in apis_by_tag.keys() {
352            let client_name = escape_go_keyword(&tag.to_pascal_case());
353            sub_clients.push(SubClientInfo {
354                name: escape_go_keyword(&tag.to_lower_camel_case()),
355                type_name: client_name.clone(),
356            });
357        }
358
359        let main_sdk_data = MainSdkData::new(sdk_name.clone(), package_name, common_header.clone())
360            .with_sub_clients(sub_clients);
361        let template_context = context! {
362            main_sdk => main_sdk_data,
363            common_file_header => common_header,
364            module_path => module_path,
365        };
366        // SDK file goes in sdk/ subdirectory so it can be imported
367        let sdk_filename = if let Some(pkg) = &self.config.package_name {
368            format!("sdk/{}.go", pkg.to_snake_case())
369        } else {
370            format!("sdk/{}.go", sdk_name.to_snake_case())
371        };
372        let file_info = self.templates.render_template(
373            TemplateName::MainSdk,
374            &sdk_filename,
375            template_context,
376        )?;
377        Ok(file_info)
378    }
379
380    /// Build model imports with module path and time import detection
381    fn build_model_imports(
382        &self,
383        imports: &[String],
384        module_path: &str,
385        type_def: &GoTypeDefinition,
386    ) -> Vec<String> {
387        // Add module path to imports
388        let mut full_imports: Vec<String> = imports
389            .iter()
390            .map(|imp| {
391                if imp.starts_with("optionalnullable") {
392                    format!("{}/runtime/{}", module_path, imp)
393                } else if imp.starts_with("internal/") {
394                    // Internal packages are now at root level (Option B)
395                    format!("{}/{}", module_path, imp)
396                } else {
397                    imp.clone()
398                }
399            })
400            .collect();
401
402        // Check if the type definition uses time types and add time import if needed
403        let type_def_str = match type_def {
404            GoTypeDefinition::Struct(s) => {
405                let doc = s.to_rcdoc();
406                doc.pretty(MAX_LINE_WIDTH).to_string()
407            }
408            GoTypeDefinition::TypeAlias(t) => {
409                let doc = t.to_rcdoc();
410                doc.pretty(MAX_LINE_WIDTH).to_string()
411            }
412        };
413        if (type_def_str.contains("time.Time") || type_def_str.contains("time.Duration"))
414            && !full_imports.iter().any(|imp| imp == "time")
415        {
416            full_imports.push("time".to_string());
417        }
418
419        full_imports
420    }
421
422    /// Process a single model and generate its file
423    fn process_model(
424        &self,
425        model: &ModelData,
426        components: &utoipa::openapi::Components,
427        header_data: &HeaderData,
428        common_header: &CommonFileHeaderData,
429        module_path: &str,
430    ) -> Result<FileInfo, Box<dyn Error + Send + Sync>> {
431        let (type_def, imports, required_fields) = type_mapping::generate_model_data(
432            &model.name,
433            &model.schema,
434            components,
435            &self.config,
436            header_data,
437        )
438        .map_err(|e| GeneratorError::ModelGeneration {
439            model_name: model.name.clone(),
440            source: Box::new(e),
441        })?;
442
443        // Build imports with module path and time detection
444        let full_imports = self.build_model_imports(&imports, module_path, &type_def);
445
446        let filename = format!("components/{}", self.generate_filename(&model.name));
447        let (template_context, template_name) = match type_def {
448            GoTypeDefinition::Struct(s) => {
449                let model_data = ModelStructData::new(s, common_header.clone())
450                    .with_imports(full_imports)
451                    .with_required_fields(required_fields);
452                (
453                    context! {
454                        model_struct => model_data,
455                        common_file_header => common_header,
456                    },
457                    TemplateName::ModelStruct,
458                )
459            }
460            GoTypeDefinition::TypeAlias(t) => {
461                let model_data =
462                    ModelTypeAliasData::new(t, common_header.clone()).with_imports(full_imports);
463                (
464                    context! {
465                        model_type_alias => model_data,
466                        common_file_header => common_header,
467                    },
468                    TemplateName::ModelTypeAlias,
469                )
470            }
471        };
472
473        let file_info =
474            self.templates
475                .render_template(template_name, &filename, template_context)?;
476        Ok(file_info)
477    }
478
479    /// Generate internal runtime files
480    fn generate_internal_runtime_files(
481        &self,
482        internal_files: &[(&str, TemplateName)],
483        common_header: &CommonFileHeaderData,
484        module_path: &str,
485    ) -> Result<Vec<FileInfo>, Box<dyn Error + Send + Sync>> {
486        let mut files = Vec::new();
487        for (filename, template_name) in internal_files {
488            let template_context = context! {
489                common_file_header => common_header,
490                module_path => module_path,
491            };
492            let content = self
493                .templates
494                .render_template_string(*template_name, template_context)?;
495            // Use ProjectFiles category so files go to root level
496            files.push(FileInfo::project(filename.to_string(), content));
497        }
498        Ok(files)
499    }
500
501    /// Generate runtime type files
502    fn generate_runtime_type_files(
503        &self,
504        runtime_files: &[(&str, TemplateName)],
505        common_header: &CommonFileHeaderData,
506        module_path: &str,
507    ) -> Result<Vec<FileInfo>, Box<dyn Error + Send + Sync>> {
508        let mut files = Vec::new();
509        for (filename, template_name) in runtime_files {
510            let template_context = context! {
511                common_file_header => common_header,
512                module_path => module_path,
513            };
514            let file_info =
515                self.templates
516                    .render_template(*template_name, filename, template_context)?;
517            files.push(file_info);
518        }
519        Ok(files)
520    }
521
522    /// Generate request body models for inline schemas
523    fn generate_request_body_models(
524        &self,
525        apis: &[ApiMethodData],
526        components: Option<&utoipa::openapi::Components>,
527        header_data: &HeaderData,
528        common_header: &CommonFileHeaderData,
529        module_path: &str,
530    ) -> Vec<FileInfo> {
531        let mut files = Vec::new();
532
533        for api in apis {
534            // Extract inline request body schema
535            let Some(request_body) = &api.request_body else {
536                continue;
537            };
538            let Some(json_content) = request_body.content.get("application/json") else {
539                continue;
540            };
541            let Some(schema_ref) = &json_content.schema else {
542                continue;
543            };
544
545            // Only generate a new model if it's an inline schema (not a reference)
546            // If it's a reference, the model already exists and will be used directly
547            let openapi::RefOr::T(_) = schema_ref else {
548                continue;
549            };
550
551            let method_name = api.method_name.to_pascal_case();
552            let request_type_name = format!("{}Request", method_name);
553
554            // Generate model for request body
555            let Some(components_ref) = components else {
556                continue;
557            };
558
559            let model_data = ModelData {
560                name: request_type_name.clone(),
561                schema: schema_ref.clone(),
562            };
563
564            if let Ok(file_info) = self.process_model(
565                &model_data,
566                components_ref,
567                header_data,
568                common_header,
569                module_path,
570            ) {
571                files.push(file_info);
572            } else {
573                // Log error but continue - some request bodies might not be generatable
574                warn!(
575                    request_type_name = %request_type_name,
576                    "Failed to generate request body type"
577                );
578            }
579        }
580
581        files
582    }
583}
584
585impl CodeGenerator for GoHttpCodeGenerator {
586    fn language(&self) -> Language {
587        Language::Go
588    }
589
590    fn generator_type(&self) -> GeneratorType {
591        GeneratorType::GoHttp
592    }
593
594    fn generate_apis(
595        &self,
596        openapi: &OpenApi,
597        apis: Vec<ApiMethodData>,
598    ) -> Result<Vec<FileInfo>, Box<dyn Error + Send + Sync>> {
599        let operations_by_tag = <Self as CodeGenerator>::collect_operations_by_tag(self, openapi);
600        let header_data = HeaderData::from_openapi(openapi);
601        let common_header = CommonFileHeaderData::from(header_data.clone());
602        let module_path = self.get_module_path();
603        let components = openapi.components.as_ref();
604
605        // Group APIs by tag
606        let apis_by_tag = self.group_apis_by_tag(&apis, &operations_by_tag);
607
608        // Generate request body models for inline schemas
609        let mut files = self.generate_request_body_models(
610            &apis,
611            components,
612            &header_data,
613            &common_header,
614            &module_path,
615        );
616
617        // Generate API client files for each tag
618        for (tag, operations) in operations_by_tag {
619            let tag_apis: Vec<&ApiMethodData> = apis_by_tag.get(&tag).cloned().unwrap_or_default();
620
621            let file_info = self.generate_api_client_file(
622                &tag,
623                &operations,
624                &tag_apis,
625                openapi,
626                &common_header,
627                &module_path,
628            )?;
629            files.push(file_info);
630        }
631
632        // Generate operations types file
633        files.push(self.generate_operations_file(&apis, &common_header, &module_path)?);
634
635        // Generate main SDK file
636        files.push(self.generate_main_sdk_file(
637            openapi,
638            &apis_by_tag,
639            &common_header,
640            &module_path,
641        )?);
642
643        Ok(files)
644    }
645
646    fn generate_models(
647        &self,
648        openapi: &OpenApi,
649        models: Vec<ModelData>,
650    ) -> Result<Vec<FileInfo>, Box<dyn Error + Send + Sync>> {
651        let header_data = HeaderData::from_openapi(openapi);
652        let common_header = CommonFileHeaderData::from(header_data.clone());
653        let mut files = Vec::new();
654
655        let module_path = self.get_module_path();
656
657        if let Some(components) = &openapi.components {
658            for model in models {
659                let file_info = self.process_model(
660                    &model,
661                    components,
662                    &header_data,
663                    &common_header,
664                    &module_path,
665                )?;
666                files.push(file_info);
667            }
668        }
669
670        // Always generate HTTPMetadata type in components package (needed by operations)
671        let httpmetadata_context = context! {
672            common_file_header => common_header.clone(),
673        };
674        let httpmetadata_file = self.templates.render_template(
675            TemplateName::ModelHttpMetadata,
676            "components/httpmetadata.go",
677            httpmetadata_context,
678        )?;
679        files.push(httpmetadata_file);
680
681        Ok(files)
682    }
683
684    fn generate_runtime(
685        &self,
686        openapi: &OpenApi,
687        _: RuntimeData,
688    ) -> Result<Vec<FileInfo>, Box<dyn Error + Send + Sync>> {
689        let header_data = HeaderData::from_openapi(openapi);
690        let common_header = CommonFileHeaderData::from(header_data);
691        let mut files = Vec::new();
692
693        // Generate runtime utility files
694        // Internal runtime files.
695        let internal_files = vec![
696            ("internal/utils/utils.go", TemplateName::RuntimeUtils),
697            (
698                "internal/utils/requestbody.go",
699                TemplateName::RuntimeRequestBody,
700            ),
701            (
702                "internal/utils/queryparams.go",
703                TemplateName::RuntimeQueryParams,
704            ),
705            (
706                "internal/utils/pathparams.go",
707                TemplateName::RuntimePathParams,
708            ),
709            ("internal/utils/headers.go", TemplateName::RuntimeHeaders),
710            ("internal/utils/json.go", TemplateName::RuntimeJson),
711            (
712                "internal/utils/contenttype.go",
713                TemplateName::RuntimeContentType,
714            ),
715            ("internal/utils/form.go", TemplateName::RuntimeForm),
716            ("internal/utils/retries.go", TemplateName::RuntimeRetries),
717            ("internal/utils/security.go", TemplateName::RuntimeSecurity),
718            ("internal/utils/env.go", TemplateName::RuntimeEnv),
719            (
720                "internal/config/sdkconfiguration.go",
721                TemplateName::RuntimeConfig,
722            ),
723            ("internal/hooks/hooks.go", TemplateName::RuntimeHooks),
724            (
725                "internal/hooks/registration.go",
726                TemplateName::RuntimeHooksRegistration,
727            ),
728        ];
729
730        let runtime_files = vec![
731            ("retry/config.go", TemplateName::RuntimeRetryConfig),
732            ("types/pointers.go", TemplateName::TypesPointers),
733            ("types/date.go", TemplateName::TypesDate),
734            ("types/datetime.go", TemplateName::TypesDateTime),
735            ("types/bigint.go", TemplateName::TypesBigInt),
736            (
737                "optionalnullable/optionalnullable.go",
738                TemplateName::TypesOptionalNullable,
739            ),
740        ];
741
742        let module_path = self.get_module_path();
743
744        // Generate internal files (go to root level with ProjectFiles category)
745        let mut internal_file_infos =
746            self.generate_internal_runtime_files(&internal_files, &common_header, &module_path)?;
747        files.append(&mut internal_file_infos);
748
749        // Generate other runtime files (stay in runtime/ directory)
750        let mut runtime_file_infos =
751            self.generate_runtime_type_files(&runtime_files, &common_header, &module_path)?;
752        files.append(&mut runtime_file_infos);
753
754        Ok(files)
755    }
756
757    fn generate_project_files(
758        &self,
759        _openapi: &OpenApi,
760    ) -> Result<Vec<FileInfo>, Box<dyn Error + Send + Sync>> {
761        let module_path = self.get_module_path();
762
763        let template_context = context! {
764            module_path => module_path,
765        };
766
767        let file_info =
768            self.templates
769                .render_template(TemplateName::GoMod, "go.mod", template_context)?;
770
771        Ok(vec![file_info])
772    }
773
774    fn generate_readme(
775        &self,
776        _openapi: &OpenApi,
777        data: ReadmeData,
778    ) -> Result<Vec<FileInfo>, Box<dyn Error + Send + Sync>> {
779        let default_module = "example.com/sdk".to_string();
780        let module_path = self.config.module_path.as_ref().unwrap_or(&default_module);
781
782        let template_context = context! {
783            package_name => data.package_name,
784            description => data.description,
785            version => data.version,
786            module_path => module_path,
787        };
788
789        let file_info =
790            self.templates
791                .render_template(TemplateName::Readme, "README.md", template_context)?;
792
793        Ok(vec![file_info])
794    }
795}
796
797impl FileWriter for GoHttpCodeGenerator {}