diff --git a/README.md b/README.md index a9d5768..8a6ea6a 100644 --- a/README.md +++ b/README.md @@ -2,14 +2,17 @@ English | [中文](README_CN.md) -**Swagger Generate** is a collection of plugins that generate Swagger documentation and provide Swagger-UI access for debugging HTTP and RPC services. This project is compatible with the [CloudWeGo](https://www.cloudwego.io) ecosystem frameworks such as [Cwgo](https://github.com/cloudwego/cwgo), [Hertz](https://github.com/cloudwego/hertz), and [Kitex](https://github.com/cloudwego/kitex). It offers a convenient toolset for developers to automatically generate Swagger documentation, simplifying the API documentation and debugging process. +**Swagger Generate** is a set of plugin tools designed for HTTP and RPC services, supporting the automatic generation of Swagger documentation and integration with Swagger-UI for debugging. Additionally, it provides the ability to convert Swagger documents into Protobuf or Thrift IDL files, simplifying the API development and maintenance process. + +This project is compatible with the [CloudWeGo](https://www.cloudwego.io) ecosystem frameworks such as [Cwgo](https://github.com/cloudwego/cwgo), [Hertz](https://github.com/cloudwego/hertz), and [Kitex](https://github.com/cloudwego/kitex). It offers a convenient toolset for developers to automatically generate Swagger documentation, simplifying the API documentation and debugging process. ## Included Plugins -- **protoc-gen-http-swagger**: Generates Swagger documentation and provides Swagger UI debugging for HTTP services based on Protobuf. -- **thrift-gen-http-swagger**: Generates Swagger documentation and provides Swagger UI debugging for HTTP services based on Thrift. -- **protoc-gen-rpc-swagger**: Generates Swagger documentation and provides Swagger UI debugging for RPC services based on Protobuf. -- **thrift-gen-rpc-swagger**: Generates Swagger documentation and provides Swagger UI debugging for RPC services based on Thrift. +- **[protoc-gen-http-swagger](https://github.com/hertz-contrib/swagger-generate/tree/main/thrift-gen-rpc-swagger)**: Generates Swagger documentation and provides Swagger UI debugging for HTTP services based on Protobuf. +- **[thrift-gen-http-swagger](https://github.com/hertz-contrib/swagger-generate/tree/main/thrift-gen-http-swagger)**: Generates Swagger documentation and provides Swagger UI debugging for HTTP services based on Thrift. +- **[protoc-gen-rpc-swagger](https://github.com/hertz-contrib/swagger-generate/tree/main/protoc-gen-rpc-swagger)**: Generates Swagger documentation and provides Swagger UI debugging for RPC services based on Protobuf. +- **[thrift-gen-rpc-swagger](https://github.com/hertz-contrib/swagger-generate/tree/main/thrift-gen-rpc-swagger)**: Generates Swagger documentation and provides Swagger UI debugging for RPC services based on Thrift. +- **[swagger2idl](https://github.com/hertz-contrib/swagger-generate/tree/main/swagger2idl)**: Converts Swagger documents into Protobuf or Thrift IDL files. ## Key Advantages @@ -17,6 +20,7 @@ English | [中文](README_CN.md) - **Integrated Debugging**: The generated Swagger UI can be used directly for service debugging, supporting both HTTP and RPC modes. - **Hertz and Kitex Integration**: Provides seamless documentation generation and debugging support for [Hertz](https://github.com/cloudwego/hertz) and [Kitex](https://github.com/cloudwego/kitex). - **Flexible Annotation Support**: Allows extending the generated Swagger documentation through annotations, supporting OpenAPI annotations such as `openapi.operation`, `openapi.schema`, etc. +- **IDL Conversion**: Supports converting Swagger documents into Protobuf or Thrift IDL files, making it easier for developers to switch between different frameworks. ## Installation @@ -99,9 +103,14 @@ func main() { } } ``` - For more examples, please refer to [kitex_swagger_gen](https://github.com/cloudwego/kitex-examples/tree/main/bizdemo/kitex_swagger_gen) and [hertz_swagger_gen](https://github.com/cloudwego/hertz-examples/tree/main/bizdemo/hertz_swagger_gen). +### Converting Swagger Documents to IDL Files + +```sh +swagger2idl -o my_output.proto -oa -a openapi.yaml +``` + ## More Information Refer to the README of each plugin for more detailed usage information. \ No newline at end of file diff --git a/README_CN.md b/README_CN.md index 00b1fb5..bacdbc4 100644 --- a/README_CN.md +++ b/README_CN.md @@ -2,14 +2,17 @@ [English](README.md) | 中文 -**Swagger Generate** 是一个为 HTTP 和 RPC 服务生成 Swagger 文档及 Swagger-UI 访问调试的插件集合。该项目适用于 [CloudWeGo](https://www.cloudwego.io) 生态下的 [Cwgo](https://github.com/cloudwego/cwgo)、 [Hertz](https://github.com/cloudwego/hertz) 和 [Kitex](https://github.com/cloudwego/kitex) 框架。它提供了一套便捷的工具来帮助开发者自动生成 Swagger 文档,从而简化 API 文档编写及调试过程。 +**Swagger Generate** 是一组插件工具,专为 HTTP 和 RPC 服务设计,支持自动生成 Swagger 文档,并集成 Swagger-UI 进行调试。此外,它还提供将 Swagger 文档转换为 Protobuf 或 Thrift IDL 文件的功能,简化了 API 开发与维护的流程。 + +该项目适用于 [CloudWeGo](https://www.cloudwego.io) 生态下的 [Cwgo](https://github.com/cloudwego/cwgo)、 [Hertz](https://github.com/cloudwego/hertz) 和 [Kitex](https://github.com/cloudwego/kitex) 框架。它提供了一套便捷的工具来帮助开发者自动生成 Swagger 文档,从而简化 API 文档编写及调试过程。 ## 包含的插件 -- **protoc-gen-http-swagger**:为基于 Protobuf 的 HTTP 服务生成 Swagger 文档和 Swagger UI 进行调试。 -- **thrift-gen-http-swagger**:为基于 Thrift 的 HTTP 服务生成 Swagger 文档和 Swagger UI 进行调试。 -- **protoc-gen-rpc-swagger**:为基于 Protobuf 的 RPC 服务生成 Swagger 文档和 Swagger UI 进行调试。 -- **thrift-gen-rpc-swagger**:为基于 Thrift 的 RPC 服务生成 Swagger 文档和 Swagger UI 进行调试。 +- **[protoc-gen-http-swagger](https://github.com/hertz-contrib/swagger-generate/tree/main/thrift-gen-rpc-swagger)**:为基于 Protobuf 的 HTTP 服务生成 Swagger 文档和 Swagger UI 进行调试。 +- **[thrift-gen-http-swagger](https://github.com/hertz-contrib/swagger-generate/tree/main/thrift-gen-http-swagger)**:为基于 Thrift 的 HTTP 服务生成 Swagger 文档和 Swagger UI 进行调试。 +- **[protoc-gen-rpc-swagger](https://github.com/hertz-contrib/swagger-generate/tree/main/protoc-gen-rpc-swagger)**:为基于 Protobuf 的 RPC 服务生成 Swagger 文档和 Swagger UI 进行调试。 +- **[thrift-gen-rpc-swagger](https://github.com/hertz-contrib/swagger-generate/tree/main/thrift-gen-rpc-swagger)**:为基于 Thrift 的 RPC 服务生成 Swagger 文档和 Swagger UI 进行调试。 +- **[swagger2idl](https://github.com/hertz-contrib/swagger-generate/tree/main/swagger2idl)**:将 Swagger 文档转换为 Protobuf 或 Thrift IDL 文件。 ## 项目优势 @@ -17,6 +20,7 @@ - **集成调试**:生成的 Swagger UI 能直接用于调试服务,支持 HTTP 和 RPC 两种模式。 - **Hertz 和 Kitex 集成**:为 [Hertz](https://github.com/cloudwego/hertz) 和 [Kitex](https://github.com/cloudwego/kitex) 提供了无缝的文档生成和调试支持。 - **灵活的注解支持**:允许通过注解扩展生成的 Swagger 文档内容,支持 `openapi.operation`、`openapi.schema` 等 OpenAPI 注解。 +- **IDL 转换**:支持将 Swagger 文档转换为 Protobuf 或 Thrift IDL 文件,方便开发者在不同框架间切换。 ## 安装 @@ -98,9 +102,14 @@ func main() { } } ``` - 请参考 [kitex_swagger_gen](https://github.com/cloudwego/kitex-examples/tree/main/bizdemo/kitex_swagger_gen) 和 [hertz_swagger_gen](https://github.com/cloudwego/hertz-examples/tree/main/bizdemo/hertz_swagger_gen) 获取更多使用场景示例。 +### 将 Swagger 文档转换为 IDL 文件 + +```sh +swagger2idl -o my_output.proto -oa -a openapi.yaml +``` + ## 更多信息 -请参考各个插件的 readme 文档获取更多使用细节。 \ No newline at end of file +请参考各个插件的 README 文档获取更多使用细节。 \ No newline at end of file diff --git a/common/consts/consts.go b/common/consts/consts.go index 0357119..d82ffd5 100644 --- a/common/consts/consts.go +++ b/common/consts/consts.go @@ -74,6 +74,9 @@ const ( DefaultInfoDesc = "API description" DefaultInfoVersion = "0.0.1" + IDLProto = "proto" + IDLThrift = "thrift" + DocumentOptionServiceType = "service" DocumentOptionStructType = "struct" @@ -100,6 +103,13 @@ const ( DefaultOutputDir = "swagger" DefaultOutputYamlFile = "openapi.yaml" DefaultOutputSwaggerFile = "swagger.go" + DefaultProtoFilename = "output.proto" + DefaultThriftFilename = "output.thrift" + OpenapiThriftFile = "openapi.thrift" + ApiProtoFile = "api.proto" + OpenapiProtoFile = "openapi/annotations.proto" + EmptyProtoFile = "google/protobuf/empty.proto" + TimestampProtoFile = "google/protobuf/timestamp.proto" DefaultServerURL = "http://127.0.0.1:8888" DefaultKitexAddr = "127.0.0.1:8888" @@ -112,4 +122,6 @@ const ( ProtobufValueName = "GoogleProtobufValue" ProtobufAnyName = "GoogleProtobufAny" + EmptyMessage = "google.protobuf.Empty" + TimestampMessage = "google.protobuf.Timestamp" ) diff --git a/common/utils/utils.go b/common/utils/utils.go index e38512a..32652b8 100644 --- a/common/utils/utils.go +++ b/common/utils/utils.go @@ -21,8 +21,12 @@ import ( "fmt" "os" "reflect" + "regexp" "strconv" "strings" + "unicode" + + "github.com/iancoleman/strcase" ) // Contains returns true if an array Contains a specified string. @@ -162,3 +166,314 @@ func FileExists(filePath string) bool { _, err := os.Stat(filePath) return err == nil } + +// Stringify converts a value to a string +func Stringify(value interface{}) string { + switch v := value.(type) { + case string: + return fmt.Sprintf("%q", v) // Add quotes around strings + case int, int64, float64: + return fmt.Sprintf("%v", v) // Output numbers directly + case *uint64: + return fmt.Sprintf("%d", *v) // Handle *uint64 pointer type + case []string: + return fmt.Sprintf("[%s]", strings.Join(v, ", ")) // Output string arrays as a list + case []interface{}: + // Handle arrays of arbitrary types + var strValues []string + for _, item := range v { + strValues = append(strValues, Stringify(item)) + } + return fmt.Sprintf("[%s]", strings.Join(strValues, ", ")) + default: + return fmt.Sprintf("%v", v) // Convert other types directly to string + } +} + +// StructToOption converts a struct to an option string +func StructToOption(value interface{}, indent string) string { + var sb strings.Builder + v := reflect.ValueOf(value) + t := reflect.TypeOf(value) + + // If it's a pointer, get the actual value + if v.Kind() == reflect.Ptr { + if v.IsNil() { + return "" // Skip nil pointers + } + v = v.Elem() + t = t.Elem() + } + + // Handle slice types + if v.Kind() == reflect.Slice { + if v.Len() == 0 { + return "" // Skip empty slices + } + sb.WriteString("[\n") + for i := 0; i < v.Len(); i++ { + sb.WriteString(fmt.Sprintf("%s ", indent)) + sb.WriteString(StructToOption(v.Index(i).Interface(), indent+" ")) + if i < v.Len()-1 { + sb.WriteString(",\n") + } + } + sb.WriteString(fmt.Sprintf("\n%s]", indent)) + return sb.String() + } + + // Handle map types + if v.Kind() == reflect.Map { + if v.Len() == 0 { + return "" // Skip empty maps + } + sb.WriteString("{\n") + for _, key := range v.MapKeys() { + if isZeroValue(v.MapIndex(key)) { + continue + } + sb.WriteString(fmt.Sprintf("%s %v: ", indent, reflect.ValueOf(ToSnakeCase(key.String())))) + sb.WriteString(StructToOption(v.MapIndex(key).Interface(), indent+" ")) + sb.WriteString(",\n") + } + sb.WriteString(fmt.Sprintf("%s}", indent)) + return sb.String() + } + + // Handle struct types + if v.Kind() == reflect.Struct { + sb.WriteString("{\n") + for i := 0; i < v.NumField(); i++ { + field := v.Field(i) + fieldType := t.Field(i) + + // Skip unexported fields + if !field.CanInterface() { + continue + } + + // Skip fields with zero values + if isZeroValue(field) { + continue + } + + fieldName := fieldType.Tag.Get("json") + if fieldName == "" { + fieldName = fieldType.Name // If no json tag, use field name + } + fieldName = strings.Split(fieldName, ",")[0] // Remove options from json tag, e.g., "omitempty" + + // Skip specific fields (Parameters, RequestBody, Responses) + if fieldName == "parameters" || fieldName == "requestBody" || fieldName == "responses" || + fieldName == "schemas" || fieldName == "requestBodies" || fieldName == "items" || + fieldName == "paths" || fieldName == "properties" || fieldName == "content" || + fieldName == "schema" || fieldName == "oneOf" || fieldName == "allOf" || fieldName == "anyOf" || + fieldName == "additionalProperties" || fieldName == "-" || + fieldName == "components" { + continue + } + + fieldName = ToSnakeCase(fieldName) // Convert field name to snake_case + + // Use the field name as the Protobuf key + sb.WriteString(fmt.Sprintf("%s %s: ", indent, fieldName)) + + // Recursively handle the field + sb.WriteString(StructToOption(field.Interface(), indent+" ")) + sb.WriteString(";\n") + } + sb.WriteString(fmt.Sprintf("%s}", indent)) + return sb.String() + } + + // Handle other basic types + switch v.Kind() { + case reflect.String: + if v.String() == "" { + return "" // Skip empty strings + } + + // Process multi-line strings by replacing actual newlines with "\n" + multiLineStr := strings.ReplaceAll(v.String(), "\n", "\\n") + return fmt.Sprintf("\"%s\"", multiLineStr) + case reflect.Int, reflect.Int64, reflect.Int32: + if v.Int() == 0 { + return "" // Skip 0 values + } + return fmt.Sprintf("%d", v.Int()) + case reflect.Float64: + if v.Float() == 0 { + return "" // Skip 0.0 + } + return fmt.Sprintf("%f", v.Float()) + case reflect.Bool: + if !v.Bool() { + return "" // Skip false + } + return fmt.Sprintf("%t", v.Bool()) + case reflect.Ptr: + if !v.IsNil() { + return StructToOption(v.Interface(), indent) + } + return "" + default: + // Skip zero values + if !v.IsValid() || v.IsZero() { + return "" + } + return fmt.Sprintf("\"%v\"", v.Interface()) + } +} + +// isZeroValue checks if a value is the zero value for its type +func isZeroValue(v reflect.Value) bool { + switch v.Kind() { + case reflect.String: + return v.String() == "" + case reflect.Bool: + return !v.Bool() + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return v.Int() == 0 + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + return v.Uint() == 0 + case reflect.Float32, reflect.Float64: + return v.Float() == 0 + case reflect.Complex64, reflect.Complex128: + return v.Complex() == 0 + case reflect.Slice, reflect.Array: + return v.Len() == 0 // Check if slice or array is empty + case reflect.Map: + if v.Len() == 0 { + return true + } + for _, key := range v.MapKeys() { + value := v.MapIndex(key) + if !isZeroValue(value) { + return false + } + } + return true + case reflect.Struct: + for i := 0; i < v.NumField(); i++ { + if !isZeroValue(v.Field(i)) { + return false + } + } + return true + case reflect.Ptr, reflect.Interface, reflect.Chan, reflect.Func: + return v.IsNil() + default: + return !v.IsValid() + } +} + +// ToUpperCase converts the first letter of a string to uppercase +func ToUpperCase(s string) string { + if len(s) == 0 { + return s + } + + firstChar := unicode.ToUpper(rune(s[0])) + + if len(s) == 1 { + return string(firstChar) + } + + return string(firstChar) + s[1:] +} + +// FormatStr formats a string to remove special characters +func FormatStr(str string) string { + str = strings.ReplaceAll(str, " ", "_") + str = strings.ReplaceAll(str, "/", "_") + str = strings.ReplaceAll(str, "-", "_") + reg, _ := regexp.Compile(`[^a-zA-Z0-9_]`) + str = reg.ReplaceAllString(str, "") + return str +} + +// ToPascaleCase converts a string to PascalCase +func ToPascaleCase(name string) string { + name = strcase.ToCamel(name) + name = ToUpperCase(name) + return name +} + +// ToSnakeCase converts a string to snake_case +func ToSnakeCase(name string) string { + name = FormatStr(name) + name = ToSnake(name) + return name +} + +// ToSnake converts a string to snake_case +func ToSnake(s string) string { + return ToDelimited(s, '_') +} + +// ToDelimited converts a string to delimited.snake.case +// (in this case `delimiter = '.'`) +func ToDelimited(s string, delimiter uint8) string { + return ToScreamingDelimited(s, delimiter, "", false) +} + +// ToScreamingDelimited converts a string to SCREAMING.DELIMITED.SNAKE.CASE +// (in this case `delimiter = '.'; screaming = true`) +// or delimited.snake.case +// (in this case `delimiter = '.'; screaming = false`) +func ToScreamingDelimited(s string, delimiter uint8, ignore string, screaming bool) string { + s = strings.TrimSpace(s) + n := strings.Builder{} + n.Grow(len(s) + 2) // nominal 2 bytes of extra space for inserted delimiters + for i, v := range []byte(s) { + vIsCap := v >= 'A' && v <= 'Z' + vIsLow := v >= 'a' && v <= 'z' + if vIsLow && screaming { + v += 'A' + v -= 'a' + } else if vIsCap && !screaming { + v += 'a' + v -= 'A' + } + + // treat acronyms as words, eg for JSONData -> JSON is a whole word + if i+1 < len(s) { + next := s[i+1] + vIsNum := v >= '0' && v <= '9' + nextIsCap := next >= 'A' && next <= 'Z' + nextIsLow := next >= 'a' && next <= 'z' + nextIsNum := next >= '0' && next <= '9' + + // add delimiter if the next character is of a different type + // but do not insert delimiter between a letter and a number + if (vIsCap && (nextIsLow || nextIsNum)) || (vIsLow && (nextIsCap || nextIsNum)) || (vIsNum && (nextIsCap || nextIsLow)) { + prevIgnore := ignore != "" && i > 0 && strings.ContainsAny(string(s[i-1]), ignore) + if !prevIgnore { + if vIsCap && nextIsLow { + if prevIsCap := i > 0 && s[i-1] >= 'A' && s[i-1] <= 'Z'; prevIsCap { + n.WriteByte(delimiter) + } + } + + // Skip adding delimiter if current character is a letter followed by a number + if !(vIsLow && nextIsNum) && !(vIsCap && nextIsNum) { + n.WriteByte(v) + if vIsLow || vIsNum || nextIsNum { + n.WriteByte(delimiter) + } + continue + } + } + } + } + + if (v == ' ' || v == '_' || v == '-' || v == '.') && !strings.ContainsAny(string(v), ignore) { + // replace space/underscore/hyphen/dot with delimiter + n.WriteByte(delimiter) + } else { + n.WriteByte(v) + } + } + + return n.String() +} diff --git a/go.mod b/go.mod index 3a3e045..1743fa6 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.18 require ( github.com/apache/thrift v0.13.0 github.com/google/gnostic-models v0.6.8 + github.com/iancoleman/strcase v0.3.0 google.golang.org/protobuf v1.34.2 gopkg.in/yaml.v3 v3.0.1 ) diff --git a/go.sum b/go.sum index 31e1cfc..4b49dc0 100644 --- a/go.sum +++ b/go.sum @@ -5,6 +5,8 @@ github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6 github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= +github.com/iancoleman/strcase v0.3.0 h1:nTXanmYxhfFAMjZL34Ov6gkzEsSJZ5DbhxWjvSASxEI= +github.com/iancoleman/strcase v0.3.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= diff --git a/licenses/LICENSE-cli.txt b/licenses/LICENSE-cli.txt new file mode 100644 index 0000000..2c84c78 --- /dev/null +++ b/licenses/LICENSE-cli.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 urfave/cli maintainers + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/licenses/LICENSE-kin-openapi.txt b/licenses/LICENSE-kin-openapi.txt new file mode 100644 index 0000000..992b983 --- /dev/null +++ b/licenses/LICENSE-kin-openapi.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2017-2018 the project authors. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/swagger2idl/LICENSE b/swagger2idl/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/swagger2idl/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/swagger2idl/README.md b/swagger2idl/README.md new file mode 100644 index 0000000..59fc40f --- /dev/null +++ b/swagger2idl/README.md @@ -0,0 +1,84 @@ +# swagger2idl + +ENGLISH | [中文](README_CN.md) + +`swagger2idl` is a tool designed to convert Swagger documentation into Thrift or Proto files. It supports relevant annotations from [swagger-generate](https://github.com/hertz-contrib/swagger-generate), [cloudwego/cwgo](https://github.com/cloudwego/cwgo), [hertz](https://github.com/cloudwego/hertz), and [kitex](https://github.com/cloudwego/kitex). + +## Installation + +```sh +# Install from the official repository + +git clone https://github.com/hertz-contrib/swagger-generate +cd swagger2idl +go install + +# Direct installation +go install github.com/hertz-contrib/swagger-generate/swagger2idl@latest +``` + +## Usage + +### Parameter Description + +| Parameter | Abbreviation | Default Value | Description | +|-----------------|--------------|--------------------------------|--------------------------------------------------------------------------------------------------------------------| +| `--type` | `-t` | Inferred from the output file extension | Specify the output type, either `'proto'` or `'thrift'`. If not provided, it is inferred from the output file extension. | +| `--output` | `-o` | `filename.proto` or `filename.thrift` | Specify the output file path. If not provided, it defaults to `output.proto` or `output.thrift`, depending on the output type. | +| `--openapi` | `-oa` | `false` | Includes OpenAPI-specific annotations and adds references. The related reference files can be found in [idl](https://github.com/hertz-contrib/swagger-generate/idl). | +| `--api` | `-a` | `false` | Adds annotations for compatibility with Cwgo/Hertz and adds references. The related reference files are in [idl](https://github.com/hertz-contrib/swagger-generate/idl). | +| `--naming` | `-n` | `true` | Use naming conventions in the output IDL file. | + +### Usage Examples + +1. Convert to Protobuf format and specify the output path: +```bash + swagger2idl --output my_output.proto --openapi --api --naming=false openapi.yaml +``` +or +```bash + swagger2idl -o my_output.proto -oa -a -n=false openapi.yaml +``` + +### Extensions +You can add extensions like `x-options` to parameters in the `openapi.yaml` file. More extensions will be supported in the future. + +For Proto files: +```yaml +x-options: + go_package: myawesomepackage +``` +Generates: +```protobuf +option go_package = "myawesomepackage"; +``` + +For Thrift files: +```yaml +x-options: + go: myawesomepackage +``` +Generates: +```thrift +namespace go myawesomepackage +``` + +### Naming Conventions + +| **Category** | **Thrift/Proto Naming Rules** | +|------------------------------------|-------------------------------------------------------------------------------| +| **Struct/Message** | - Use **PascalCase**.
Example: `UserInfo` | +| **Field** | - Use **snake_case**.
Example: `user_id`. If a field name contains a number, the number should follow a letter, not an underscore. | +| **Enum**, **Service**, **Union** | - Use **PascalCase**.
Example: `UserType` | +| **Enum Values** | - Use **UPPER_SNAKE_CASE**.
Example: `ADMIN_USER` | +| **RPC Methods** | - Use **PascalCase**.
Example: `GetUserInfo` | +| **Package/Namespace** | - Use **snake_case**, typically based on the project structure.
Example: `com.project.service` | + +#### Naming Conventions Explained: +- **PascalCase**: Capitalize the first letter of each word, such as `UserInfo`. +- **snake_case**: All lowercase with underscores separating words, such as `user_info`. +- **UPPER_SNAKE_CASE**: All uppercase letters with underscores separating words, such as `ADMIN_USER`. + +## More Information + +For more usage details, refer to the [Examples](example). \ No newline at end of file diff --git a/swagger2idl/README_CN.md b/swagger2idl/README_CN.md new file mode 100644 index 0000000..aaac445 --- /dev/null +++ b/swagger2idl/README_CN.md @@ -0,0 +1,82 @@ +# swagger2idl + +[English](README.md) | 中文 + +swagger2idl 是一个用于将 Swagger 文档转换为 Thrift 或 Proto 文件的工具。 +适配了[swagger-generate](https://github.com/hertz-contrib/swagger-generate)、[cloudwego/cwgo](https://github.com/cloudwego/cwgo)、[hertz](https://github.com/cloudwego/hertz)及[kitex](https://github.com/cloudwego/kitex)中的相关注解。 + +## 安装 + +```sh +# 官方仓库安装 + +git clone https://github.com/hertz-contrib/swagger-generate +cd swagger2idl +go install + +# 直接安装 +go install github.com/hertz-contrib/swagger-generate/swagger2idl@latest +``` + +## 使用 +### 参数说明 + +| 参数名称 | 缩写 | 默认值 | 说明 | +|-------------|-------|----------------------------|-------------------------------------------------------------------------------------------------------| +| `--type` | `-t` | 自动根据输出文件扩展名推断 | 指定输出类型,可选值为 `'proto'` 或 `'thrift'`。如果未提供,则从输出文件扩展名推断。 | +| `--output` | `-o` | `文件名.proto` 或 `文件名.thrift` | 指定输出文件的路径。如果未提供,默认为 `output.proto` 或 `output.thrift`,具体取决于输出类型。 | +| `--openapi` | `-oa` | `false` | 会生成相应的openapi注解,并添加引用,相关引用文件可以在[idl](https://github.com/hertz-contrib/swagger-generate/idl)中找到。 | +| `--api` | `-a` | `false` | 会生成相应的适配Cwgo/Hertz的注解,并添加引用,相关引用文件可以在[idl](https://github.com/hertz-contrib/swagger-generate/idl)中找到。 | +| `--naming` | `-n` | `true` | 在输出的 IDL 文件中使用命名约定。 | + +### 使用示例 + +1. 指定输出为 Protobuf 格式,并输出到指定路径: +```bash + swagger2idl --output my_output.proto --openapi --api --naming=false openapi.yaml +``` +or +```bash + swagger2idl -o my_output.proto -oa -a -n=false openapi.yaml +``` + +### 扩展 +支持向openapi.yaml中的参数添加扩展,如`x-options`,后面会增加更多扩展。 + +如果是proto文件 +```yaml +x-options: + go_package: myawesomepackage +``` +会生成 +```protobuf +option go_package = "myawesomepackage"; +``` +如果是thrift文件 +```yaml +x-options: + go: myawesomepackage +``` +会生成 +```thrift +namespace go myawesomepackage +``` +### 命名约定 + +| **类别** | **Thrift/Proto 命名规范** | +|----------------------------------|-------------------------------------------------------------------------------| +| **Struct/Message** | - 使用 **PascalCase** 命名。
- 例:`UserInfo` | +| **Field** | - 使用 **snake_case** 命名。
- 例:`user_id`, 如果你的字段名包含一个数字,数字应该出现在字母后面,而不是下划线后面 | +| **Enum**, **Service**, **Union** | - 使用 **PascalCase**。
- 例:`UserType` | +| **Enum 值** | - 使用 **UPPER_SNAKE_CASE** 命名。
- 例:`ADMIN_USER` | +| **RPC 方法** | - 使用 **PascalCase** 命名。
- 例:`GetUserInfo` | +| **Package/Namespace** | - 使用 **snake_case**,通常基于项目结构命名。
例:`com.project.service` | + +#### 详细说明: +- **PascalCase**: 首字母大写,每个单词的首字母都大写,例如 `UserInfo`。 +- **snake_case**: 全部小写,单词之间使用下划线分隔,例如 `user_info`。 +- **UPPER_SNAKE_CASE**: 全部字母大写,单词之间用下划线分隔,例如 `ADMIN_USER`。 + +## 更多信息 + +更多的使用方法请参考 [示例](example) \ No newline at end of file diff --git a/swagger2idl/converter/converter.go b/swagger2idl/converter/converter.go new file mode 100644 index 0000000..888d10b --- /dev/null +++ b/swagger2idl/converter/converter.go @@ -0,0 +1,42 @@ +/* + * Copyright 2024 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package converter + +import "github.com/hertz-contrib/swagger-generate/common/consts" + +// Converter is an interface for converting files +type Converter interface { + Convert() error + GetIdl() interface{} +} + +// ConvertOption adds a struct for conversion options +type ConvertOption struct { + OpenapiOption bool + ApiOption bool + NamingOption bool +} + +var MethodToOption = map[string]string{ + consts.HttpMethodGet: consts.ApiGet, + consts.HttpMethodPost: consts.ApiPost, + consts.HttpMethodPut: consts.ApiPut, + consts.HttpMethodPatch: consts.ApiPatch, + consts.HttpMethodDelete: consts.ApiDelete, + consts.HttpMethodOptions: consts.ApiOptions, + consts.HttpMethodHead: consts.ApiHEAD, +} diff --git a/swagger2idl/converter/proto_converter.go b/swagger2idl/converter/proto_converter.go new file mode 100644 index 0000000..413680d --- /dev/null +++ b/swagger2idl/converter/proto_converter.go @@ -0,0 +1,1327 @@ +/* + * Copyright 2024 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package converter + +import ( + "errors" + "fmt" + + "github.com/getkin/kin-openapi/openapi3" + "github.com/hertz-contrib/swagger-generate/common/consts" + common "github.com/hertz-contrib/swagger-generate/common/utils" + "github.com/hertz-contrib/swagger-generate/swagger2idl/protobuf" + "github.com/hertz-contrib/swagger-generate/swagger2idl/utils" +) + +// ProtoConverter struct, used to convert OpenAPI specifications into Proto files +type ProtoConverter struct { + spec *openapi3.T + ProtoFile *protobuf.ProtoFile + converterOption *ConvertOption +} + +// NewProtoConverter creates and initializes a ProtoConverter +func NewProtoConverter(spec *openapi3.T, option *ConvertOption) *ProtoConverter { + return &ProtoConverter{ + spec: spec, + ProtoFile: &protobuf.ProtoFile{ + PackageName: utils.GetPackageName(spec), + Messages: []*protobuf.ProtoMessage{}, + Services: []*protobuf.ProtoService{}, + Enums: []*protobuf.ProtoEnum{}, + Imports: []string{}, + Options: []*protobuf.Option{}, + }, + converterOption: option, + } +} + +// Convert converts the OpenAPI specification to a Proto file +func (c *ProtoConverter) Convert() error { + // Convert the go Option to Proto + err := c.addExtensionsToProtoOptions() + if err != nil { + return fmt.Errorf("error parsing extensions to proto options: %w", err) + } + + // Convert tags into Proto services + c.convertTagsToProtoServices() + + // Convert components into Proto messages + err = c.convertComponentsToProtoMessages() + if err != nil { + return fmt.Errorf("error converting components to proto messages: %w", err) + } + + // Convert paths into Proto services + err = c.convertPathsToProtoServices() + if err != nil { + return fmt.Errorf("error converting paths to proto services: %w", err) + } + + if c.converterOption.OpenapiOption { + c.addOptionsToProto() + } + + return nil +} + +func (c *ProtoConverter) GetIdl() interface{} { + return c.ProtoFile +} + +// convertTagsToProtoServices converts OpenAPI tags into Proto services and stores them in the ProtoFile +func (c *ProtoConverter) convertTagsToProtoServices() { + tags := c.spec.Tags + for _, tag := range tags { + serviceName := common.ToPascaleCase(tag.Name) + service := &protobuf.ProtoService{ + Name: serviceName, + Description: tag.Description, + } + c.ProtoFile.Services = append(c.ProtoFile.Services, service) + } +} + +// convertComponentsToProtoMessages converts OpenAPI components into Proto messages and stores them in the ProtoFile +func (c *ProtoConverter) convertComponentsToProtoMessages() error { + components := c.spec.Components + if components == nil { + return nil + } + + if components.Schemas == nil { + return nil + } + + for name, schemaRef := range components.Schemas { + schema := schemaRef + + if c.converterOption.NamingOption { + name = common.ToPascaleCase(name) + } + + protoType, err := c.ConvertSchemaToProtoType(schema, name, nil) + if err != nil { + return fmt.Errorf("error converting schema %s: %w", name, err) + } + + switch v := protoType.(type) { + case *protobuf.ProtoField: + message := &protobuf.ProtoMessage{ + Name: name, + Fields: []*protobuf.ProtoField{v}, + } + + if c.converterOption.OpenapiOption { + optionStr := common.StructToOption(schema.Value, " ") + + schemaOption := &protobuf.Option{ + Name: consts.OpenapiSchema, + Value: optionStr, + } + message.Options = append(message.Options, schemaOption) + c.AddProtoImport(consts.OpenapiProtoFile) + } + c.addMessageToProto(message) + case *protobuf.ProtoMessage: + if c.converterOption.OpenapiOption { + optionStr := common.StructToOption(schema.Value, " ") + + schemaOption := &protobuf.Option{ + Name: consts.OpenapiSchema, + Value: optionStr, + } + v.Options = append(v.Options, schemaOption) + c.AddProtoImport(consts.OpenapiProtoFile) + } + c.addMessageToProto(v) + case *protobuf.ProtoEnum: + if c.converterOption.OpenapiOption { + optionStr := common.StructToOption(schema.Value, " ") + + schemaOption := &protobuf.Option{ + Name: consts.OpenapiSchema, + Value: optionStr, + } + v.Options = append(v.Options, schemaOption) + c.AddProtoImport(consts.OpenapiProtoFile) + } + c.addEnumToProto(v) + case *protobuf.ProtoOneOf: + message := &protobuf.ProtoMessage{ + Name: name, + OneOfs: []*protobuf.ProtoOneOf{v}, + } + + if c.converterOption.OpenapiOption { + optionStr := common.StructToOption(schema.Value, " ") + + schemaOption := &protobuf.Option{ + Name: consts.OpenapiSchema, + Value: optionStr, + } + message.Options = append(message.Options, schemaOption) + c.AddProtoImport(consts.OpenapiProtoFile) + } + c.addMessageToProto(message) + } + } + return nil +} + +// convertPathsToProtoServices converts OpenAPI path items into Proto services and stores them in the ProtoFile +func (c *ProtoConverter) convertPathsToProtoServices() error { + paths := c.spec.Paths + services, err := c.ConvertPathsToProtoServices(paths) + if err != nil { + return fmt.Errorf("error converting paths to proto services: %w", err) + } + + c.ProtoFile.Services = append(c.ProtoFile.Services, services...) + return nil +} + +// ConvertPathsToProtoServices converts OpenAPI path items into Proto services +func (c *ProtoConverter) ConvertPathsToProtoServices(paths *openapi3.Paths) ([]*protobuf.ProtoService, error) { + var services []*protobuf.ProtoService + + for path, pathItem := range paths.Map() { + for method, operation := range pathItem.Operations() { + serviceName := utils.GetServiceName(operation) + methodName := utils.GetMethodName(operation, path, method) + + if c.converterOption.NamingOption { + serviceName = common.ToPascaleCase(serviceName) + methodName = common.ToPascaleCase(methodName) + } + + inputMessage, err := c.generateRequestMessage(operation, methodName) + if err != nil { + return nil, fmt.Errorf("error generating request message for %s: %w", methodName, err) + } + + outputMessage, err := c.generateResponseMessage(operation, methodName) + if err != nil { + return nil, fmt.Errorf("error generating response message for %s: %w", methodName, err) + } + + service := c.findOrCreateService(serviceName) + + if !c.methodExistsInService(service, methodName) { + protoMethod := &protobuf.ProtoMethod{ + Name: methodName, + Input: inputMessage, + Output: outputMessage, + } + + if c.converterOption.ApiOption { + if optionName, ok := MethodToOption[method]; ok { + option := &protobuf.Option{ + Name: optionName, + Value: fmt.Sprintf("%q", utils.ConvertPath(path)), + } + protoMethod.Options = append(protoMethod.Options, option) + c.AddProtoImport(consts.ApiProtoFile) + } + } + + if c.converterOption.OpenapiOption { + optionStr := common.StructToOption(operation, " ") + + schemaOption := &protobuf.Option{ + Name: consts.OpenapiOperation, + Value: optionStr, + } + protoMethod.Options = append(protoMethod.Options, schemaOption) + c.AddProtoImport(consts.OpenapiProtoFile) + + } + service.Methods = append(service.Methods, protoMethod) + } + } + } + + return services, nil +} + +// generateRequestMessage generates a request message for an operation +func (c *ProtoConverter) generateRequestMessage(operation *openapi3.Operation, methodName string) (string, error) { + messageName := utils.GetMessageName(operation, methodName, "Request") + + if c.converterOption.NamingOption { + messageName = common.ToPascaleCase(messageName) + } + + message := &protobuf.ProtoMessage{Name: messageName} + + if operation.RequestBody == nil && len(operation.Parameters) == 0 { + c.AddProtoImport(consts.EmptyProtoFile) + return consts.EmptyMessage, nil + } + + if operation.RequestBody != nil { + if operation.RequestBody.Ref != "" { + return common.ToPascaleCase(utils.ExtractMessageNameFromRef(operation.RequestBody.Ref)), nil + } + + if operation.RequestBody.Value != nil && len(operation.RequestBody.Value.Content) > 0 { + for mediaTypeStr, mediaType := range operation.RequestBody.Value.Content { + schema := mediaType.Schema + if schema != nil { + protoType, err := c.ConvertSchemaToProtoType(schema, common.FormatStr(mediaTypeStr), message) + if err != nil { + return "", err + } + + switch v := protoType.(type) { + case *protobuf.ProtoField: + if c.converterOption.ApiOption { + var optionName string + if mediaTypeStr == "application/json" { + optionName = "api.body" + } else if mediaTypeStr == "application/x-www-form-urlencoded" || mediaTypeStr == "multipart/form-data" { + optionName = "api.form" + } + if optionName != "" { + v.Options = append(v.Options, &protobuf.Option{ + Name: optionName, + Value: fmt.Sprintf("%q", v.Name), + }) + c.AddProtoImport(consts.ApiProtoFile) + } + } + c.addFieldIfNotExists(&message.Fields, v) + case *protobuf.ProtoMessage: + for _, field := range v.Fields { + if c.converterOption.ApiOption { + var optionName string + if mediaTypeStr == "application/json" { + optionName = "api.body" + } else if mediaTypeStr == "application/x-www-form-urlencoded" || mediaTypeStr == "multipart/form-data" { + optionName = "api.form" + } + if optionName != "" { + field.Options = append(field.Options, &protobuf.Option{ + Name: optionName, + Value: fmt.Sprintf("%q", field.Name), + }) + c.AddProtoImport(consts.ApiProtoFile) + } + } + c.addFieldIfNotExists(&message.Fields, field) + } + + message.Enums = append(message.Enums, v.Enums...) + + message.OneOfs = append(message.OneOfs, v.OneOfs...) + + for _, nestedMessage := range v.Messages { + c.addMessageIfNotExists(&message.Messages, nestedMessage) + } + case *protobuf.ProtoEnum: + name := mediaTypeStr + if c.converterOption.NamingOption { + name = common.ToSnakeCase(name) + } else { + name = common.FormatStr(name) + } + newField := &protobuf.ProtoField{ + Name: name + "_field", + Type: v.Name, + } + if c.converterOption.ApiOption { + var optionName string + if mediaTypeStr == "application/json" { + optionName = "api.body" + } else if mediaTypeStr == "application/x-www-form-urlencoded" || mediaTypeStr == "multipart/form-data" { + optionName = "api.form" + } + if optionName != "" { + newField.Options = append(newField.Options, &protobuf.Option{ + Name: optionName, + Value: fmt.Sprintf("%q", v.Name), + }) + c.AddProtoImport(consts.ApiProtoFile) + } + } + if c.converterOption.OpenapiOption { + optionStr := common.StructToOption(schema.Value, " ") + + schemaOption := &protobuf.Option{ + Name: consts.OpenapiProperty, + Value: optionStr, + } + newField.Options = append(newField.Options, schemaOption) + c.AddProtoImport(consts.OpenapiProtoFile) + } + message.Enums = append(message.Enums, v) + message.Fields = append(message.Fields, newField) + case *protobuf.ProtoOneOf: + message.OneOfs = append(message.OneOfs, v) + } + } + } + } + } + + if len(operation.Parameters) > 0 { + for _, param := range operation.Parameters { + if param.Value.Schema != nil { + fieldOrMessage, err := c.ConvertSchemaToProtoType(param.Value.Schema, param.Value.Name, message) + if err != nil { + return "", err + } + description := param.Value.Description + switch v := fieldOrMessage.(type) { + case *protobuf.ProtoField: + if c.converterOption.ApiOption { + v.Options = append(v.Options, &protobuf.Option{ + Name: "api." + param.Value.In, + Value: fmt.Sprintf("%q", param.Value.Name), + }) + c.AddProtoImport(consts.ApiProtoFile) + } + if c.converterOption.OpenapiOption { + optionStr := common.StructToOption(param.Value, " ") + + schemaOption := &protobuf.Option{ + Name: consts.OpenapiParameter, + Value: optionStr, + } + v.Options = append(v.Options, schemaOption) + c.AddProtoImport(consts.OpenapiProtoFile) + } + v.Description = description + c.addFieldIfNotExists(&message.Fields, v) + case *protobuf.ProtoMessage: + for _, field := range v.Fields { + if c.converterOption.ApiOption { + field.Options = append(field.Options, &protobuf.Option{ + Name: "api." + param.Value.In, + Value: fmt.Sprintf("%q", param.Value.Name), + }) + c.AddProtoImport(consts.ApiProtoFile) + } + if c.converterOption.OpenapiOption { + optionStr := common.StructToOption(param.Value, " ") + + schemaOption := &protobuf.Option{ + Name: consts.OpenapiParameter, + Value: optionStr, + } + field.Options = append(field.Options, schemaOption) + c.AddProtoImport(consts.OpenapiProtoFile) + } + c.addFieldIfNotExists(&message.Fields, field) + } + message.Enums = append(message.Enums, v.Enums...) + + message.OneOfs = append(message.OneOfs, v.OneOfs...) + + for _, nestedMessage := range v.Messages { + c.addMessageIfNotExists(&message.Messages, nestedMessage) + } + case *protobuf.ProtoEnum: + name := param.Value.Name + if c.converterOption.NamingOption { + name = common.ToPascaleCase(name) + } + newField := &protobuf.ProtoField{ + Name: name + "_field", + Type: v.Name, + } + if c.converterOption.ApiOption { + newField.Options = append(newField.Options, &protobuf.Option{ + Name: "api." + param.Value.In, + Value: fmt.Sprintf("%q", param.Value.Name), + }) + c.AddProtoImport(consts.ApiProtoFile) + } + if c.converterOption.OpenapiOption { + optionStr := common.StructToOption(param.Value, " ") + + schemaOption := &protobuf.Option{ + Name: consts.OpenapiParameter, + Value: optionStr, + } + newField.Options = append(newField.Options, schemaOption) + c.AddProtoImport(consts.OpenapiProtoFile) + } + message.Enums = append(message.Enums, v) + message.Fields = append(message.Fields, newField) + case *protobuf.ProtoOneOf: + message.OneOfs = append(message.OneOfs, v) + } + } + } + } + + // if there are no fields or messages, return an empty message + if len(message.Fields) > 0 || len(message.Messages) > 0 || len(message.Enums) > 0 { + c.addMessageToProto(message) + return message.Name, nil + } + + return "", nil +} + +// generateResponseMessage generates a response message for an operation +func (c *ProtoConverter) generateResponseMessage(operation *openapi3.Operation, methodName string) (string, error) { + if operation.Responses == nil { + return "", nil + } + + responses := operation.Responses.Map() + responseCount := 0 + for _, responseRef := range responses { + if responseRef.Ref == "" && (responseRef.Value == nil || (len(responseRef.Value.Content) == 0 && len(responseRef.Value.Headers) == 0)) { + continue + } + responseCount++ + } + + if responseCount == 1 { + for _, responseRef := range responses { + if responseRef.Ref == "" && (responseRef.Value == nil || (len(responseRef.Value.Content) == 0 && len(responseRef.Value.Headers) == 0)) { + continue + } + return c.processSingleResponse("", responseRef, operation, methodName) + } + } + + if responseCount == 0 { + c.AddProtoImport(consts.EmptyProtoFile) + return consts.EmptyMessage, nil + } + + // create a wrapper message for multiple responses + wrapperMessageName := utils.GetMessageName(operation, methodName, "Response") + if c.converterOption.NamingOption { + wrapperMessageName = common.ToPascaleCase(wrapperMessageName) + } + + wrapperMessage := &protobuf.ProtoMessage{Name: wrapperMessageName} + + emptyFlag := true + + for statusCode, responseRef := range responses { + if responseRef.Ref == "" && (responseRef.Value == nil || len(responseRef.Value.Content) == 0) { + break + } + emptyFlag = false + messageName, err := c.processSingleResponse(statusCode, responseRef, operation, methodName) + if err != nil { + return "", err + } + + name := "Response_" + statusCode + if c.converterOption.NamingOption { + name = common.ToSnakeCase(name) + } + field := &protobuf.ProtoField{ + Name: name, + Type: messageName, + } + wrapperMessage.Fields = append(wrapperMessage.Fields, field) + } + + if emptyFlag { + c.AddProtoImport(consts.EmptyProtoFile) + return consts.EmptyMessage, nil + } + + c.addMessageToProto(wrapperMessage) + + return wrapperMessageName, nil +} + +// processSingleResponse deals with a single response in an operation +func (c *ProtoConverter) processSingleResponse(statusCode string, responseRef *openapi3.ResponseRef, operation *openapi3.Operation, methodName string) (string, error) { + if responseRef.Ref != "" { + return common.ToPascaleCase(utils.ExtractMessageNameFromRef(responseRef.Ref)), nil + } + + response := responseRef.Value + messageName := utils.GetMessageName(operation, methodName, "Response") + common.ToUpperCase(statusCode) + + if c.converterOption.NamingOption { + messageName = common.ToPascaleCase(messageName) + } + + message := &protobuf.ProtoMessage{Name: messageName} + + if len(response.Headers) > 0 { + for headerName, headerRef := range response.Headers { + if headerRef != nil { + + fieldOrMessage, err := c.ConvertSchemaToProtoType(headerRef.Value.Schema, headerName, message) + if err != nil { + return "", err + } + + switch v := fieldOrMessage.(type) { + case *protobuf.ProtoField: + if c.converterOption.ApiOption { + option := &protobuf.Option{ + Name: "api.header", + Value: fmt.Sprintf("%q", headerName), + } + v.Options = append(v.Options, option) + c.AddProtoImport(consts.ApiProtoFile) + } + if c.converterOption.OpenapiOption { + optionStr := common.StructToOption(headerRef.Value, " ") + + schemaOption := &protobuf.Option{ + Name: consts.OpenapiProperty, + Value: optionStr, + } + v.Options = append(v.Options, schemaOption) + c.AddProtoImport(consts.OpenapiProtoFile) + } + c.addFieldIfNotExists(&message.Fields, v) + case *protobuf.ProtoMessage: + for _, field := range v.Fields { + if c.converterOption.ApiOption { + option := &protobuf.Option{ + Name: "api.header", + Value: fmt.Sprintf("%q", field.Name), + } + field.Options = append(field.Options, option) + c.AddProtoImport(consts.ApiProtoFile) + } + if c.converterOption.OpenapiOption { + optionStr := common.StructToOption(headerRef.Value, " ") + + schemaOption := &protobuf.Option{ + Name: consts.OpenapiProperty, + Value: optionStr, + } + field.Options = append(field.Options, schemaOption) + c.AddProtoImport(consts.OpenapiProtoFile) + } + c.addFieldIfNotExists(&message.Fields, field) + } + message.Enums = append(message.Enums, v.Enums...) + + message.OneOfs = append(message.OneOfs, v.OneOfs...) + + for _, nestedMessage := range v.Messages { + c.addMessageIfNotExists(&message.Messages, nestedMessage) + } + case *protobuf.ProtoEnum: + name := headerName + if c.converterOption.NamingOption { + name = common.ToSnakeCase(name) + } + newField := &protobuf.ProtoField{ + Name: name + "_field", + Type: v.Name, + } + if c.converterOption.ApiOption { + option := &protobuf.Option{ + Name: "api.header", + Value: fmt.Sprintf("%q", headerName), + } + newField.Options = append(newField.Options, option) + c.AddProtoImport(consts.ApiProtoFile) + } + if c.converterOption.OpenapiOption { + optionStr := common.StructToOption(headerRef.Value, " ") + + schemaOption := &protobuf.Option{ + Name: consts.OpenapiProperty, + Value: optionStr, + } + newField.Options = append(newField.Options, schemaOption) + c.AddProtoImport(consts.OpenapiProtoFile) + } + message.Enums = append(message.Enums, v) + message.Fields = append(message.Fields, newField) + case *protobuf.ProtoOneOf: + message.OneOfs = append(message.OneOfs, v) + } + } + } + } + + for mediaTypeStr, mediaType := range response.Content { + schema := mediaType.Schema + if schema != nil { + + protoType, err := c.ConvertSchemaToProtoType(schema, common.FormatStr(mediaTypeStr), message) + if err != nil { + return "", err + } + + switch v := protoType.(type) { + case *protobuf.ProtoField: + if c.converterOption.ApiOption && mediaTypeStr == "application/json" { + option := &protobuf.Option{ + Name: "api.body", + Value: fmt.Sprintf("%q", v.Name), + } + v.Options = append(v.Options, option) + c.AddProtoImport(consts.ApiProtoFile) + } + if c.converterOption.OpenapiOption { + optionStr := common.StructToOption(schema.Value, " ") + + schemaOption := &protobuf.Option{ + Name: consts.OpenapiProperty, + Value: optionStr, + } + v.Options = append(v.Options, schemaOption) + c.AddProtoImport(consts.OpenapiProtoFile) + } + c.addFieldIfNotExists(&message.Fields, v) + case *protobuf.ProtoMessage: + for _, field := range v.Fields { + if c.converterOption.ApiOption && mediaTypeStr == "application/json" { + option := &protobuf.Option{ + Name: "api.body", + Value: fmt.Sprintf("%q", field.Name), + } + field.Options = append(field.Options, option) + c.AddProtoImport(consts.ApiProtoFile) + } + if c.converterOption.OpenapiOption { + optionStr := common.StructToOption(schema.Value, " ") + + schemaOption := &protobuf.Option{ + Name: consts.OpenapiProperty, + Value: optionStr, + } + field.Options = append(field.Options, schemaOption) + c.AddProtoImport(consts.OpenapiProtoFile) + } + c.addFieldIfNotExists(&message.Fields, field) + } + message.Enums = append(message.Enums, v.Enums...) + + message.OneOfs = append(message.OneOfs, v.OneOfs...) + + for _, nestedMessage := range v.Messages { + c.addMessageIfNotExists(&message.Messages, nestedMessage) + } + case *protobuf.ProtoEnum: + name := mediaTypeStr + if c.converterOption.NamingOption { + name = common.ToSnakeCase(mediaTypeStr) + } else { + name = common.ToUpperCase(name) + } + newField := &protobuf.ProtoField{ + Name: name + "_field", + Type: v.Name, + } + if c.converterOption.ApiOption && mediaTypeStr == "application/json" { + option := &protobuf.Option{ + Name: "api.body", + Value: fmt.Sprintf("%q", v.Name), + } + newField.Options = append(newField.Options, option) + c.AddProtoImport(consts.ApiProtoFile) + } + if c.converterOption.OpenapiOption { + optionStr := common.StructToOption(schema.Value, " ") + + schemaOption := &protobuf.Option{ + Name: consts.OpenapiProperty, + Value: optionStr, + } + newField.Options = append(newField.Options, schemaOption) + c.AddProtoImport(consts.OpenapiProtoFile) + } + message.Enums = append(message.Enums, v) + message.Fields = append(message.Fields, newField) + case *protobuf.ProtoOneOf: + message.OneOfs = append(message.OneOfs, v) + } + } + } + + if len(message.Fields) > 0 || len(message.Messages) > 0 || len(message.Enums) > 0 { + c.addMessageToProto(message) + return message.Name, nil + } + return "", nil +} + +// ConvertSchemaToProtoType converts an OpenAPI schema to a Proto field or message +func (c *ProtoConverter) ConvertSchemaToProtoType( + schemaRef *openapi3.SchemaRef, + protoName string, + parentMessage *protobuf.ProtoMessage, +) (interface{}, error) { + var protoType string + var result interface{} + + // Handle referenced schema + if schemaRef.Ref != "" { + name := c.applySnakeCaseNamingOption(utils.ExtractMessageNameFromRef(schemaRef.Ref)) + return &protobuf.ProtoField{ + Name: name, + Type: common.ToPascaleCase(utils.ExtractMessageNameFromRef(schemaRef.Ref)), + }, nil + } + + // Ensure schema value is valid + if schemaRef.Value == nil { + return nil, errors.New("schema type is required") + } + + schema := schemaRef.Value + description := schema.Description + + // Handle oneOf, allOf, anyOf even if schema.Type is nil + if len(schema.OneOf) > 0 { + protoUnion, err := c.handleOneOf(schema.OneOf, protoName, parentMessage) + if err != nil { + return nil, err + } + return protoUnion, nil + } else if len(schema.AllOf) > 0 { + protoMessage, err := c.handleAllOf(schema.AllOf, protoName, parentMessage) + if err != nil { + return nil, err + } + return protoMessage, nil + } else if len(schema.AnyOf) > 0 { + protoMessage, err := c.handleAnyOf(schema.AnyOf, protoName, parentMessage) + if err != nil { + return nil, err + } + return protoMessage, nil + } + + // Process schema type + switch { + case schema.Type.Includes("string"): + if schema.Format == "date" || schema.Format == "date-time" { + protoType = consts.TimestampMessage + c.AddProtoImport(consts.TimestampProtoFile) + } else if len(schema.Enum) != 0 { + var name string + if parentMessage == nil { + name = protoName + } else { + name = c.applyPascaleCaseNamingOption(common.ToUpperCase(protoName)) + } + protoEnum := &protobuf.ProtoEnum{ + Name: name, + Description: description, + } + for i, enumValue := range schema.Enum { + protoEnum.Values = append(protoEnum.Values, &protobuf.ProtoEnumValue{ + Index: i, + Value: enumValue, + }) + } + result = protoEnum + } else { + protoType = "string" + } + + case schema.Type.Includes("integer"): + if len(schema.Enum) != 0 { + var name string + if parentMessage == nil { + name = protoName + } else { + name = c.applyPascaleCaseNamingOption(common.ToUpperCase(protoName)) + } + protoEnum := &protobuf.ProtoEnum{ + Name: name, + Description: description, + } + for i, enumValue := range schema.Enum { + protoEnum.Values = append(protoEnum.Values, &protobuf.ProtoEnumValue{ + Index: i, + Value: enumValue, + }) + } + result = protoEnum + } else if schema.Format == "int32" { + protoType = "int32" + } else { + protoType = "int64" + } + + case schema.Type.Includes("number"): + if len(schema.Enum) != 0 { + var name string + if parentMessage == nil { + name = protoName + } else { + name = c.applyPascaleCaseNamingOption(common.ToUpperCase(protoName)) + } + protoEnum := &protobuf.ProtoEnum{ + Name: name, + Description: description, + } + for i, enumValue := range schema.Enum { + protoEnum.Values = append(protoEnum.Values, &protobuf.ProtoEnumValue{ + Index: i, + Value: enumValue, + }) + } + result = protoEnum + } else if schema.Format == "float" { + protoType = "float" + } else { + protoType = "double" + } + + case schema.Type.Includes("boolean"): + protoType = "bool" + + case schema.Type.Includes("array"): + if schema.Items != nil { + fieldOrMessage, err := c.ConvertSchemaToProtoType(schema.Items, protoName+"Item", parentMessage) + if err != nil { + return nil, err + } + + fieldType := "" + if field, ok := fieldOrMessage.(*protobuf.ProtoField); ok { + fieldType = field.Type + } else if nestedMessage, ok := fieldOrMessage.(*protobuf.ProtoMessage); ok { + fieldType = nestedMessage.Name + c.addNestedMessageToParent(parentMessage, nestedMessage) + } else if enum, ok := fieldOrMessage.(*protobuf.ProtoEnum); ok { + fieldType = enum.Name + c.addNestedEnumToParent(parentMessage, enum) + } + + result = &protobuf.ProtoField{ + Name: c.applySnakeCaseNamingOption(protoName), + Type: fieldType, + Repeated: true, + Description: description, + } + } + + case schema.Type.Includes("object"): + var message *protobuf.ProtoMessage + if parentMessage == nil { + message = &protobuf.ProtoMessage{Name: protoName} + } else { + message = &protobuf.ProtoMessage{Name: c.applyPascaleCaseNamingOption(common.ToUpperCase(protoName))} + } + for propName, propSchema := range schema.Properties { + protoType, err := c.ConvertSchemaToProtoType(propSchema, propName, message) + if err != nil { + return nil, err + } + + if field, ok := protoType.(*protobuf.ProtoField); ok { + if c.converterOption.OpenapiOption { + optionStr := common.StructToOption(propSchema.Value, " ") + + schemaOption := &protobuf.Option{ + Name: consts.OpenapiProperty, + Value: optionStr, + } + field.Options = append(field.Options, schemaOption) + c.AddProtoImport(consts.OpenapiProtoFile) + } + message.Fields = append(message.Fields, field) + } else if nestedMessage, ok := protoType.(*protobuf.ProtoMessage); ok { + var name string + if c.converterOption.NamingOption { + name = common.ToSnakeCase(nestedMessage.Name) + } + newField := &protobuf.ProtoField{ + Name: name + "_field", + Type: nestedMessage.Name, + } + if c.converterOption.OpenapiOption { + optionStr := common.StructToOption(propSchema.Value, " ") + + schemaOption := &protobuf.Option{ + Name: consts.OpenapiProperty, + Value: optionStr, + } + newField.Options = append(newField.Options, schemaOption) + c.AddProtoImport(consts.OpenapiProtoFile) + } + c.addNestedMessageToParent(message, nestedMessage) + message.Fields = append(message.Fields, newField) + } else if enum, ok := protoType.(*protobuf.ProtoEnum); ok { + c.addNestedEnumToParent(message, enum) + message.Fields = append(message.Fields, &protobuf.ProtoField{ + Name: c.applySnakeCaseNamingOption(propName + "_field"), + Type: enum.Name, + }) + } else if oneOf, ok := protoType.(*protobuf.ProtoOneOf); ok { + c.addNestedOneOfToParent(message, oneOf) + } + } + + if schema.AdditionalProperties.Schema != nil { + mapValueType := "string" + additionalPropMessage, err := c.ConvertSchemaToProtoType(schema.AdditionalProperties.Schema, protoName+"AdditionalProperties", parentMessage) + if err != nil { + return nil, err + } + if msg, ok := additionalPropMessage.(*protobuf.ProtoMessage); ok { + mapValueType = msg.Name + } else if enum, ok := additionalPropMessage.(*protobuf.ProtoEnum); ok { + mapValueType = enum.Name + } + + message.Fields = append(message.Fields, &protobuf.ProtoField{ + Name: "additional_properties", + Type: "map", + }) + } + + message.Description = description + result = message + } + + // If result is still nil, construct a default ProtoField + if result == nil { + result = &protobuf.ProtoField{ + Name: c.applySnakeCaseNamingOption(protoName), + Type: protoType, + Description: description, + } + } + + return result, nil +} + +// handleOneOf processes oneOf schemas +func (c *ProtoConverter) handleOneOf(oneOfSchemas []*openapi3.SchemaRef, protoName string, parentMessage *protobuf.ProtoMessage) (*protobuf.ProtoOneOf, error) { + oneOf := &protobuf.ProtoOneOf{ + Name: c.applyPascaleCaseNamingOption(protoName + "OneOf"), + } + + for i, schemaRef := range oneOfSchemas { + fieldName := fmt.Sprintf("%sOption%d", protoName, i+1) + protoType, err := c.ConvertSchemaToProtoType(schemaRef, fieldName, parentMessage) + if err != nil { + return nil, err + } + switch v := protoType.(type) { + case *protobuf.ProtoField: + oneOf.Fields = append(oneOf.Fields, v) + case *protobuf.ProtoMessage: + newField := &protobuf.ProtoField{ + Name: c.applySnakeCaseNamingOption(v.Name + "_field"), + Type: v.Name, + } + c.addNestedMessageToParent(parentMessage, v) + oneOf.Fields = append(oneOf.Fields, newField) + case *protobuf.ProtoEnum: + newField := &protobuf.ProtoField{ + Name: c.applySnakeCaseNamingOption(v.Name + "_field"), + Type: v.Name, + } + c.addNestedEnumToParent(parentMessage, v) + oneOf.Fields = append(oneOf.Fields, newField) + case *protobuf.ProtoOneOf: + c.addNestedOneOfToParent(parentMessage, v) + } + } + return oneOf, nil +} + +// handleAllOf processes allOf schemas +func (c *ProtoConverter) handleAllOf(allOfSchemas []*openapi3.SchemaRef, protoName string, parentMessage *protobuf.ProtoMessage) (*protobuf.ProtoMessage, error) { + allOfMessage := &protobuf.ProtoMessage{ + Name: c.applyPascaleCaseNamingOption(protoName + "AllOf"), + } + + for i, schemaRef := range allOfSchemas { + fieldName := fmt.Sprintf("%sPart%d", protoName, i+1) + protoType, err := c.ConvertSchemaToProtoType(schemaRef, fieldName, parentMessage) + if err != nil { + return nil, err + } + + switch v := protoType.(type) { + case *protobuf.ProtoField: + allOfMessage.Fields = append(allOfMessage.Fields, v) + case *protobuf.ProtoMessage: + newField := &protobuf.ProtoField{ + Name: c.applySnakeCaseNamingOption(v.Name + "_field"), + Type: v.Name, + } + c.addNestedMessageToParent(allOfMessage, v) + allOfMessage.Fields = append(allOfMessage.Fields, newField) + case *protobuf.ProtoEnum: + newField := &protobuf.ProtoField{ + Name: c.applySnakeCaseNamingOption(v.Name + "_field"), + Type: v.Name, + } + c.addNestedEnumToParent(allOfMessage, v) + allOfMessage.Fields = append(allOfMessage.Fields, newField) + case *protobuf.ProtoOneOf: + c.addNestedOneOfToParent(allOfMessage, v) + } + } + + return allOfMessage, nil +} + +// handleAnyOf processes anyOf schemas +func (c *ProtoConverter) handleAnyOf(anyOfSchemas []*openapi3.SchemaRef, protoName string, parentMessage *protobuf.ProtoMessage) (*protobuf.ProtoMessage, error) { + anyOfMessage := &protobuf.ProtoMessage{ + Name: c.applyPascaleCaseNamingOption(protoName + "AnyOf"), + } + + for i, schemaRef := range anyOfSchemas { + fieldName := fmt.Sprintf("%sOption%d", protoName, i+1) + protoType, err := c.ConvertSchemaToProtoType(schemaRef, fieldName, parentMessage) + if err != nil { + return nil, err + } + + switch v := protoType.(type) { + case *protobuf.ProtoField: + anyOfMessage.Fields = append(anyOfMessage.Fields, v) + case *protobuf.ProtoMessage: + newField := &protobuf.ProtoField{ + Name: c.applySnakeCaseNamingOption(v.Name + "_field"), + Type: v.Name, + } + c.addNestedMessageToParent(anyOfMessage, v) + anyOfMessage.Fields = append(anyOfMessage.Fields, newField) + case *protobuf.ProtoEnum: + newField := &protobuf.ProtoField{ + Name: c.applySnakeCaseNamingOption(v.Name + "_field"), + Type: v.Name, + } + c.addNestedEnumToParent(anyOfMessage, v) + anyOfMessage.Fields = append(anyOfMessage.Fields, newField) + case *protobuf.ProtoOneOf: + c.addNestedOneOfToParent(anyOfMessage, v) + } + } + + return anyOfMessage, nil +} + +// applyPascaleCaseNamingOption applies naming convention based on the converter's naming option +func (c *ProtoConverter) applyPascaleCaseNamingOption(name string) string { + if c.converterOption.NamingOption { + return common.ToPascaleCase(name) + } + return name +} + +// applySnakeCaseNamingOption applies naming convention based on the converter's naming option +func (c *ProtoConverter) applySnakeCaseNamingOption(name string) string { + if c.converterOption.NamingOption { + return common.ToSnakeCase(name) + } + return name +} + +// addOptionsToProto adds options to the ProtoFile +func (c *ProtoConverter) addOptionsToProto() { + optionStr := common.StructToOption(c.spec, "") + + schemaOption := &protobuf.Option{ + Name: consts.OpenapiDocument, + Value: optionStr, + } + c.ProtoFile.Options = append(c.ProtoFile.Options, schemaOption) + c.AddProtoImport(consts.OpenapiProtoFile) +} + +// Add a new method to handle structured extensions +func (c *ProtoConverter) addExtensionsToProtoOptions() error { + // Check for x-option in spec extensions + if xOption, ok := c.spec.Extensions["x-options"]; ok { + if optionMap, ok := xOption.(map[string]interface{}); ok { + for key, value := range optionMap { + option := &protobuf.Option{ + Name: key, + Value: fmt.Sprintf("%q", value), + } + c.ProtoFile.Options = append(c.ProtoFile.Options, option) + } + } + } + + // Check for x-option in spec.info.extensions + if c.spec.Info != nil { + if xOption, ok := c.spec.Info.Extensions["x-options"]; ok { + if optionMap, ok := xOption.(map[string]interface{}); ok { + for key, value := range optionMap { + option := &protobuf.Option{ + Name: key, + Value: fmt.Sprintf("%q", value), + } + c.ProtoFile.Options = append(c.ProtoFile.Options, option) + } + } + } + } + + return nil +} + +// addNestedMessageToParent adds a nested message to a parent message +func (c *ProtoConverter) addNestedMessageToParent(parentMessage, nestedMessage *protobuf.ProtoMessage) { + if parentMessage != nil && nestedMessage != nil { + parentMessage.Messages = append(parentMessage.Messages, nestedMessage) + } +} + +// addNestedEnum adds a nested Enum to a parent message +func (c *ProtoConverter) addNestedEnumToParent(parentMessage *protobuf.ProtoMessage, nestedEnum *protobuf.ProtoEnum) { + if parentMessage != nil && nestedEnum != nil { + parentMessage.Enums = append(parentMessage.Enums, nestedEnum) + } +} + +// addNestedOneOfToParent adds a nested oneOf to a parent message +func (c *ProtoConverter) addNestedOneOfToParent(parentMessage *protobuf.ProtoMessage, nestedOneOf *protobuf.ProtoOneOf) { + if parentMessage != nil && nestedOneOf != nil { + parentMessage.OneOfs = append(parentMessage.OneOfs, nestedOneOf) + } +} + +// mergeProtoMessage merges a ProtoMessage into the ProtoFile +func (c *ProtoConverter) addMessageToProto(message *protobuf.ProtoMessage) error { + var existingMessage *protobuf.ProtoMessage + for _, msg := range c.ProtoFile.Messages { + if msg.Name == message.Name { + existingMessage = msg + break + } + } + + // merge message + if existingMessage != nil { + // merge Fields + fieldNames := make(map[string]struct{}) + for _, field := range existingMessage.Fields { + fieldNames[field.Name] = struct{}{} + } + for _, newField := range message.Fields { + if _, exists := fieldNames[newField.Name]; !exists { + existingMessage.Fields = append(existingMessage.Fields, newField) + } + } + + // merge Messages + messageNames := make(map[string]struct{}) + for _, nestedMsg := range existingMessage.Messages { + messageNames[nestedMsg.Name] = struct{}{} + } + for _, newMessage := range message.Messages { + if _, exists := messageNames[newMessage.Name]; !exists { + existingMessage.Messages = append(existingMessage.Messages, newMessage) + } + } + + // merge Enums + enumNames := make(map[string]struct{}) + for _, enum := range existingMessage.Enums { + enumNames[enum.Name] = struct{}{} + } + for _, newEnum := range message.Enums { + if _, exists := enumNames[newEnum.Name]; !exists { + existingMessage.Enums = append(existingMessage.Enums, newEnum) + } + } + + // merge Options + optionNames := make(map[string]struct{}) + for _, option := range existingMessage.Options { + optionNames[option.Name] = struct{}{} + } + for _, newOption := range message.Options { + if _, exists := optionNames[newOption.Name]; !exists { + existingMessage.Options = append(existingMessage.Options, newOption) + } + } + } else { + c.ProtoFile.Messages = append(c.ProtoFile.Messages, message) + } + + return nil +} + +// addEnumToProto adds an enum to the ProtoFile +func (c *ProtoConverter) addEnumToProto(enum *protobuf.ProtoEnum) { + c.ProtoFile.Enums = append(c.ProtoFile.Enums, enum) +} + +// AddProtoImport adds an import to the ProtoFile +func (c *ProtoConverter) AddProtoImport(importFile string) { + if c.ProtoFile != nil { + for _, existingImport := range c.ProtoFile.Imports { + if existingImport == importFile { + return + } + } + c.ProtoFile.Imports = append(c.ProtoFile.Imports, importFile) + } +} + +// addFieldIfNotExists adds a field to Fields if it does not already exist +func (c *ProtoConverter) addFieldIfNotExists(fields *[]*protobuf.ProtoField, field *protobuf.ProtoField) { + for _, existingField := range *fields { + if existingField.Name == field.Name { + return + } + } + *fields = append(*fields, field) +} + +// addMessageIfNotExists adds a message to Messages if it does not already exist +func (c *ProtoConverter) addMessageIfNotExists(messages *[]*protobuf.ProtoMessage, nestedMessage *protobuf.ProtoMessage) { + for _, existingMessage := range *messages { + if existingMessage.Name == nestedMessage.Name { + return + } + } + *messages = append(*messages, nestedMessage) +} + +// methodExistsInService checks if a method exists in a service +func (c *ProtoConverter) methodExistsInService(service *protobuf.ProtoService, methodName string) bool { + for _, method := range service.Methods { + if method.Name == methodName { + return true + } + } + return false +} + +// findOrCreateService finds an existing service by name or creates a new one if it doesn't exist +func (c *ProtoConverter) findOrCreateService(serviceName string) *protobuf.ProtoService { + // Iterate over existing services to find a match + for i := range c.ProtoFile.Services { + if c.ProtoFile.Services[i].Name == serviceName { + return c.ProtoFile.Services[i] + } + } + + // If no existing service is found, create a new one + newService := &protobuf.ProtoService{Name: serviceName} + c.ProtoFile.Services = append(c.ProtoFile.Services, newService) + return newService +} diff --git a/swagger2idl/converter/thrift_converter.go b/swagger2idl/converter/thrift_converter.go new file mode 100644 index 0000000..e24532c --- /dev/null +++ b/swagger2idl/converter/thrift_converter.go @@ -0,0 +1,1297 @@ +/* + * Copyright 2024 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package converter + +import ( + "errors" + "fmt" + + "github.com/getkin/kin-openapi/openapi3" + "github.com/hertz-contrib/swagger-generate/common/consts" + common "github.com/hertz-contrib/swagger-generate/common/utils" + "github.com/hertz-contrib/swagger-generate/swagger2idl/thrift" + "github.com/hertz-contrib/swagger-generate/swagger2idl/utils" +) + +// ThriftConverter struct, used to convert OpenAPI specifications into Thrift files +type ThriftConverter struct { + spec *openapi3.T + ThriftFile *thrift.ThriftFile + converterOption *ConvertOption +} + +// NewThriftConverter creates and initializes a ThriftConverter +func NewThriftConverter(spec *openapi3.T, option *ConvertOption) *ThriftConverter { + return &ThriftConverter{ + spec: spec, + ThriftFile: &thrift.ThriftFile{ + Namespace: map[string]string{}, + Includes: []string{}, + Structs: []*thrift.ThriftStruct{}, + Enums: []*thrift.ThriftEnum{}, + Services: []*thrift.ThriftService{}, + }, + converterOption: option, + } +} + +// Convert converts the OpenAPI specification to a Thrift file +func (c *ThriftConverter) Convert() error { + // Convert the go Option to Thrift + err := c.addExtensionsToProtoOptions() + if err != nil { + return fmt.Errorf("error parsing extensions to proto options: %w", err) + } + + // Convert tags into Thrift services + c.convertTagsToThriftServices() + + // Convert components into Thrift messages + err = c.convertComponentsToThriftMessages() + if err != nil { + return fmt.Errorf("error converting components to thrift messages: %w", err) + } + + // Convert paths into Thrift services + err = c.convertPathsToThriftServices() + if err != nil { + return fmt.Errorf("error converting paths to thrift services: %w", err) + } + + if c.converterOption.OpenapiOption { + c.addOptionsToThrift() + } + + return nil +} + +func (c *ThriftConverter) GetIdl() interface{} { + return c.ThriftFile +} + +// convertTagsToThriftServices converts OpenAPI tags into Thrift services and stores them in the ThriftFile +func (c *ThriftConverter) convertTagsToThriftServices() { + tags := c.spec.Tags + for _, tag := range tags { + serviceName := common.ToPascaleCase(tag.Name) + service := &thrift.ThriftService{ + Name: serviceName, + Description: tag.Description, + } + c.ThriftFile.Services = append(c.ThriftFile.Services, service) + } +} + +// convertComponentsToThriftMessages converts OpenAPI components into Thrift messages and stores them in the ThriftFile +func (c *ThriftConverter) convertComponentsToThriftMessages() error { + components := c.spec.Components + if components == nil { + return nil + } + + if components.Schemas == nil { + return nil + } + + for name, schemaRef := range components.Schemas { + schema := schemaRef + + if c.converterOption.NamingOption { + name = common.ToPascaleCase(name) + } + + thriftType, err := c.ConvertSchemaToThriftType(schema, name, nil) + if err != nil { + return fmt.Errorf("error converting schema %s: %w", name, err) + } + + switch v := thriftType.(type) { + case *thrift.ThriftField: + message := &thrift.ThriftStruct{ + Name: name, + Fields: []*thrift.ThriftField{v}, + } + if c.converterOption.OpenapiOption { + optionStr := common.StructToOption(schema.Value, " ") + + schemaOption := &thrift.Option{ + Name: consts.OpenapiSchema, + Value: optionStr, + } + message.Options = append(message.Options, schemaOption) + c.AddThriftInclude(consts.OpenapiThriftFile) + } + c.addMessageToThrift(message) + case *thrift.ThriftStruct: + if c.converterOption.OpenapiOption { + optionStr := common.StructToOption(schema.Value, " ") + + schemaOption := &thrift.Option{ + Name: consts.OpenapiSchema, + Value: optionStr, + } + v.Options = append(v.Options, schemaOption) + c.AddThriftInclude(consts.OpenapiThriftFile) + } + c.addMessageToThrift(v) + case *thrift.ThriftEnum: + if c.converterOption.OpenapiOption { + optionStr := common.StructToOption(schema.Value, " ") + + schemaOption := &thrift.Option{ + Name: consts.OpenapiSchema, + Value: optionStr, + } + v.Options = append(v.Options, schemaOption) + c.AddThriftInclude(consts.OpenapiThriftFile) + } + c.addEnumToThrift(v) + case *thrift.ThriftUnion: + if c.converterOption.OpenapiOption { + optionStr := common.StructToOption(schema.Value, " ") + + schemaOption := &thrift.Option{ + Name: consts.OpenapiSchema, + Value: optionStr, + } + v.Options = append(v.Options, schemaOption) + c.AddThriftInclude(consts.OpenapiThriftFile) + } + c.addUnionToThrift(v) + } + } + return nil +} + +// convertPathsToThriftServices converts OpenAPI path items into Thrift services and stores them in the ThriftFile +func (c *ThriftConverter) convertPathsToThriftServices() error { + paths := c.spec.Paths + services, err := c.ConvertPathsToThriftServices(paths) + if err != nil { + return fmt.Errorf("error converting paths to thrift services: %w", err) + } + + c.ThriftFile.Services = append(c.ThriftFile.Services, services...) + return nil +} + +// ConvertPathsToThriftServices converts OpenAPI path items into Thrift services +func (c *ThriftConverter) ConvertPathsToThriftServices(paths *openapi3.Paths) ([]*thrift.ThriftService, error) { + var services []*thrift.ThriftService + + for path, pathItem := range paths.Map() { + for method, operation := range pathItem.Operations() { + serviceName := utils.GetServiceName(operation) + methodName := utils.GetMethodName(operation, path, method) + + if c.converterOption.NamingOption { + serviceName = common.ToPascaleCase(serviceName) + methodName = common.ToPascaleCase(methodName) + } + + inputMessage, err := c.generateRequestMessage(operation, methodName) + if err != nil { + return nil, fmt.Errorf("error generating request message for %s: %w", methodName, err) + } + + outputMessage, err := c.generateResponseMessage(operation, methodName) + if err != nil { + return nil, fmt.Errorf("error generating response message for %s: %w", methodName, err) + } + + service := c.findOrCreateService(serviceName) + + if !c.methodExistsInService(service, methodName) { + thriftMethod := &thrift.ThriftMethod{ + Name: methodName, + Input: inputMessage, + Output: outputMessage, + } + + if c.converterOption.ApiOption { + if optionName, ok := MethodToOption[method]; ok { + option := &thrift.Option{ + Name: optionName, + Value: fmt.Sprintf("%q", utils.ConvertPath(path)), + } + thriftMethod.Options = append(thriftMethod.Options, option) + } + } + + if c.converterOption.OpenapiOption { + optionStr := common.StructToOption(operation, " ") + + schemaOption := &thrift.Option{ + Name: "openapi.operation", + Value: optionStr, + } + thriftMethod.Options = append(thriftMethod.Options, schemaOption) + c.AddThriftInclude(consts.OpenapiThriftFile) + } + service.Methods = append(service.Methods, thriftMethod) + } + } + } + + return services, nil +} + +// generateRequestMessage generates a request message for an operation +func (c *ThriftConverter) generateRequestMessage(operation *openapi3.Operation, methodName string) ([]string, error) { + messageName := utils.GetMessageName(operation, methodName, "Request") + + if c.converterOption.NamingOption { + messageName = common.ToPascaleCase(messageName) + } + + message := &thrift.ThriftStruct{Name: messageName} + + if operation.RequestBody == nil && len(operation.Parameters) == 0 { + return []string{""}, nil + } + + if operation.RequestBody != nil { + if operation.RequestBody.Ref != "" { + return []string{common.ToPascaleCase(utils.ExtractMessageNameFromRef(operation.RequestBody.Ref))}, nil + } + + if operation.RequestBody.Value != nil && len(operation.RequestBody.Value.Content) > 0 { + for mediaTypeStr, mediaType := range operation.RequestBody.Value.Content { + schema := mediaType.Schema + if schema != nil { + thriftType, err := c.ConvertSchemaToThriftType(schema, common.FormatStr(mediaTypeStr), message) + if err != nil { + return []string{""}, err + } + + switch v := thriftType.(type) { + case *thrift.ThriftField: + if c.converterOption.ApiOption { + var optionName string + if mediaTypeStr == "application/json" { + optionName = "api.body" + } else if mediaTypeStr == "application/x-www-form-urlencoded" || mediaTypeStr == "multipart/form-data" { + optionName = "api.form" + } + if optionName != "" { + v.Options = append(v.Options, &thrift.Option{ + Name: optionName, + Value: fmt.Sprintf("%q", v.Name), + }) + } + } + c.addFieldIfNotExists(&message.Fields, v) + case *thrift.ThriftStruct: + for _, field := range v.Fields { + if c.converterOption.ApiOption { + var optionName string + if mediaTypeStr == "application/json" { + optionName = "api.body" + } else if mediaTypeStr == "application/x-www-form-urlencoded" || mediaTypeStr == "multipart/form-data" { + optionName = "api.form" + } + if optionName != "" { + field.Options = append(field.Options, &thrift.Option{ + Name: optionName, + Value: fmt.Sprintf("%q", field.Name), + }) + } + } + c.addFieldIfNotExists(&message.Fields, field) + } + case *thrift.ThriftEnum: + newField := &thrift.ThriftField{ + Name: c.applySnakeCaseNamingOption(mediaTypeStr + "_field"), + Type: v.Name, + } + if c.converterOption.ApiOption { + var optionName string + if mediaTypeStr == "application/json" { + optionName = "api.body" + } else if mediaTypeStr == "application/x-www-form-urlencoded" || mediaTypeStr == "multipart/form-data" { + optionName = "api.form" + } + if optionName != "" { + newField.Options = append(newField.Options, &thrift.Option{ + Name: optionName, + Value: fmt.Sprintf("%q", v.Name), + }) + } + } + if c.converterOption.OpenapiOption { + optionStr := common.StructToOption(operation.RequestBody.Value, " ") + + schemaOption := &thrift.Option{ + Name: consts.OpenapiProperty, + Value: optionStr, + } + newField.Options = append(newField.Options, schemaOption) + c.AddThriftInclude(consts.OpenapiThriftFile) + } + message.Fields = append(message.Fields, newField) + c.addEnumToThrift(v) + case *thrift.ThriftUnion: + newField := &thrift.ThriftField{ + Name: c.applySnakeCaseNamingOption(mediaTypeStr + "_field"), + Type: v.Name, + } + if c.converterOption.ApiOption { + var optionName string + if mediaTypeStr == "application/json" { + optionName = "api.body" + } else if mediaTypeStr == "application/x-www-form-urlencoded" || mediaTypeStr == "multipart/form-data" { + optionName = "api.form" + } + if optionName != "" { + newField.Options = append(newField.Options, &thrift.Option{ + Name: optionName, + Value: fmt.Sprintf("%q", v.Name), + }) + } + } + if c.converterOption.OpenapiOption { + optionStr := common.StructToOption(operation.RequestBody.Value, " ") + + schemaOption := &thrift.Option{ + Name: consts.OpenapiProperty, + Value: optionStr, + } + newField.Options = append(newField.Options, schemaOption) + c.AddThriftInclude(consts.OpenapiThriftFile) + } + message.Fields = append(message.Fields, newField) + c.addUnionToThrift(v) + } + } + } + } + } + + if len(operation.Parameters) > 0 { + for _, param := range operation.Parameters { + if param.Value.Schema != nil { + fieldOrMessage, err := c.ConvertSchemaToThriftType(param.Value.Schema, param.Value.Name, message) + if err != nil { + return []string{""}, err + } + + switch v := fieldOrMessage.(type) { + case *thrift.ThriftField: + if c.converterOption.ApiOption { + v.Options = append(v.Options, &thrift.Option{ + Name: "api." + param.Value.In, + Value: fmt.Sprintf("%q", param.Value.Name), + }) + } + if c.converterOption.OpenapiOption { + optionStr := common.StructToOption(param.Value, " ") + + schemaOption := &thrift.Option{ + Name: consts.OpenapiParameter, + Value: optionStr, + } + v.Options = append(v.Options, schemaOption) + c.AddThriftInclude(consts.OpenapiThriftFile) + } + v.Description = param.Value.Description + c.addFieldIfNotExists(&message.Fields, v) + case *thrift.ThriftStruct: + for _, field := range v.Fields { + if c.converterOption.ApiOption { + field.Options = append(field.Options, &thrift.Option{ + Name: "api." + param.Value.In, + Value: fmt.Sprintf("%q", param.Value.Name), + }) + } + if c.converterOption.OpenapiOption { + optionStr := common.StructToOption(param.Value, " ") + + schemaOption := &thrift.Option{ + Name: consts.OpenapiParameter, + Value: optionStr, + } + field.Options = append(field.Options, schemaOption) + c.AddThriftInclude(consts.OpenapiThriftFile) + } + c.addFieldIfNotExists(&message.Fields, field) + } + case *thrift.ThriftEnum: + newField := &thrift.ThriftField{ + Name: c.applySnakeCaseNamingOption(param.Value.Name + "_field"), + Type: v.Name, + } + if c.converterOption.OpenapiOption { + optionStr := common.StructToOption(param.Value, " ") + + schemaOption := &thrift.Option{ + Name: consts.OpenapiParameter, + Value: optionStr, + } + newField.Options = append(newField.Options, schemaOption) + c.AddThriftInclude(consts.OpenapiThriftFile) + } + message.Fields = append(message.Fields, newField) + c.addEnumToThrift(v) + case *thrift.ThriftUnion: + newField := &thrift.ThriftField{ + Name: c.applySnakeCaseNamingOption(param.Value.Name + "_field"), + Type: v.Name, + } + if c.converterOption.ApiOption { + newField.Options = append(newField.Options, &thrift.Option{ + Name: "api." + param.Value.In, + Value: fmt.Sprintf("%q", param.Value.Name), + }) + } + if c.converterOption.OpenapiOption { + optionStr := common.StructToOption(param.Value, " ") + + schemaOption := &thrift.Option{ + Name: consts.OpenapiParameter, + Value: optionStr, + } + newField.Options = append(newField.Options, schemaOption) + c.AddThriftInclude(consts.OpenapiThriftFile) + } + message.Fields = append(message.Fields, newField) + c.addUnionToThrift(v) + } + } + } + } + + // if there are no fields or messages, return an empty message + if len(message.Fields) > 0 { + c.addMessageToThrift(message) + return []string{message.Name}, nil + } + + return []string{""}, nil +} + +// generateResponseMessage generates a response message for an operation +func (c *ThriftConverter) generateResponseMessage(operation *openapi3.Operation, methodName string) (string, error) { + if operation.Responses == nil { + return "", nil + } + + responses := operation.Responses.Map() + responseCount := 0 + for _, responseRef := range responses { + if responseRef.Ref == "" && (responseRef.Value == nil || (len(responseRef.Value.Content) == 0 && len(responseRef.Value.Headers) == 0)) { + continue + } + responseCount++ + } + + if responseCount == 1 { + for _, responseRef := range responses { + if responseRef.Ref == "" && (responseRef.Value == nil || (len(responseRef.Value.Content) == 0 && len(responseRef.Value.Headers) == 0)) { + continue + } + return c.processSingleResponse("", responseRef, operation, methodName) + } + } + + if responseCount == 0 { + return "void", nil + } + + // create a wrapper message for multiple responses + wrapperMessageName := utils.GetMessageName(operation, methodName, "Response") + if c.converterOption.NamingOption { + wrapperMessageName = common.ToPascaleCase(wrapperMessageName) + } + + wrapperMessage := &thrift.ThriftStruct{Name: wrapperMessageName} + + emptyFlag := true + + for statusCode, responseRef := range responses { + if responseRef.Ref == "" && (responseRef.Value == nil || len(responseRef.Value.Content) == 0) { + break + } + emptyFlag = false + messageName, err := c.processSingleResponse(statusCode, responseRef, operation, methodName) + if err != nil { + return "", err + } + + name := "Response_" + statusCode + if c.converterOption.NamingOption { + name = common.ToSnakeCase(name) + } + field := &thrift.ThriftField{ + Name: name, + Type: messageName, + } + wrapperMessage.Fields = append(wrapperMessage.Fields, field) + } + + if emptyFlag { + // c.AddThriftInclude(emptyThriftFile) + return "void", nil + } + + c.addMessageToThrift(wrapperMessage) + + return wrapperMessage.Name, nil +} + +// processSingleResponse deals with a single response in an operation +func (c *ThriftConverter) processSingleResponse(statusCode string, responseRef *openapi3.ResponseRef, operation *openapi3.Operation, methodName string) (string, error) { + if responseRef.Ref != "" { + return common.ToPascaleCase(utils.ExtractMessageNameFromRef(responseRef.Ref)), nil + } + + response := responseRef.Value + messageName := utils.GetMessageName(operation, methodName, "Response") + common.ToUpperCase(statusCode) + + if c.converterOption.NamingOption { + messageName = common.ToPascaleCase(messageName) + } + + message := &thrift.ThriftStruct{Name: messageName} + + if len(response.Headers) > 0 { + for headerName, headerRef := range response.Headers { + if headerRef != nil { + + fieldOrMessage, err := c.ConvertSchemaToThriftType(headerRef.Value.Schema, headerName, message) + if err != nil { + return "", err + } + + switch v := fieldOrMessage.(type) { + case *thrift.ThriftField: + if c.converterOption.ApiOption { + option := &thrift.Option{ + Name: "api.header", + Value: fmt.Sprintf("%q", headerName), + } + v.Options = append(v.Options, option) + } + if c.converterOption.OpenapiOption { + optionStr := common.StructToOption(headerRef.Value, " ") + + schemaOption := &thrift.Option{ + Name: consts.OpenapiProperty, + Value: optionStr, + } + v.Options = append(v.Options, schemaOption) + c.AddThriftInclude(consts.OpenapiThriftFile) + } + c.addFieldIfNotExists(&message.Fields, v) + case *thrift.ThriftStruct: + for _, field := range v.Fields { + if c.converterOption.ApiOption { + option := &thrift.Option{ + Name: "api.header", + Value: fmt.Sprintf("%q", field.Name), + } + field.Options = append(field.Options, option) + } + if c.converterOption.OpenapiOption { + optionStr := common.StructToOption(headerRef.Value, " ") + + schemaOption := &thrift.Option{ + Name: consts.OpenapiProperty, + Value: optionStr, + } + field.Options = append(field.Options, schemaOption) + c.AddThriftInclude(consts.OpenapiThriftFile) + } + c.addFieldIfNotExists(&message.Fields, field) + } + case *thrift.ThriftEnum: + newField := &thrift.ThriftField{ + Name: c.applySnakeCaseNamingOption(headerName + "_field"), + Type: v.Name, + } + if c.converterOption.ApiOption { + option := &thrift.Option{ + Name: "api.header", + Value: fmt.Sprintf("%q", headerName), + } + newField.Options = append(newField.Options, option) + } + if c.converterOption.OpenapiOption { + optionStr := common.StructToOption(headerRef.Value, " ") + + schemaOption := &thrift.Option{ + Name: consts.OpenapiProperty, + Value: optionStr, + } + newField.Options = append(newField.Options, schemaOption) + c.AddThriftInclude(consts.OpenapiThriftFile) + } + message.Fields = append(message.Fields, newField) + c.addEnumToThrift(v) + case *thrift.ThriftUnion: + newField := &thrift.ThriftField{ + Name: c.applySnakeCaseNamingOption(headerName + "_field"), + Type: v.Name, + } + if c.converterOption.ApiOption { + option := &thrift.Option{ + Name: "api.header", + Value: fmt.Sprintf("%q", headerName), + } + newField.Options = append(newField.Options, option) + } + if c.converterOption.OpenapiOption { + optionStr := common.StructToOption(headerRef.Value, " ") + + schemaOption := &thrift.Option{ + Name: consts.OpenapiProperty, + Value: optionStr, + } + newField.Options = append(newField.Options, schemaOption) + c.AddThriftInclude(consts.OpenapiThriftFile) + } + message.Fields = append(message.Fields, newField) + c.addUnionToThrift(v) + } + } + } + } + + for mediaTypeStr, mediaType := range response.Content { + schema := mediaType.Schema + if schema != nil { + + thriftType, err := c.ConvertSchemaToThriftType(schema, common.FormatStr(mediaTypeStr), message) + if err != nil { + return "", err + } + + switch v := thriftType.(type) { + case *thrift.ThriftField: + if c.converterOption.ApiOption && mediaTypeStr == "application/json" { + option := &thrift.Option{ + Name: "api.body", + Value: fmt.Sprintf("%q", v.Name), + } + v.Options = append(v.Options, option) + } + c.addFieldIfNotExists(&message.Fields, v) + case *thrift.ThriftStruct: + for _, field := range v.Fields { + if c.converterOption.ApiOption && mediaTypeStr == "application/json" { + option := &thrift.Option{ + Name: "api.body", + Value: fmt.Sprintf("%q", field.Name), + } + field.Options = append(field.Options, option) + } + if c.converterOption.OpenapiOption { + optionStr := common.StructToOption(schema, " ") + + schemaOption := &thrift.Option{ + Name: consts.OpenapiProperty, + Value: optionStr, + } + field.Options = append(field.Options, schemaOption) + c.AddThriftInclude(consts.OpenapiThriftFile) + } + c.addFieldIfNotExists(&message.Fields, field) + } + case *thrift.ThriftEnum: + newField := &thrift.ThriftField{ + Name: c.applySnakeCaseNamingOption(mediaTypeStr + "_field"), + Type: v.Name, + } + if c.converterOption.ApiOption && mediaTypeStr == "application/json" { + option := &thrift.Option{ + Name: "api.body", + Value: fmt.Sprintf("%q", v.Name), + } + newField.Options = append(newField.Options, option) + } + if c.converterOption.OpenapiOption { + optionStr := common.StructToOption(schema, " ") + + schemaOption := &thrift.Option{ + Name: consts.OpenapiProperty, + Value: optionStr, + } + newField.Options = append(newField.Options, schemaOption) + c.AddThriftInclude(consts.OpenapiThriftFile) + } + message.Fields = append(message.Fields, newField) + c.addEnumToThrift(v) + case *thrift.ThriftUnion: + newField := &thrift.ThriftField{ + Name: c.applySnakeCaseNamingOption(mediaTypeStr + "_field"), + Type: v.Name, + } + if c.converterOption.ApiOption && mediaTypeStr == "application/json" { + option := &thrift.Option{ + Name: "api.body", + Value: fmt.Sprintf("%q", v.Name), + } + newField.Options = append(newField.Options, option) + } + if c.converterOption.OpenapiOption { + optionStr := common.StructToOption(schema, " ") + + schemaOption := &thrift.Option{ + Name: consts.OpenapiProperty, + Value: optionStr, + } + newField.Options = append(newField.Options, schemaOption) + c.AddThriftInclude(consts.OpenapiThriftFile) + } + message.Fields = append(message.Fields, newField) + c.addUnionToThrift(v) + } + } + } + + if len(message.Fields) > 0 { + c.addMessageToThrift(message) + return message.Name, nil + } + return "", nil +} + +// ConvertSchemaToThriftType converts an OpenAPI schema to a Thrift field or message +func (c *ThriftConverter) ConvertSchemaToThriftType( + schemaRef *openapi3.SchemaRef, + thriftName string, + parentMessage *thrift.ThriftStruct, +) (interface{}, error) { + var thriftType string + var result interface{} + + // Handle referenced schema + if schemaRef.Ref != "" { + name := c.applySnakeCaseNamingOption(utils.ExtractMessageNameFromRef(schemaRef.Ref)) + return &thrift.ThriftField{ + Name: name, + Type: common.ToPascaleCase(utils.ExtractMessageNameFromRef(schemaRef.Ref)), + }, nil + } + + // Ensure schema value is valid + if schemaRef.Value == nil { + return nil, errors.New("schema type is required") + } + + schema := schemaRef.Value + description := schema.Description + + // Handle oneOf, allOf, anyOf even if schema.Type is nil + if len(schema.OneOf) > 0 { + thriftStruct, err := c.handleOneOf(schema.OneOf, thriftName, parentMessage) + if err != nil { + return nil, err + } + return thriftStruct, nil + } else if len(schema.AllOf) > 0 { + thriftStruct, err := c.handleAllOf(schema.AllOf, thriftName, parentMessage) + if err != nil { + return nil, err + } + return thriftStruct, nil + } else if len(schema.AnyOf) > 0 { + thriftStruct, err := c.handleAnyOf(schema.AnyOf, thriftName, parentMessage) + if err != nil { + return nil, err + } + return thriftStruct, nil + } + + // Process schema type + switch { + case schema.Type.Includes("string"): + if schema.Format == "date" || schema.Format == "date-time" { + thriftType = "string" + } else if schema.Format == "byte" || schema.Format == "binary" { + thriftType = "binary" + } else if len(schema.Enum) != 0 { + var name string + if parentMessage == nil { + name = thriftName + } else { + name = c.applyPascalseCaseNamingOption(common.ToUpperCase(thriftName)) + } + thriftEnum := &thrift.ThriftEnum{ + Name: name, + Description: description, + } + for i, enumValue := range schema.Enum { + thriftEnum.Values = append(thriftEnum.Values, &thrift.ThriftEnumValue{ + Index: i, + Value: enumValue, + }) + } + result = thriftEnum + } else { + thriftType = "string" + } + + case schema.Type.Includes("integer"): + if len(schema.Enum) != 0 { + var name string + if parentMessage == nil { + name = thriftName + } else { + name = c.applyPascalseCaseNamingOption(common.ToUpperCase(thriftName)) + } + thriftEnum := &thrift.ThriftEnum{ + Name: name, + Description: description, + } + for i, enumValue := range schema.Enum { + thriftEnum.Values = append(thriftEnum.Values, &thrift.ThriftEnumValue{ + Index: i, + Value: enumValue, + }) + } + result = thriftEnum + } else if schema.Format == "int32" { + thriftType = "i32" + } else { + thriftType = "i64" + } + + case schema.Type.Includes("number"): + if len(schema.Enum) != 0 { + var name string + if parentMessage == nil { + name = thriftName + } else { + name = c.applyPascalseCaseNamingOption(common.ToUpperCase(thriftName)) + } + thriftEnum := &thrift.ThriftEnum{ + Name: name, + Description: description, + } + for i, enumValue := range schema.Enum { + thriftEnum.Values = append(thriftEnum.Values, &thrift.ThriftEnumValue{ + Index: i, + Value: enumValue, + }) + } + result = thriftEnum + } else if schema.Format == "float" { + thriftType = "float" + } else { + thriftType = "double" + } + + case schema.Type.Includes("boolean"): + thriftType = "bool" + + case schema.Type.Includes("array"): + if schema.Items != nil { + fieldOrMessage, err := c.ConvertSchemaToThriftType(schema.Items, thriftName+"Item", parentMessage) + if err != nil { + return nil, err + } + + fieldType := "" + if field, ok := fieldOrMessage.(*thrift.ThriftField); ok { + fieldType = field.Type + } else if nestedMessage, ok := fieldOrMessage.(*thrift.ThriftStruct); ok { + fieldType = nestedMessage.Name + c.addMessageToThrift(nestedMessage) + } else if enum, ok := fieldOrMessage.(*thrift.ThriftEnum); ok { + fieldType = enum.Name + c.addEnumToThrift(enum) + } else if union, ok := fieldOrMessage.(*thrift.ThriftUnion); ok { + fieldType = union.Name + c.addUnionToThrift(union) + } + + result = &thrift.ThriftField{ + Name: c.applySnakeCaseNamingOption(thriftName), + Type: fieldType, + Repeated: true, + Description: description, + } + } + + case schema.Type.Includes("object"): + + // Regular object handling + var message *thrift.ThriftStruct + if parentMessage == nil { + message = &thrift.ThriftStruct{Name: thriftName} + } else { + message = &thrift.ThriftStruct{Name: c.applyPascalseCaseNamingOption(common.ToUpperCase(thriftName))} + } + + // Process each property in the object + for propName, propSchema := range schema.Properties { + thriftType, err := c.ConvertSchemaToThriftType(propSchema, propName, message) + if err != nil { + return nil, err + } + + // Add the converted fields to the message + if field, ok := thriftType.(*thrift.ThriftField); ok { + if c.converterOption.OpenapiOption { + optionStr := common.StructToOption(propSchema, " ") + + schemaOption := &thrift.Option{ + Name: consts.OpenapiProperty, + Value: optionStr, + } + field.Options = append(field.Options, schemaOption) + c.AddThriftInclude(consts.OpenapiThriftFile) + } + message.Fields = append(message.Fields, field) + } else if nestedMessage, ok := thriftType.(*thrift.ThriftStruct); ok { + var name string + if c.converterOption.NamingOption { + name = common.ToSnakeCase(nestedMessage.Name) + } + newField := &thrift.ThriftField{ + Name: name + "_field", + Type: nestedMessage.Name, + } + if c.converterOption.OpenapiOption { + optionStr := common.StructToOption(propSchema.Value, " ") + + schemaOption := &thrift.Option{ + Name: consts.OpenapiProperty, + Value: optionStr, + } + newField.Options = append(newField.Options, schemaOption) + c.AddThriftInclude(consts.OpenapiThriftFile) + } + c.addMessageToThrift(nestedMessage) + message.Fields = append(message.Fields, newField) + } else if enum, ok := thriftType.(*thrift.ThriftEnum); ok { + c.addEnumToThrift(enum) + message.Fields = append(message.Fields, &thrift.ThriftField{ + Name: c.applySnakeCaseNamingOption(propName + "_field"), + Type: enum.Name, + }) + } else if union, ok := thriftType.(*thrift.ThriftUnion); ok { + c.addUnionToThrift(union) + message.Fields = append(message.Fields, &thrift.ThriftField{ + Name: c.applySnakeCaseNamingOption(propName + "_field"), + Type: union.Name, + }) + } + } + + // Handle additionalProperties if present + if schema.AdditionalProperties.Schema != nil { + mapValueType := "string" + additionalPropMessage, err := c.ConvertSchemaToThriftType(schema.AdditionalProperties.Schema, thriftName+"AdditionalProperties", parentMessage) + if err != nil { + return nil, err + } + if msg, ok := additionalPropMessage.(*thrift.ThriftStruct); ok { + mapValueType = msg.Name + } else if enum, ok := additionalPropMessage.(*thrift.ThriftEnum); ok { + mapValueType = enum.Name + } + + message.Fields = append(message.Fields, &thrift.ThriftField{ + Name: "additionalProperties", + Type: "map", + }) + } + + // Set the result as the final message + message.Description = description + result = message + } + + // If result is still nil, construct a default ThriftField + if result == nil { + result = &thrift.ThriftField{ + Name: c.applySnakeCaseNamingOption(thriftName), + Type: thriftType, + Description: description, + } + } + + return result, nil +} + +func (c *ThriftConverter) handleOneOf(oneOfSchemas []*openapi3.SchemaRef, thriftName string, parentMessage *thrift.ThriftStruct) (*thrift.ThriftUnion, error) { + oneOfUnion := &thrift.ThriftUnion{ + Name: c.applyPascalseCaseNamingOption(thriftName + "OneOf"), + } + + for i, schemaRef := range oneOfSchemas { + fieldName := fmt.Sprintf("%sOption%d", thriftName, i+1) + thriftType, err := c.ConvertSchemaToThriftType(schemaRef, fieldName, parentMessage) + if err != nil { + return nil, err + } + + switch v := thriftType.(type) { + case *thrift.ThriftField: + oneOfUnion.Fields = append(oneOfUnion.Fields, v) + case *thrift.ThriftStruct: + newField := &thrift.ThriftField{ + Name: c.applySnakeCaseNamingOption(v.Name + "_field"), + Type: v.Name, + } + c.addMessageToThrift(v) + oneOfUnion.Fields = append(oneOfUnion.Fields, newField) + case *thrift.ThriftEnum: + newField := &thrift.ThriftField{ + Name: c.applySnakeCaseNamingOption(v.Name + "_field"), + Type: v.Name, + } + c.addEnumToThrift(v) + oneOfUnion.Fields = append(oneOfUnion.Fields, newField) + case *thrift.ThriftUnion: + newField := &thrift.ThriftField{ + Name: c.applySnakeCaseNamingOption(v.Name + "_field"), + Type: v.Name, + } + c.addUnionToThrift(v) + oneOfUnion.Fields = append(oneOfUnion.Fields, newField) + } + } + + return oneOfUnion, nil +} + +func (c *ThriftConverter) handleAllOf(allOfSchemas []*openapi3.SchemaRef, thriftName string, parentMessage *thrift.ThriftStruct) (*thrift.ThriftStruct, error) { + allOfStruct := &thrift.ThriftStruct{ + Name: c.applyPascalseCaseNamingOption(thriftName + "AllOf"), + } + + for i, schemaRef := range allOfSchemas { + fieldName := fmt.Sprintf("%sPart%d", thriftName, i+1) + thriftType, err := c.ConvertSchemaToThriftType(schemaRef, fieldName, parentMessage) + if err != nil { + return nil, err + } + + switch v := thriftType.(type) { + case *thrift.ThriftField: + allOfStruct.Fields = append(allOfStruct.Fields, v) + case *thrift.ThriftStruct: + newField := &thrift.ThriftField{ + Name: c.applySnakeCaseNamingOption(v.Name + "_field"), + Type: v.Name, + } + c.addMessageToThrift(v) + allOfStruct.Fields = append(allOfStruct.Fields, newField) + case *thrift.ThriftEnum: + newField := &thrift.ThriftField{ + Name: c.applySnakeCaseNamingOption(v.Name + "_field"), + Type: v.Name, + } + c.addEnumToThrift(v) + allOfStruct.Fields = append(allOfStruct.Fields, newField) + case *thrift.ThriftUnion: + newField := &thrift.ThriftField{ + Name: c.applySnakeCaseNamingOption(v.Name + "_field"), + Type: v.Name, + } + c.addUnionToThrift(v) + allOfStruct.Fields = append(allOfStruct.Fields, newField) + } + } + + return allOfStruct, nil +} + +func (c *ThriftConverter) handleAnyOf(anyOfSchemas []*openapi3.SchemaRef, thriftName string, parentMessage *thrift.ThriftStruct) (*thrift.ThriftStruct, error) { + anyOfStruct := &thrift.ThriftStruct{ + Name: c.applyPascalseCaseNamingOption(thriftName + "AnyOf"), + } + + for i, schemaRef := range anyOfSchemas { + fieldName := fmt.Sprintf("%sOption%d", thriftName, i+1) + thriftType, err := c.ConvertSchemaToThriftType(schemaRef, fieldName, parentMessage) + if err != nil { + return nil, err + } + + switch v := thriftType.(type) { + case *thrift.ThriftField: + anyOfStruct.Fields = append(anyOfStruct.Fields, v) + case *thrift.ThriftStruct: + newField := &thrift.ThriftField{ + Name: c.applySnakeCaseNamingOption(v.Name + "_field"), + Type: v.Name, + } + c.addMessageToThrift(v) + anyOfStruct.Fields = append(anyOfStruct.Fields, newField) + case *thrift.ThriftEnum: + newField := &thrift.ThriftField{ + Name: c.applySnakeCaseNamingOption(v.Name + "_field"), + Type: v.Name, + } + c.addEnumToThrift(v) + anyOfStruct.Fields = append(anyOfStruct.Fields, newField) + case *thrift.ThriftUnion: + newField := &thrift.ThriftField{ + Name: c.applySnakeCaseNamingOption(v.Name + "_field"), + Type: v.Name, + } + c.addUnionToThrift(v) + anyOfStruct.Fields = append(anyOfStruct.Fields, newField) + } + } + + return anyOfStruct, nil +} + +// applyPascalseCaseNamingOption applies naming convention based on the converter's naming option +func (c *ThriftConverter) applyPascalseCaseNamingOption(name string) string { + if c.converterOption.NamingOption { + return common.ToPascaleCase(name) + } + return name +} + +// applySnakeCaseNamingOption applies naming convention based on the converter's naming option +func (c *ThriftConverter) applySnakeCaseNamingOption(name string) string { + if c.converterOption.NamingOption { + return common.ToSnakeCase(name) + } + return name +} + +// Add a new method to handle structured extensions +func (c *ThriftConverter) addExtensionsToProtoOptions() error { + // Check for x-option in spec extensions + if xOption, ok := c.spec.Extensions["x-options"]; ok { + if optionMap, ok := xOption.(map[string]interface{}); ok { + for key, value := range optionMap { + c.ThriftFile.Namespace[key] = fmt.Sprintf("%q", value) + } + } + } + + // Check for x-option in spec.info.extensions + if c.spec.Info != nil { + if xOption, ok := c.spec.Info.Extensions["x-options"]; ok { + if optionMap, ok := xOption.(map[string]interface{}); ok { + for key, value := range optionMap { + c.ThriftFile.Namespace[key] = fmt.Sprintf("%q", value) + } + } + } + } + + return nil +} + +// addMessageToThrift adds a ThriftStruct to the ThriftFile globally +func (c *ThriftConverter) addMessageToThrift(message *thrift.ThriftStruct) error { + if message == nil { + return errors.New("message is nil") + } + + // Check if the message already exists in the ThriftFile + for _, existingMessage := range c.ThriftFile.Structs { + if existingMessage.Name == message.Name { + // Merge fields if the message already exists + fieldNames := make(map[string]struct{}) + for _, field := range existingMessage.Fields { + fieldNames[field.Name] = struct{}{} + } + for _, newField := range message.Fields { + if _, exists := fieldNames[newField.Name]; !exists { + existingMessage.Fields = append(existingMessage.Fields, newField) + } + } + return nil + } + } + + // Add the message globally + c.ThriftFile.Structs = append(c.ThriftFile.Structs, message) + return nil +} + +// addEnumToThrift adds an enum to the ThriftFile +func (c *ThriftConverter) addEnumToThrift(enum *thrift.ThriftEnum) { + c.ThriftFile.Enums = append(c.ThriftFile.Enums, enum) +} + +// addUnionToThrift adds a union to the ThriftFile +func (c *ThriftConverter) addUnionToThrift(union *thrift.ThriftUnion) { + c.ThriftFile.Unions = append(c.ThriftFile.Unions, union) +} + +// AddThriftInclude adds an include to the ThriftFile +func (c *ThriftConverter) AddThriftInclude(includeFile string) { + if c.ThriftFile != nil { + for _, existingInclude := range c.ThriftFile.Includes { + if existingInclude == includeFile { + return + } + } + c.ThriftFile.Includes = append(c.ThriftFile.Includes, includeFile) + } +} + +// addOptionsToThrift adds options to the Thrift file +func (c *ThriftConverter) addOptionsToThrift() { + if len(c.ThriftFile.Services) > 0 { + if c.converterOption.OpenapiOption { + optionStr := common.StructToOption(c.spec, "") + + schemaOption := &thrift.Option{ + Name: consts.OpenapiDocument, + Value: optionStr, + } + c.ThriftFile.Services[0].Options = append(c.ThriftFile.Services[0].Options, schemaOption) + c.AddThriftInclude(consts.OpenapiThriftFile) + } + } +} + +// addFieldIfNotExists adds a field to Fields if it does not already exist +func (c *ThriftConverter) addFieldIfNotExists(fields *[]*thrift.ThriftField, field *thrift.ThriftField) { + for _, existingField := range *fields { + if existingField.Name == field.Name { + return + } + } + *fields = append(*fields, field) +} + +// methodExistsInService checks if a method exists in a service +func (c *ThriftConverter) methodExistsInService(service *thrift.ThriftService, methodName string) bool { + for _, method := range service.Methods { + if method.Name == methodName { + return true + } + } + return false +} + +// findOrCreateService finds or creates a service +func (c *ThriftConverter) findOrCreateService(serviceName string) *thrift.ThriftService { + for i := range c.ThriftFile.Services { + if c.ThriftFile.Services[i].Name == serviceName { + return c.ThriftFile.Services[i] + } + } + + // If no existing service is found, create a new one + newService := &thrift.ThriftService{Name: serviceName} + c.ThriftFile.Services = append(c.ThriftFile.Services, newService) + return newService +} diff --git a/swagger2idl/example/openapi.yaml b/swagger2idl/example/openapi.yaml new file mode 100644 index 0000000..65019ca --- /dev/null +++ b/swagger2idl/example/openapi.yaml @@ -0,0 +1,224 @@ +# Generated with protoc-gen-http-swagger +# https://github.com/hertz-contrib/swagger-generate/protoc-gen-http-swagger + +openapi: 3.0.3 +info: + title: example swagger doc + version: Version from annotation +servers: + - url: http://127.0.0.1:8888 + - url: http://127.0.0.1:8889 +paths: + /body: + post: + tags: + - HelloService1 + operationId: HelloService1_BodyMethod + parameters: + - name: query2 + in: query + description: 'field: query描述' + schema: + type: string + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/BodyReqBody' + responses: + "200": + description: HelloResp描述 + headers: + token: + schema: + type: string + content: + application/json: + schema: + $ref: '#/components/schemas/HelloRespBody' + servers: + - url: http://127.0.0.1:8888 + /form: + post: + tags: + - HelloService1 + operationId: HelloService1_FormMethod + requestBody: + content: + multipart/form-data: + schema: + $ref: '#/components/schemas/FormReqForm' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/FormReqForm' + responses: + "200": + description: HelloResp描述 + headers: + token: + schema: + type: string + content: + application/json: + schema: + $ref: '#/components/schemas/HelloRespBody' + servers: + - url: http://127.0.0.1:8888 + /hello1: + get: + tags: + - HelloService1 + operationId: HelloService1_QueryMethod1 + parameters: + - name: query1 + in: query + schema: + type: object + additionalProperties: + type: string + - name: items + in: query + schema: + type: array + items: + type: string + - name: query2 + in: query + description: QueryValue描述 + required: true + schema: + title: Name + maxLength: 50 + minLength: 1 + type: string + description: Name + responses: + "200": + description: HelloResp描述 + headers: + token: + schema: + type: string + content: + application/json: + schema: + $ref: '#/components/schemas/HelloRespBody' + servers: + - url: http://127.0.0.1:8888 + /hello2: + get: + tags: + - HelloService2 + summary: Hello - Get + description: Hello - Get + operationId: HelloService2_QueryMethod2 + parameters: + - name: query1 + in: query + schema: + type: object + additionalProperties: + type: string + - name: items + in: query + schema: + type: array + items: + type: string + - name: query2 + in: query + description: QueryValue描述 + required: true + schema: + title: Name + maxLength: 50 + minLength: 1 + type: string + description: Name + responses: + "200": + description: HelloResp描述 + headers: + token: + schema: + type: string + content: + application/json: + schema: + $ref: '#/components/schemas/HelloRespBody' + servers: + - url: http://127.0.0.1:8889 + /path{path1}: + get: + tags: + - HelloService1 + operationId: HelloService1_PathMethod + parameters: + - name: path1 + in: path + description: 'field: path描述' + required: true + schema: + type: string + responses: + "200": + description: HelloResp描述 + headers: + token: + schema: + type: string + content: + application/json: + schema: + $ref: '#/components/schemas/HelloRespBody' + servers: + - url: http://127.0.0.1:8888 +components: + schemas: + BodyReqBody: + type: object + properties: + body: + type: string + description: 'field: body描述' + body1: + type: string + description: 'field: body1描述' + FormReqForm: + title: Hello - request + required: + - form1 + type: object + properties: + form1: + title: this is an override field schema title + maxLength: 255 + type: string + form2: + $ref: '#/components/schemas/FormReq_InnerForm' + description: Hello - request + FormReq_InnerForm: + type: object + properties: + form3: + type: string + description: 内嵌message描述 + HelloRespBody: + title: Hello - response + required: + - body + type: object + properties: + body: + title: response content + maxLength: 80 + minLength: 1 + type: string + description: response content + description: Hello - response +tags: + - name: HelloService1 + description: HelloService1描述 + - name: HelloService2 +x-options: + go_package: example \ No newline at end of file diff --git a/swagger2idl/generate/generate.go b/swagger2idl/generate/generate.go new file mode 100644 index 0000000..748bf27 --- /dev/null +++ b/swagger2idl/generate/generate.go @@ -0,0 +1,22 @@ +/* + * Copyright 2024 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package generate + +// Generator is an interface for generating files +type Generator interface { + Generate(fileContent interface{}) (string, error) +} diff --git a/swagger2idl/generate/proto_generate.go b/swagger2idl/generate/proto_generate.go new file mode 100644 index 0000000..2feac1a --- /dev/null +++ b/swagger2idl/generate/proto_generate.go @@ -0,0 +1,321 @@ +/* + * Copyright 2024 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package generate + +import ( + "fmt" + "sort" + "strconv" + "strings" + + common "github.com/hertz-contrib/swagger-generate/common/utils" + "github.com/hertz-contrib/swagger-generate/swagger2idl/protobuf" +) + +// ProtoGenerate is used to handle the encoding context +type ProtoGenerate struct { + dst *strings.Builder // The target for output +} + +// NewProtoGenerate creates a new ProtoGenerate instance +func NewProtoGenerate() *ProtoGenerate { + return &ProtoGenerate{dst: &strings.Builder{}} +} + +// Generate converts the ProtoFile structure into Proto file content +func (e *ProtoGenerate) Generate(fileContent interface{}) (string, error) { + protoFile, ok := fileContent.(*protobuf.ProtoFile) + if !ok { + return "", fmt.Errorf("invalid type: expected *protobuf.ProtoFile") + } + + e.dst.WriteString("syntax = \"proto3\";\n\n") + e.dst.WriteString(fmt.Sprintf("package %s;\n\n", protoFile.PackageName)) + + // Generate imports + if len(protoFile.Imports) > 0 { + for _, importFile := range protoFile.Imports { + e.dst.WriteString(fmt.Sprintf("import \"%s\";\n", importFile)) + } + e.dst.WriteString("\n") + } + + // Generate file-level options + if len(protoFile.Options) > 0 { + for _, value := range protoFile.Options { + e.dst.WriteString(fmt.Sprintf("option %s = %s;\n", value.Name, value.Value)) + } + e.dst.WriteString("\n") + } + + // Sort enums by name + sort.Slice(protoFile.Enums, func(i, j int) bool { + return protoFile.Enums[i].Name < protoFile.Enums[j].Name + }) + + // Generate enums + for _, enum := range protoFile.Enums { + if err := e.encodeEnum(enum, 0); err != nil { + return "", fmt.Errorf("failed to encode enum %s: %w", enum.Name, err) + } + } + + // Sort messages by name + sort.Slice(protoFile.Messages, func(i, j int) bool { + return protoFile.Messages[i].Name < protoFile.Messages[j].Name + }) + + if len(protoFile.Messages) > 0 { + for _, message := range protoFile.Messages { + if err := e.encodeMessage(message, 0); err != nil { + return "", fmt.Errorf("failed to encode message %s: %w", message.Name, err) + } + } + } + + // Sort services by name + sort.Slice(protoFile.Services, func(i, j int) bool { + return protoFile.Services[i].Name < protoFile.Services[j].Name + }) + + // Generate services + for _, service := range protoFile.Services { + if err := e.encodeService(service); err != nil { + return "", fmt.Errorf("failed to encode service %s: %w", service.Name, err) + } + } + + return e.dst.String(), nil +} + +// encodeService encodes service types +func (e *ProtoGenerate) encodeService(service *protobuf.ProtoService) error { + if service.Description != "" { + e.dst.WriteString(fmt.Sprintf("// %s\n", service.Description)) + } + e.dst.WriteString(fmt.Sprintf("service %s {\n", service.Name)) + + // Sort methods by name + sort.Slice(service.Methods, func(i, j int) bool { + return service.Methods[i].Name < service.Methods[j].Name + }) + + for _, method := range service.Methods { + if method.Description != "" { + e.dst.WriteString(fmt.Sprintf(" // %s\n", method.Description)) + } + e.dst.WriteString(fmt.Sprintf(" rpc %s(%s) returns (%s)", method.Name, method.Input, method.Output)) + if len(method.Options) > 0 { + sort.Slice(method.Options, func(i, j int) bool { + return method.Options[i].Name < method.Options[j].Name + }) + e.dst.WriteString(" {\n") + for _, option := range method.Options { + e.dst.WriteString(" option ") + if err := e.encodeFieldOption(option); err != nil { + return fmt.Errorf("failed to encode option for method %s: %w", method.Name, err) + } + e.dst.WriteString(";\n") + } + e.dst.WriteString(" }\n") + } else { + e.dst.WriteString(";\n") + } + } + e.dst.WriteString("}\n\n") + return nil +} + +// encodeMessage recursively encodes messages, including nested messages, enums, and oneofs +func (e *ProtoGenerate) encodeMessage(message *protobuf.ProtoMessage, indentLevel int) error { + if indentLevel > 0 { + e.dst.WriteString("\n") + } + indent := strings.Repeat(" ", indentLevel) + if message.Description != "" { + e.dst.WriteString(fmt.Sprintf("%s// %s\n", indent, message.Description)) + } + e.dst.WriteString(fmt.Sprintf("%smessage %s {\n", indent, message.Name)) + + // Generate message-level options + if len(message.Options) > 0 { + sort.Slice(message.Options, func(i, j int) bool { + return message.Options[i].Name < message.Options[j].Name + }) + e.dst.WriteString(fmt.Sprintf("%s option", indent)) + for _, option := range message.Options { + if err := e.encodeFieldOption(option); err != nil { + return fmt.Errorf("failed to encode option for message %s: %w", message.Name, err) + } + e.dst.WriteString(";\n") + } + } + + // Sort fields by name + sort.Slice(message.Fields, func(i, j int) bool { + return message.Fields[i].Name < message.Fields[j].Name + }) + + // Generate fields + for i, field := range message.Fields { + err := e.encodeField(field, i+1, indentLevel) + if err != nil { + return fmt.Errorf("failed to encode field %s: %w", field.Name, err) + } + } + + // Generate oneofs + for _, oneOf := range message.OneOfs { + err := e.encodeOneOf(oneOf, indentLevel+1, len(message.Fields)+1) + if err != nil { + return fmt.Errorf("failed to encode oneof %s: %w", oneOf.Name, err) + } + } + + // Generate nested enums + if len(message.Enums) > 0 { + e.dst.WriteString("\n") + for _, nestedEnum := range message.Enums { + if err := e.encodeEnum(nestedEnum, indentLevel+1); err != nil { + return fmt.Errorf("failed to encode nested enum %s: %w", nestedEnum.Name, err) + } + } + } + + // Generate nested messages + for _, nestedMessage := range message.Messages { + if err := e.encodeMessage(nestedMessage, indentLevel+1); err != nil { + return fmt.Errorf("failed to encode nested message %s: %w", nestedMessage.Name, err) + } + } + + e.dst.WriteString(fmt.Sprintf("%s}\n\n", indent)) + return nil +} + +// encodeEnum encodes enum types +func (e *ProtoGenerate) encodeEnum(enum *protobuf.ProtoEnum, indentLevel int) error { + indent := strings.Repeat(" ", indentLevel) + e.dst.WriteString(fmt.Sprintf("%senum %s {\n", indent, enum.Name)) + + // Generate enum values + for _, value := range enum.Values { + valueStr := fmt.Sprintf("%v", value.Value) + enumValueName := valueStr + if _, err := strconv.Atoi(valueStr); err == nil { + enumValueName = fmt.Sprintf("%s%s", enum.Name, valueStr) + } + enumValueName = strings.ToUpper(common.FormatStr(enumValueName)) + e.dst.WriteString(fmt.Sprintf("%s %s = %d;\n", indent, enumValueName, value.Index)) + } + + e.dst.WriteString(fmt.Sprintf("%s}\n\n", indent)) + return nil +} + +// encodeField encodes a single field in the message. +func (e *ProtoGenerate) encodeField(field *protobuf.ProtoField, fieldNumber, indentLevel int) error { + indent := strings.Repeat(" ", indentLevel) + + // Add field description if present + if field.Description != "" { + e.dst.WriteString(fmt.Sprintf("%s // %s\n", indent, field.Description)) + } + + // Determine if the field is repeated + repeated := "" + if field.Repeated { + repeated = "repeated " + } + + // Write the field definition + e.dst.WriteString(fmt.Sprintf("%s %s%s %s = %d", indent, repeated, field.Type, common.FormatStr(field.Name), fieldNumber)) + + // Generate field-level options if present + if len(field.Options) > 0 { + sort.Slice(field.Options, func(i, j int) bool { + return field.Options[i].Name < field.Options[j].Name + }) + e.dst.WriteString(" [\n ") + for j, option := range field.Options { + if err := e.encodeFieldOption(option); err != nil { + return fmt.Errorf("failed to encode option for field %s: %w", field.Name, err) + } + if j < len(field.Options)-1 { + e.dst.WriteString(",\n ") + } + } + e.dst.WriteString("\n ]") + } + + e.dst.WriteString(";\n") + return nil +} + +// encodeOneOf encodes oneof types +func (e *ProtoGenerate) encodeOneOf(oneOf *protobuf.ProtoOneOf, indentLevel, fieldNumber int) error { + indent := strings.Repeat(" ", indentLevel) + e.dst.WriteString(fmt.Sprintf("%soneof %s {\n", indent, oneOf.Name)) + // Generate oneof fields + for _, field := range oneOf.Fields { + e.dst.WriteString(fmt.Sprintf("%s %s %s = %d;\n", indent, field.Type, field.Name, fieldNumber)) + fieldNumber++ + } + + e.dst.WriteString(fmt.Sprintf("%s}\n", indent)) + return nil +} + +// encodeFieldOption encodes an option for a single field +func (e *ProtoGenerate) encodeFieldOption(opt *protobuf.Option) error { + // Output the option name + fmt.Fprintf(e.dst, "(%s) = ", opt.Name) // Add indentation for consistency + + // Check if the option value is a complex structure + switch value := opt.Value.(type) { + case map[string]interface{}: + // If it's a map type, it needs to output as a nested structure + fmt.Fprintf(e.dst, "{\n") // Newline after { + e.encodeFieldOptionMap(value, 6) // Output map content, passing the current indentation level + fmt.Fprintf(e.dst, " }") // Indent and output the closing }, with the appropriate indentation level + default: + fmt.Fprintf(e.dst, "%s", value) // For simple types, output directly + } + + return nil +} + +// encodeFieldOptionMap encodes a complex map type option value +func (e *ProtoGenerate) encodeFieldOptionMap(optionMap map[string]interface{}, indent int) error { + keys := make([]string, 0, len(optionMap)) + for k := range optionMap { + keys = append(keys, k) + } + sort.Strings(keys) // Sort keys to ensure consistent output order + + indentSpace := strings.Repeat(" ", indent) // Dynamically generate indent spaces + + for _, key := range keys { + value := optionMap[key] + // Output key-value pairs with appropriate indentation + fmt.Fprintf(e.dst, "%s%s: %s", indentSpace, key, common.Stringify(value)) // Add deeper indentation + // Don't add a semicolon after the last item, maintain correct format + fmt.Fprintf(e.dst, ";\n") + } + + return nil +} diff --git a/swagger2idl/generate/thrift_generate.go b/swagger2idl/generate/thrift_generate.go new file mode 100644 index 0000000..bfaa0d6 --- /dev/null +++ b/swagger2idl/generate/thrift_generate.go @@ -0,0 +1,293 @@ +/* + * Copyright 2024 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package generate + +import ( + "fmt" + "sort" + "strconv" + "strings" + + common "github.com/hertz-contrib/swagger-generate/common/utils" + "github.com/hertz-contrib/swagger-generate/swagger2idl/thrift" +) + +// ThriftGenerate handles the encoding context for Thrift files +type ThriftGenerate struct { + dst *strings.Builder // Output destination +} + +// NewThriftGenerate creates a new instance of ThriftGenerate +func NewThriftGenerate() *ThriftGenerate { + return &ThriftGenerate{dst: &strings.Builder{}} +} + +// Generate converts a ThriftFile structure into Thrift file content +func (e *ThriftGenerate) Generate(fileContent interface{}) (string, error) { + thriftFile, ok := fileContent.(*thrift.ThriftFile) + if !ok { + return "", fmt.Errorf("invalid type: expected *ThriftFile") + } + + if len(thriftFile.Namespace) == 0 { + e.dst.WriteString("namespace go example\n\n") + } else { + for language, ns := range thriftFile.Namespace { + e.dst.WriteString(fmt.Sprintf("namespace %s %s\n", language, ns)) + } + e.dst.WriteString("\n") + } + + // Generate includes + if len(thriftFile.Includes) > 0 { + for _, include := range thriftFile.Includes { + e.dst.WriteString(fmt.Sprintf("include \"%s\"\n", include)) + } + + e.dst.WriteString("\n") + } + + // sort the enums + sort.Slice(thriftFile.Enums, func(i, j int) bool { + return thriftFile.Enums[i].Name < thriftFile.Enums[j].Name + }) + + // Generate enums + for _, enum := range thriftFile.Enums { + e.encodeEnum(enum, 0) + } + + // sort the structs + sort.Slice(thriftFile.Structs, func(i, j int) bool { + return thriftFile.Structs[i].Name < thriftFile.Structs[j].Name + }) + + // Generate structs + for _, message := range thriftFile.Structs { + e.encodeMessage(message, 0) + } + + // sort the unions + sort.Slice(thriftFile.Unions, func(i, j int) bool { + return thriftFile.Unions[i].Name < thriftFile.Unions[j].Name + }) + + // Generate unions + for _, union := range thriftFile.Unions { + e.encodeUnion(union, 0) + } + + // sort the services + sort.Slice(thriftFile.Services, func(i, j int) bool { + return thriftFile.Services[i].Name < thriftFile.Services[j].Name + }) + + // Generate services + for _, service := range thriftFile.Services { + e.encodeService(service) + } + + return e.dst.String(), nil +} + +// encodeService encodes service definitions +func (e *ThriftGenerate) encodeService(service *thrift.ThriftService) { + if service.Description != "" { + e.dst.WriteString(fmt.Sprintf("// %s\n", service.Description)) + } + + e.dst.WriteString(fmt.Sprintf("service %s {\n", service.Name)) // Service declaration + + // Methods + for _, method := range service.Methods { + e.encodeMethod(method) + } + + e.dst.WriteString("}") + + // Service options + if len(service.Options) > 0 { + e.dst.WriteString("(") + for i, option := range service.Options { + if i > 0 { + e.dst.WriteString(", ") + } + e.encodeOption(option) + } + e.dst.WriteString(")\n") + } else { + e.dst.WriteString("\n") + } + + e.dst.WriteString("\n") +} + +// encodeMethod encodes methods within a service +func (e *ThriftGenerate) encodeMethod(method *thrift.ThriftMethod) { + if method.Description != "" { + e.dst.WriteString(fmt.Sprintf(" // %s\n", method.Description)) + } + + e.dst.WriteString(fmt.Sprintf(" %s %s (", method.Output, method.Name)) // Method signature + + // Input parameters + for i, input := range method.Input { + if i > 0 { + e.dst.WriteString(", ") + } + e.dst.WriteString(fmt.Sprintf("%d: %s req", i+1, input)) + } + + e.dst.WriteString(")") + + // Method options + if len(method.Options) > 0 { + e.dst.WriteString(" (\n") + for i, option := range method.Options { + if i > 0 { + e.dst.WriteString(",\n") + } + e.dst.WriteString(" ") + e.encodeOption(option) + } + e.dst.WriteString("\n )") + } + + e.dst.WriteString("\n") +} + +// encodeMessage recursively encodes structs, including nested structs and enums +func (e *ThriftGenerate) encodeMessage(message *thrift.ThriftStruct, indentLevel int) { + indent := strings.Repeat(" ", indentLevel) + + if message.Description != "" { + e.dst.WriteString(fmt.Sprintf("%s// %s\n", indent, message.Description)) + } + + e.dst.WriteString(fmt.Sprintf("%sstruct %s {\n", indent, message.Name)) + + // Fields: Traverse the fields and assign indexes + for i, field := range message.Fields { + e.encodeField(field, i+1, indentLevel+1) // Use 1-based indexing with `i+1` + } + + e.dst.WriteString(fmt.Sprintf("%s}", indent)) + + // Struct options + if len(message.Options) > 0 { + e.dst.WriteString(indent + "(\n") + for i, option := range message.Options { + if i > 0 { + e.dst.WriteString(",\n") + } + e.dst.WriteString(indent + " ") // Increase indentation + e.encodeOption(option) + } + e.dst.WriteString("\n" + indent + ")\n") + } else { + e.dst.WriteString("\n") + } + + e.dst.WriteString("\n") +} + +// encodeUnion encodes a Thrift union +func (e *ThriftGenerate) encodeUnion(union *thrift.ThriftUnion, indentLevel int) { + indent := strings.Repeat(" ", indentLevel) + + e.dst.WriteString(fmt.Sprintf("%sunion %s {\n", indent, union.Name)) + + // Traverse union fields + for i, field := range union.Fields { + e.encodeField(field, i+1, indentLevel+1) // Use 1-based indexing with `i+1` + } + + e.dst.WriteString(fmt.Sprintf("%s}\n\n", indent)) +} + +// encodeEnum encodes enum types +func (e *ThriftGenerate) encodeEnum(enum *thrift.ThriftEnum, indentLevel int) { + indent := strings.Repeat(" ", indentLevel) + + e.dst.WriteString(fmt.Sprintf("%senum %s {\n", indent, enum.Name)) + + for _, value := range enum.Values { + valueStr := fmt.Sprintf("%v", value.Value) // Convert the value to a string + + // Check if the value is numeric and generate a name if needed + enumValueName := valueStr + if _, err := strconv.Atoi(valueStr); err == nil { + enumValueName = fmt.Sprintf("%s%s", enum.Name, valueStr) + } + + enumValueName = strings.ToUpper(common.FormatStr(enumValueName)) + + e.dst.WriteString(fmt.Sprintf("%s %s = %d;\n", indent, enumValueName, value.Index)) + } + + e.dst.WriteString(fmt.Sprintf("%s}\n\n", indent)) +} + +// encodeField encodes a single field within a struct +func (e *ThriftGenerate) encodeField(field *thrift.ThriftField, index, indentLevel int) { + indent := strings.Repeat(" ", indentLevel) + + if field.Description != "" { + e.dst.WriteString(fmt.Sprintf("%s// %s\n", indent, field.Description)) + } + + // Field index and type + fieldType := field.Type + if field.Repeated { + fieldType = fmt.Sprintf("list<%s>", field.Type) + } + + // Handle optional fields + optionalFlag := "" + if field.Optional { + optionalFlag = "optional " + } + + // Assign the provided index to the field + e.dst.WriteString(fmt.Sprintf("%s%d: %s%s %s", indent, index, optionalFlag, fieldType, common.FormatStr(field.Name))) + + // Field options + if len(field.Options) > 0 { + e.dst.WriteString(" (") + for i, option := range field.Options { + if i > 0 { + e.dst.WriteString(",\n" + indent) + } + e.encodeOption(option) + } + e.dst.WriteString(")\n") // Close parentheses aligned with the field + } else { + e.dst.WriteString("\n") + } +} + +// encodeOption handles the encoding of options for methods, structs, and fields +func (e *ThriftGenerate) encodeOption(option *thrift.Option) { + // If the option key starts with "api", use double quotes for the value + if strings.HasPrefix(option.Name, "api.") { + e.dst.WriteString(fmt.Sprintf("%s = %s", option.Name, option.Value)) + } else if strings.HasPrefix(option.Name, "openapi.") { + e.dst.WriteString(fmt.Sprintf("%s = '%s'", option.Name, option.Value)) + } else { + e.dst.WriteString(fmt.Sprintf("%s = %s", option.Name, option.Value)) + } +} diff --git a/swagger2idl/go.mod b/swagger2idl/go.mod new file mode 100644 index 0000000..bff41fd --- /dev/null +++ b/swagger2idl/go.mod @@ -0,0 +1,26 @@ +module github.com/hertz-contrib/swagger-generate/swagger2idl + +go 1.18 + +require ( + github.com/getkin/kin-openapi v0.127.0 + github.com/hertz-contrib/swagger-generate v0.0.0-00010101000000-000000000000 + github.com/urfave/cli/v2 v2.27.4 +) + +require ( + github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect + github.com/go-openapi/jsonpointer v0.21.0 // indirect + github.com/go-openapi/swag v0.23.0 // indirect + github.com/iancoleman/strcase v0.3.0 // indirect + github.com/invopop/yaml v0.3.1 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/mailru/easyjson v0.7.7 // indirect + github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect + github.com/perimeterx/marshmallow v1.1.5 // indirect + github.com/russross/blackfriday/v2 v2.1.0 // indirect + github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) + +replace github.com/hertz-contrib/swagger-generate => ../../swagger-generate diff --git a/swagger2idl/go.sum b/swagger2idl/go.sum new file mode 100644 index 0000000..ca53ed7 --- /dev/null +++ b/swagger2idl/go.sum @@ -0,0 +1,38 @@ +github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4= +github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/getkin/kin-openapi v0.127.0 h1:Mghqi3Dhryf3F8vR370nN67pAERW+3a95vomb3MAREY= +github.com/getkin/kin-openapi v0.127.0/go.mod h1:OZrfXzUfGrNbsKj+xmFBx6E5c6yH3At/tAKSc2UszXM= +github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= +github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= +github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= +github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= +github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM= +github.com/iancoleman/strcase v0.3.0 h1:nTXanmYxhfFAMjZL34Ov6gkzEsSJZ5DbhxWjvSASxEI= +github.com/iancoleman/strcase v0.3.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= +github.com/invopop/yaml v0.3.1 h1:f0+ZpmhfBSS4MhG+4HYseMdJhoeeopbSKbq5Rpeelso= +github.com/invopop/yaml v0.3.1/go.mod h1:PMOp3nn4/12yEZUFfmOuNHJsZToEEOwoWsT+D81KkeA= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw= +github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= +github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s= +github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0= +github.com/urfave/cli/v2 v2.27.4 h1:o1owoI+02Eb+K107p27wEX9Bb8eqIoZCfLXloLUSWJ8= +github.com/urfave/cli/v2 v2.27.4/go.mod h1:m4QzxcD2qpra4z7WhzEGn74WZLViBnMpb1ToCAKdGRQ= +github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4= +github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/swagger2idl/main.go b/swagger2idl/main.go new file mode 100644 index 0000000..5b60b9d --- /dev/null +++ b/swagger2idl/main.go @@ -0,0 +1,177 @@ +/* + * Copyright 2024 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package main + +import ( + "log" + "os" + "path/filepath" + + "github.com/hertz-contrib/swagger-generate/common/consts" + "github.com/hertz-contrib/swagger-generate/swagger2idl/converter" + "github.com/hertz-contrib/swagger-generate/swagger2idl/generate" + "github.com/hertz-contrib/swagger-generate/swagger2idl/parser" + "github.com/urfave/cli/v2" +) + +var ( + outputType string + outputFile string + openapiOption bool + apiOption bool + namingOption bool +) + +func main() { + app := &cli.App{ + Name: "swagger2idl", + Usage: "Convert OpenAPI specs to Protobuf or Thrift IDL", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "type", + Aliases: []string{"t"}, + Usage: "Specify output type: 'proto' or 'thrift'. If not provided, inferred from output file extension.", + Destination: &outputType, + }, + &cli.StringFlag{ + Name: "output", + Aliases: []string{"o"}, + Usage: "Specify output file path. If not provided, defaults to output.proto or output.thrift based on the output type.", + Destination: &outputFile, + }, + &cli.BoolFlag{ + Name: "openapi", + Aliases: []string{"oa"}, + Usage: "Include OpenAPI specific options in the output", + Destination: &openapiOption, + }, + &cli.BoolFlag{ + Name: "api", + Aliases: []string{"a"}, + Usage: "Include API specific options in the output", + Destination: &apiOption, + }, + &cli.BoolFlag{ + Name: "naming", + Aliases: []string{"n"}, + Usage: "use naming conventions for the output IDL file", + Value: true, + Destination: &namingOption, + }, + }, + Action: func(c *cli.Context) error { + args := c.Args().Slice() + + if len(args) < 1 { + log.Fatal("Please provide the path to the OpenAPI file.") + } + + openapiFile := args[0] + + // Automatically determine output type based on file extension if not provided + if outputType == "" && outputFile != "" { + ext := filepath.Ext(outputFile) + switch ext { + case ".proto": + outputType = consts.IDLProto + case ".thrift": + outputType = consts.IDLThrift + default: + log.Fatalf("Cannot determine output type from file extension: %s. Use --type to specify explicitly.", ext) + } + } + + if outputFile == "" { + if outputType == consts.IDLProto { + outputFile = consts.DefaultProtoFilename + } else if outputType == consts.IDLThrift { + outputFile = consts.DefaultThriftFilename + } else { + log.Fatal("Output file must be specified if output type is not provided.") + } + } + + spec, err := parser.LoadOpenAPISpec(openapiFile) + if err != nil { + log.Fatalf("Failed to load OpenAPI file: %v", err) + } + + converterOption := &converter.ConvertOption{ + OpenapiOption: openapiOption, + ApiOption: apiOption, + NamingOption: namingOption, + } + + var idlContent string + var file *os.File + var errFile error + + switch outputType { + case consts.IDLProto: + protoConv := converter.NewProtoConverter(spec, converterOption) + + if err = protoConv.Convert(); err != nil { + log.Fatalf("Error during conversion: %v", err) + } + protoEngine := generate.NewProtoGenerate() + + idlContent, err = protoEngine.Generate(protoConv.GetIdl()) + if err != nil { + log.Fatalf("Error generating proto docs: %v", err) + } + + file, errFile = os.Create(outputFile) + case consts.IDLThrift: + thriftConv := converter.NewThriftConverter(spec, converterOption) + + if err = thriftConv.Convert(); err != nil { + log.Fatalf("Error during conversion: %v", err) + } + thriftEngine := generate.NewThriftGenerate() + + idlContent, err = thriftEngine.Generate(thriftConv.GetIdl()) + if err != nil { + log.Fatalf("Error generating thrift docs: %v", err) + } + + file, errFile = os.Create(outputFile) + default: + log.Fatalf("Invalid output type: %s", outputType) + } + + if errFile != nil { + log.Fatalf("Failed to create file: %v", errFile) + } + defer func() { + if err := file.Close(); err != nil { + log.Printf("Error closing file: %v", err) + } + }() + + if _, err = file.WriteString(idlContent); err != nil { + log.Fatalf("Error writing to file: %v", err) + } + + return nil + }, + } + + err := app.Run(os.Args) + if err != nil { + log.Fatal(err) + } +} diff --git a/swagger2idl/parser/parser.go b/swagger2idl/parser/parser.go new file mode 100644 index 0000000..7712ca4 --- /dev/null +++ b/swagger2idl/parser/parser.go @@ -0,0 +1,46 @@ +/* + * Copyright 2024 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package parser + +import ( + "fmt" + "os" + + "github.com/getkin/kin-openapi/openapi3" +) + +// LoadOpenAPISpec parses an OpenAPI spec from a file and returns it. +func LoadOpenAPISpec(filePath string) (*openapi3.T, error) { + loader := openapi3.NewLoader() + var err error + var spec *openapi3.T + + if _, err = os.Stat(filePath); os.IsNotExist(err) { + return nil, fmt.Errorf("file %s does not exist", filePath) + } + + spec, err = loader.LoadFromFile(filePath) + if err != nil { + return nil, fmt.Errorf("failed to load OpenAPI spec: %v", err) + } + + if err = spec.Validate(loader.Context); err != nil { + return nil, fmt.Errorf("failed to validate OpenAPI spec: %v", err) + } + + return spec, nil +} diff --git a/swagger2idl/protobuf/protobuf.go b/swagger2idl/protobuf/protobuf.go new file mode 100644 index 0000000..eb52a14 --- /dev/null +++ b/swagger2idl/protobuf/protobuf.go @@ -0,0 +1,91 @@ +/* + * Copyright 2024 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package protobuf + +// ProtoFile represents a complete Proto file +type ProtoFile struct { + PackageName string // The package name of the Proto file + Messages []*ProtoMessage // List of Proto messages + Services []*ProtoService // List of Proto services + Enums []*ProtoEnum // List of Proto enums + Imports []string // List of imported Proto files + Options []*Option // File-level options +} + +// ProtoService represents a Proto service +type ProtoService struct { + Name string // Name of the service + Description string // Description for the service + Methods []*ProtoMethod // List of methods in the service + Options []*Option // Service-level options +} + +// ProtoMethod represents a method in a Proto service +type ProtoMethod struct { + Name string // Name of the method + Description string // Description for the method + Input string // Input message type + Output string // Output message type + Options []*Option // Options for the method +} + +// ProtoMessage represents a Proto message +type ProtoMessage struct { + Name string + Description string // Description for the Proto message + Fields []*ProtoField // List of fields in the Proto message + Messages []*ProtoMessage // Nested Proto messages + Enums []*ProtoEnum // Enums within the Proto message + OneOfs []*ProtoOneOf // OneOfs within the Proto message + Options []*Option // Options specific to this Proto message +} + +// ProtoField represents a field in a Proto message +type ProtoField struct { + Name string // Name of the field + Type string // Type of the field + Description string // Description for the field + Repeated bool // Indicates if the field is repeated (array) + Options []*Option // Additional options for this field +} + +// Option represents an option in a Proto field or message +type Option struct { + Name string // Name of the option + Value interface{} // Value of the option +} + +// ProtoEnum represents a Proto enum +type ProtoEnum struct { + Name string // Name of the enum + Description string // Description for the enum + Values []*ProtoEnumValue // Values within the enum + Options []*Option // Enum-level options +} + +// ProtoEnumValue represents a value in a Proto enum +type ProtoEnumValue struct { + Index int // index of the enum value + Value any // Corresponding integer value for the enum +} + +// ProtoOneOf represents a oneof in a Proto message +type ProtoOneOf struct { + Name string // Name of the oneof + Fields []*ProtoField // List of fields in the oneof + Options []*Option // Options specific to this oneof +} diff --git a/swagger2idl/thrift/thrift.go b/swagger2idl/thrift/thrift.go new file mode 100644 index 0000000..45947d4 --- /dev/null +++ b/swagger2idl/thrift/thrift.go @@ -0,0 +1,90 @@ +/* + * Copyright 2024 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package thrift + +// ThriftFile represents a complete Thrift file +type ThriftFile struct { + Namespace map[string]string // Namespace for the Thrift file + Includes []string // List of included Thrift files + Structs []*ThriftStruct // List of Thrift structs + Unions []*ThriftUnion // List of Thrift unions + Enums []*ThriftEnum // List of Thrift enums + Services []*ThriftService // List of Thrift services +} + +// ThriftService represents a Thrift service +type ThriftService struct { + Name string // Name of the service + Description string // Description of the service + Methods []*ThriftMethod // List of methods in the service + Options []*Option // Service-level options +} + +// ThriftMethod represents a method in a Thrift service +type ThriftMethod struct { + Name string // Name of the method + Description string // Description of the method + Input []string // List of input fields for the method + Output string // Output field for the method + Options []*Option // Options for the method +} + +// ThriftStruct represents a Thrift struct +type ThriftStruct struct { + Name string // Name of the struct + Description string // Description of the struct + Fields []*ThriftField // List of fields in the struct + Options []*Option // Options specific to this struct +} + +// ThriftField represents a field in a Thrift struct or union +type ThriftField struct { + ID int // Field ID for Thrift + Name string // Name of the field + Description string // Description of the field + Type string // Type of the field (Thrift types) + Optional bool // Indicates if the field is optional + Repeated bool // Indicates if the field is repeated (list) + Options []*Option // Additional options for this field +} + +// ThriftUnion represents a Thrift union (similar to a struct but only one field can be set at a time) +type ThriftUnion struct { + Name string // Name of the union + Fields []*ThriftField // List of fields in the union + Options []*Option // Options specific to this union +} + +// ThriftEnum represents a Thrift enum +type ThriftEnum struct { + Name string // Name of the enum + Description string // Description of the enum + Values []*ThriftEnumValue // Values within the enum + Options []*Option // Enum-level options +} + +// ThriftEnumValue represents a value in a Thrift enum +type ThriftEnumValue struct { + Index int // Index of the enum value + Value any // Enum values are integers in Thrift +} + +// Option represents an option in a Thrift field or struct +type Option struct { + Name string // Name of the option + Value interface{} // Value of the option +} diff --git a/swagger2idl/utils/utils.go b/swagger2idl/utils/utils.go new file mode 100644 index 0000000..b347840 --- /dev/null +++ b/swagger2idl/utils/utils.go @@ -0,0 +1,102 @@ +/* + * Copyright 2024 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package utils + +import ( + "regexp" + "strings" + + "github.com/getkin/kin-openapi/openapi3" + "github.com/hertz-contrib/swagger-generate/common/utils" +) + +// GetMethodName generates a method name from the OpenAPI spec +func GetMethodName(operation *openapi3.Operation, path, method string) string { + if operation.OperationID != "" { + return operation.OperationID + } + if operation.Tags != nil { + return operation.Tags[0] + } + if path != "" { + // Convert path to PascalCase, replacing placeholders with a suitable format + convertedPath := ConvertPathToPascalCase(path) + return convertedPath + strings.Title(strings.ToLower(method)) + } + // If no OperationID, generate using HTTP method + return strings.Title(strings.ToLower(method)) + "Method" +} + +// GetServiceName generates a service name from the OpenAPI spec +func GetServiceName(operation *openapi3.Operation) string { + if len(operation.Tags) > 0 { + return operation.Tags[0] + } + return "DefaultService" +} + +// GetMessageName generates a message name from the OpenAPI spec +func GetMessageName(operation *openapi3.Operation, methodName, suffix string) string { + if operation.OperationID != "" { + return operation.OperationID + suffix + } + return methodName + suffix +} + +// GetPackageName generates a package name from the OpenAPI spec +func GetPackageName(spec *openapi3.T) string { + if spec.Info.Title != "" { + return utils.FormatStr(utils.ToSnakeCase(spec.Info.Title)) + } + if spec.Info.Description != "" { + return utils.FormatStr(utils.ToSnakeCase(spec.Info.Description)) + } + return "DefaultPackage" +} + +// ConvertPathToPascalCase converts a path with placeholders to PascalCase +func ConvertPathToPascalCase(path string) string { + // Replace placeholders like {orderId} with OrderId + re := regexp.MustCompile(`\{(\w+)\}`) + path = re.ReplaceAllStringFunc(path, func(s string) string { + return utils.ToPascaleCase(strings.Trim(s, "{}")) + }) + + // Split the path by '/' and convert each segment to PascalCase + segments := strings.Split(path, "/") + for i, segment := range segments { + segments[i] = utils.ToPascaleCase(segment) + } + + // Join the segments back together + return strings.Join(segments, "") +} + +// ExtractMessageNameFromRef extracts the name of a message from a reference +func ExtractMessageNameFromRef(ref string) string { + parts := strings.Split(ref, "/") + return parts[len(parts)-1] // Return the last part, usually the name of the reference +} + +// ConvertPath converts a path with placeholders to a format that can be used in a URL +func ConvertPath(path string) string { + // Regular expression to match content inside {} + re := regexp.MustCompile(`\{(\w+)\}`) + // Replace {param} with :param + result := re.ReplaceAllString(path, ":$1") + return result +}