Skip to main content

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