1use 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#[derive(Debug, Clone)]
34pub struct GoHttpCodeGenerator {
35 config: GoHttpConfig,
36 templates: Templates,
37}
38
39impl GoHttpCodeGenerator {
40 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 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 let escaped_name = escape_go_keyword(&base_name);
63
64 format!("{}.go", escaped_name)
65 }
66
67 fn convert_parameter(
69 &self,
70 param: &ParameterInfo,
71 components: Option<&utoipa::openapi::Components>,
72 ) -> Result<GoParameterInfo, GeneratorError> {
73 let go_type = if let Some(schema_ref) = ¶m.schema {
75 let go_expr = type_mapping::schema_to_go_expression(schema_ref, components)?;
76 let doc = go_expr.to_rcdoc();
78 doc.pretty(MAX_LINE_WIDTH).to_string().trim().to_string()
79 } else {
80 "string".to_string() };
82
83 let param_name_pascal = escape_go_keyword(¶m.param_name.to_pascal_case());
84 let param_name_camel = escape_go_keyword(¶m.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 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 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 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 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 let op_info = operations
135 .iter()
136 .find(|op| op.path == api.path && op.method == api.http_method);
137
138 let method_name = api.method_name.to_pascal_case();
140
141 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 let request_body_content_type = api
160 .request_body
161 .as_ref()
162 .and_then(|rb| {
163 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 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 openapi::RefOr::T(_) => format!("{}Request", method_name),
182 openapi::RefOr::Ref(reference) => {
184 if let Some(schema_name) = reference.schema_name() {
186 schema_name.to_pascal_case()
187 } else {
188 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, has_request_body: api.request_body.is_some(),
207 request_body_content_type,
208 request_body_type,
209 response_type: None, description: op_info.and_then(|op| op.operation.description.clone()),
211 })
212 }
213
214 fn build_api_imports(&self, go_methods: &[GoApiMethodData], module_path: &str) -> Vec<String> {
216 let needs_io_import = go_methods.iter().any(|method| !method.has_request_body);
218
219 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 let mut imports = std_imports;
243 imports.push(String::new()); imports.extend(project_imports);
245 imports
246 }
247
248 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 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 let client_struct = GoStruct::new(escape_go_keyword(&tag.to_pascal_case()));
269
270 let sdk_name = openapi.info.title.to_pascal_case();
272
273 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 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 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, });
315 }
316 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 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 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 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 fn build_model_imports(
382 &self,
383 imports: &[String],
384 module_path: &str,
385 type_def: &GoTypeDefinition,
386 ) -> Vec<String> {
387 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 format!("{}/{}", module_path, imp)
396 } else {
397 imp.clone()
398 }
399 })
400 .collect();
401
402 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 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 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 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 files.push(FileInfo::project(filename.to_string(), content));
497 }
498 Ok(files)
499 }
500
501 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 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 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 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 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 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 let apis_by_tag = self.group_apis_by_tag(&apis, &operations_by_tag);
607
608 let mut files = self.generate_request_body_models(
610 &apis,
611 components,
612 &header_data,
613 &common_header,
614 &module_path,
615 );
616
617 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 files.push(self.generate_operations_file(&apis, &common_header, &module_path)?);
634
635 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 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 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 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 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 {}