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;
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#[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 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 fn convert_parameter(
77 &self,
78 param: &ParameterInfo,
79 components: Option<&Components>,
80 ) -> Result<GoParameterInfo, GeneratorError> {
81 let go_type = if let Some(schema_ref) = ¶m.schema {
83 let go_expr = type_mapping::schema_to_go_expression(schema_ref, components)?;
84 let doc = go_expr.to_rcdoc();
86 doc.pretty(MAX_LINE_WIDTH).to_string().trim().to_string()
87 } else {
88 "string".to_string() };
90
91 let param_name_pascal = escape_go_keyword(¶m.param_name.to_pascal_case());
92 let param_name_camel = escape_go_keyword(¶m.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 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 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 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 fn convert_api_to_go_method(
136 &self,
137 api: &ApiMethodData,
138 operations: &[OperationInfo],
139 components: Option<&Components>,
140 ) -> Result<GoApiMethodData, GeneratorError> {
141 let op_info = operations
143 .iter()
144 .find(|op| op.path == api.path && op.method == api.http_method);
145
146 let method_name = api.method_name.to_pascal_case();
148
149 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 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 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, has_request_body: api.request_body.is_some(),
213 request_body_content_type,
214 request_body_type,
215 response_type: None, description: op_info.and_then(|op| op.operation.description.clone()),
217 })
218 }
219
220 fn build_api_imports(&self, go_methods: &[GoApiMethodData], module_path: &str) -> Vec<String> {
222 let needs_io_import = go_methods.iter().any(|method| !method.has_request_body);
224
225 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 let mut imports = std_imports;
249 imports.push(String::new()); imports.extend(project_imports);
251 imports
252 }
253
254 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 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 let client_struct = GoStruct::new(escape_go_keyword(&tag.to_pascal_case()));
275
276 let sdk_name = openapi.info.title.to_pascal_case();
278
279 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 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 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, });
321 }
322 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 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 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 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 fn build_model_imports(
388 &self,
389 imports: &[String],
390 module_path: &str,
391 type_def: &GoTypeDefinition,
392 ) -> Vec<String> {
393 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 format!("{}/{}", module_path, imp)
402 } else {
403 imp.clone()
404 }
405 })
406 .collect();
407
408 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 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 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 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 files.push(FileInfo::project(filename.to_string(), content));
503 }
504 Ok(files)
505 }
506
507 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 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 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 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 let apis_by_tag = self.group_apis_by_tag(&apis, &operations_by_tag);
610
611 let mut files = self.generate_request_body_models(
613 &apis,
614 components,
615 &header_data,
616 &common_header,
617 &module_path,
618 );
619
620 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 files.push(self.generate_operations_file(&apis, &common_header, &module_path)?);
637
638 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 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 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 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 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 {}