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
+}