diff options
| author | Xe Iaso <me@xeiaso.net> | 2024-02-24 14:48:23 -0500 |
|---|---|---|
| committer | Xe Iaso <me@xeiaso.net> | 2024-02-24 14:50:50 -0500 |
| commit | b27ea6b8587f6735a309e47e1abcf3acd12f7953 (patch) | |
| tree | 1c943a6a73fc1e7fbbaaa50852881f4d35252882 /cmd | |
| parent | 4bca52442d62a5dee3d8b533b48df0223e1679b0 (diff) | |
| download | xesite-b27ea6b8587f6735a309e47e1abcf3acd12f7953.tar.xz xesite-b27ea6b8587f6735a309e47e1abcf3acd12f7953.zip | |
cmd: import twirp-openapi-gen, fix some bugs
Signed-off-by: Xe Iaso <me@xeiaso.net>
Diffstat (limited to 'cmd')
15 files changed, 5471 insertions, 0 deletions
diff --git a/cmd/twirp-openapi-gen/LICENSE b/cmd/twirp-openapi-gen/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/cmd/twirp-openapi-gen/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/cmd/twirp-openapi-gen/README.md b/cmd/twirp-openapi-gen/README.md new file mode 100644 index 0000000..d3bf9a8 --- /dev/null +++ b/cmd/twirp-openapi-gen/README.md @@ -0,0 +1,3 @@ +# twirp-openapi-gen + +This was imported from [github.com/blockthrough/twirp-openapi-gen](https://github.com/blockthrough/twirp-openapi-gen) and a few non-upstreamable changes were made. These changes are made in order to meet my needs in particular. If you want to use this tool, I recommend you to use the original repository. diff --git a/cmd/twirp-openapi-gen/internal/generator/aliases.go b/cmd/twirp-openapi-gen/internal/generator/aliases.go new file mode 100644 index 0000000..a8e29f3 --- /dev/null +++ b/cmd/twirp-openapi-gen/internal/generator/aliases.go @@ -0,0 +1,85 @@ +package generator + +var typeAliases = map[string]struct { + Type, Format string +}{ + // proto numeric types + "int32": {Type: "integer", Format: "int32"}, + "uint32": {Type: "integer", Format: "uint32"}, + "sint32": {Type: "integer", Format: "int32"}, + "fixed32": {Type: "integer", Format: "int32"}, + "sfixed32": {Type: "integer", Format: "int32"}, + + // proto numeric types, 64bit + "int64": {Type: "string", Format: "int64"}, + "uint64": {Type: "string", Format: "uint64"}, + "sint64": {Type: "string", Format: "int64"}, + "fixed64": {Type: "string", Format: "int64"}, + "sfixed64": {Type: "string", Format: "int64"}, + + "double": {Type: "number", Format: "double"}, + "float": {Type: "number", Format: "float"}, + + // effectively copies google.protobuf.BytesValue + "bytes": { + Type: "string", + Format: "byte", + }, + + // It is what it is + "bool": { + Type: "boolean", + Format: "boolean", + }, + + "google.protobuf.Timestamp": { + Type: "string", + Format: "date-time", + }, + "google.protobuf.Duration": { + Type: "string", + }, + "google.protobuf.StringValue": { + Type: "string", + }, + "google.protobuf.BytesValue": { + Type: "string", + Format: "byte", + }, + "google.protobuf.Int32Value": { + Type: "integer", + Format: "int32", + }, + "google.protobuf.UInt32Value": { + Type: "integer", + Format: "uint32", + }, + "google.protobuf.Int64Value": { + Type: "string", + Format: "int64", + }, + "google.protobuf.UInt64Value": { + Type: "string", + Format: "uint64", + }, + "google.protobuf.FloatValue": { + Type: "number", + Format: "float", + }, + "google.protobuf.DoubleValue": { + Type: "number", + Format: "double", + }, + "google.protobuf.BoolValue": { + Type: "boolean", + Format: "boolean", + }, + "google.protobuf.Empty": { + Type: "object", + }, + + "google.type.DateTime": { + Type: "string", + Format: "date-time", + }, +} diff --git a/cmd/twirp-openapi-gen/internal/generator/generator.go b/cmd/twirp-openapi-gen/internal/generator/generator.go new file mode 100644 index 0000000..2a08055 --- /dev/null +++ b/cmd/twirp-openapi-gen/internal/generator/generator.go @@ -0,0 +1,187 @@ +package generator + +import ( + "encoding/json" + "fmt" + "log/slog" + "os" + "path/filepath" + + "github.com/emicklei/proto" + "github.com/getkin/kin-openapi/openapi3" + "github.com/invopop/yaml" +) + +type generatorConfig struct { + protoPaths []string + servers []string + title string + docVersion string + pathPrefix string + format string +} + +type Option func(config *generatorConfig) error + +func ProtoPaths(paths []string) Option { + return func(config *generatorConfig) error { + config.protoPaths = paths + return nil + } +} + +func Servers(servers []string) Option { + return func(config *generatorConfig) error { + config.servers = servers + return nil + } +} + +func Title(title string) Option { + return func(config *generatorConfig) error { + config.title = title + return nil + } +} + +func DocVersion(version string) Option { + return func(config *generatorConfig) error { + config.docVersion = version + return nil + } +} + +func PathPrefix(pathPrefix string) Option { + return func(config *generatorConfig) error { + config.pathPrefix = pathPrefix + return nil + } +} + +func Format(format string) Option { + return func(config *generatorConfig) error { + config.format = format + return nil + } +} + +type generator struct { + openAPIV3 *openapi3.T + + conf *generatorConfig + inputFiles []string + packageName string + + importedFiles map[string]struct{} +} + +func NewGenerator(inputFiles []string, options ...Option) (*generator, error) { + conf := generatorConfig{} + for _, opt := range options { + if err := opt(&conf); err != nil { + return nil, err + } + } + + if len(inputFiles) < 1 { + return nil, fmt.Errorf("missing input files") + } + + openAPIV3 := openapi3.T{ + OpenAPI: "3.0.0", + Info: &openapi3.Info{ + Title: conf.title, + Version: conf.docVersion, + }, + Paths: openapi3.Paths{}, + Components: &openapi3.Components{ + Schemas: map[string]*openapi3.SchemaRef{}, + }, + } + + for _, server := range conf.servers { + openAPIV3.Servers = append(openAPIV3.Servers, &openapi3.Server{URL: server}) + } + + slog.Debug("generating doc", "format", conf.format, "inputFiles", inputFiles) + + return &generator{ + inputFiles: inputFiles, + openAPIV3: &openAPIV3, + conf: &conf, + importedFiles: map[string]struct{}{}, + }, nil +} + +func (gen *generator) Generate(filename string) error { + if _, err := gen.Parse(); err != nil { + return err + } + + if err := gen.Save(filename); err != nil { + return err + } + + return nil +} + +func (gen *generator) Parse() (*openapi3.T, error) { + for _, filename := range gen.inputFiles { + protoFile, err := readProtoFile(filename, gen.conf.protoPaths) + if err != nil { + return nil, fmt.Errorf("readProtoFile: %w", err) + } + proto.Walk(protoFile, gen.Handlers()...) + } + + slog.Debug("generated", "paths", len(gen.openAPIV3.Paths), "components", len(gen.openAPIV3.Components.Schemas)) + return gen.openAPIV3, nil +} + +func (gen *generator) Save(filename string) error { + var by []byte + var err error + switch gen.conf.format { + case "json": + by, err = gen.JSON() + case "yaml", "yml": + by, err = gen.YAML() + default: + return fmt.Errorf("missing format") + } + if err != nil { + return err + } + + return os.WriteFile(filename, by, os.ModePerm^0111) +} + +func (gen *generator) JSON() ([]byte, error) { + return json.MarshalIndent(gen.openAPIV3, "", " ") +} + +func (gen *generator) YAML() ([]byte, error) { + return yaml.Marshal(gen.openAPIV3) +} + +func readProtoFile(filename string, protoPaths []string) (*proto.Proto, error) { + var file *os.File + var err error + for _, path := range append(protoPaths, "") { + file, err = os.Open(filepath.Join(path, filename)) + if err != nil { + if os.IsNotExist(err) { + continue + } + return nil, fmt.Errorf("Open: %w", err) + } + break + } + if file == nil { + return nil, fmt.Errorf("could not read file %q", filename) + } + defer file.Close() + + parser := proto.NewParser(file) + return parser.Parse() +} diff --git a/cmd/twirp-openapi-gen/internal/generator/generator_test.go b/cmd/twirp-openapi-gen/internal/generator/generator_test.go new file mode 100644 index 0000000..78af950 --- /dev/null +++ b/cmd/twirp-openapi-gen/internal/generator/generator_test.go @@ -0,0 +1,317 @@ +package generator + +import ( + "flag" + "log/slog" + "os" + "strings" + "testing" +) + +type ProtoRPC struct { + name string + input string + output string + desc string +} + +type ProtoMessage struct { + name string + fields []ProtoField +} + +type ProtoField struct { + name string + fieldType string + format string + desc string + enums []string + ref string + itemsRef string + itemsType string +} + +var ( + verbose = flag.Bool("slog.verbose", false, "print debug logs to the console") +) + +func init() { + if *verbose { + h := slog.NewJSONHandler(os.Stderr, &slog.HandlerOptions{ + AddSource: true, + Level: slog.LevelDebug, + }) + slog.SetDefault(slog.New(h)) + } else { + slog.SetDefault(slog.New(slog.NewJSONHandler(os.Stderr, &slog.HandlerOptions{ + AddSource: true, + }))) + } +} + +func TestGenerator(t *testing.T) { + flag.Parse() + + opts := []Option{ + ProtoPaths([]string{"./testdata/paymentapis", "./testdata/petapis"}), + Servers([]string{"https://example.com"}), + Title("Test"), + DocVersion("0.1"), + Format("json"), + } + gen, err := NewGenerator([]string{"./testdata/petapis/pet/v1/pet.proto"}, opts...) + if err != nil { + t.Fatal(err) + } + openAPI, err := gen.Parse() + if err != nil { + t.Fatal(err) + } + + if err := gen.Save("./testdata/doc.json"); err != nil { + t.Fatal(err) + } + + pkgName := "pet.v1" + serviceName := "PetStoreService" + rpcs := []ProtoRPC{ + { + name: "GetPet", + input: "GetPetRequest", + output: "GetPetResponse", + }, + } + messages := []ProtoMessage{ + { + name: "GetPetRequest", + fields: []ProtoField{ + { + name: "pet_id", + fieldType: "string", + }, + }, + }, + { + name: "Pet", + fields: []ProtoField{ + { + name: "pet_type", + fieldType: "object", + ref: "#/components/schemas/pet.v1.PetType", + enums: []string{ + "PET_TYPE_UNSPECIFIED", + "PET_TYPE_CAT", + "PET_TYPE_DOG", + "PET_TYPE_SNAKE", + "PET_TYPE_HAMSTER", + }, + }, + { + name: "pet_types", + fieldType: "array", + itemsRef: "#/components/schemas/pet.v1.PetType", + }, + { + name: "tags", + fieldType: "array", + itemsType: "string", + }, + { + name: "pet_id", + fieldType: "string", + desc: "pet_id is an auto-generated id for the pet\\nthe id uniquely identifies a pet in the system", + }, + { + name: "name", + fieldType: "string", + }, + { + name: "created_at", + fieldType: "string", + format: "date-time", + }, + { + name: "vet", + fieldType: "object", + ref: "#/components/schemas/pet.v1.Vet", + }, + { + name: "vets", + fieldType: "array", + itemsRef: "#/components/schemas/pet.v1.Vet", + itemsType: "object", + }, + }, + }, + } + + t.Run("RPC", func(t *testing.T) { + for _, rpc := range rpcs { + pathName := "/" + pkgName + "." + serviceName + "/" + rpc.name + path, ok := openAPI.Paths[pathName] + if !ok { + t.Errorf("%s: missing rpc %q", pathName, rpc.name) + } + + if path.Description != rpc.desc { + t.Errorf("%s: expected desc %q but got %q", pathName, rpc.desc, path.Description) + } + + post := path.Post + if post == nil { + t.Errorf("%s: missing post", pathName) + continue + } + + if post.Summary != rpc.name { + t.Errorf("%s: expected summary %q but got %q", pathName, rpc.name, post.Summary) + } + + requestBodyRef := post.RequestBody + if requestBodyRef == nil { + t.Errorf("%s: missing request body", pathName) + continue + } + + // request + { + requestBody := requestBodyRef.Value + if requestBody == nil { + t.Errorf("%s: missing request body", pathName) + continue + } + + mediaType, ok := requestBody.Content["application/json"] + if !ok { + t.Errorf("%s: missing content type", pathName) + continue + } + + if mediaType.Schema == nil { + t.Errorf("%s: missing media type schema", pathName) + continue + } + + expectedRef := "#/components/schemas/" + pkgName + "." + rpc.input + if mediaType.Schema.Ref != expectedRef { + t.Errorf("%s: expected ref %q but got %q", pathName, expectedRef, mediaType.Schema.Ref) + } + } + + // response + { + respRef := post.Responses["200"] + if respRef == nil { + t.Errorf("%s: missing resp", pathName) + continue + } + + resp := respRef.Value + if resp == nil { + t.Errorf("%s: missing resp", pathName) + continue + } + + mediaType, ok := resp.Content["application/json"] + if !ok { + t.Errorf("%s: missing content type", pathName) + continue + } + + if mediaType.Schema == nil { + t.Errorf("%s: missing media type schema", pathName) + continue + } + + expectedRef := "#/components/schemas/" + pkgName + "." + rpc.output + if mediaType.Schema.Ref != expectedRef { + t.Errorf("%s: expected ref %q but got %q", pathName, expectedRef, mediaType.Schema.Ref) + } + } + } + }) + + t.Run("Messages", func(*testing.T) { + for _, message := range messages { + schemaName := "" + pkgName + "." + message.name + schema, ok := openAPI.Components.Schemas[schemaName] + if !ok { + t.Errorf("%s: missing message %q", schemaName, message.name) + } + if schema.Value == nil { + t.Errorf("%s: missing component", schemaName) + continue + } + properties := schema.Value.Properties + for _, messageField := range message.fields { + propertyRef, ok := properties[messageField.name] + if !ok { + t.Errorf("%s: missing property %q", schemaName, messageField.name) + } + + if propertyRef == nil || propertyRef.Value == nil { + t.Errorf("%s: missing property ref", schemaName) + continue + } + + property := propertyRef.Value + if property.Type != messageField.fieldType { + t.Errorf("%s: %q expected property type %q but got %q", schemaName, message.name, messageField.fieldType, property.Type) + continue + } + + if messageField.format != "" { + if messageField.format != "" && property.Format != messageField.format { + t.Errorf("%s: expected property format %q but got %q", schemaName, messageField.format, property.Format) + continue + } + } + + if propertyRef.Ref != messageField.ref { + t.Errorf("%s: %q expected reference %q but got %q", schemaName, messageField.name, messageField.ref, propertyRef.Ref) + } + + // check the reference schema + if messageField.ref != "" { + refParts := strings.Split(messageField.ref, "/") + // the reference schema has the format of #/components/schemas/<type> so we need to get the last part + schemaRef, ok := openAPI.Components.Schemas[refParts[len(refParts)-1]] + if !ok { + t.Errorf("%s: %q expected reference schema %q but got nil", schemaName, messageField.name, messageField.ref) + } else { + // check if the schema reference has the expected enum values + if len(messageField.enums) > 0 { + if schemaRef.Value.Enum == nil { + t.Errorf("%s: %q expected reference schema enums %q but got nil", schemaName, messageField.name, messageField.ref) + } else { + enums := map[string]struct{}{} + for _, e := range schemaRef.Value.Enum { + enums[e.(string)] = struct{}{} + } + for _, e := range messageField.enums { + if _, ok := enums[e]; !ok { + t.Errorf("%s: %q expected reference schema enum %q to have %q but got nil", schemaName, messageField.name, messageField.ref, e) + } + } + } + } + } + } + + if property.Type == "array" { + if property.Items == nil || property.Items.Value == nil { + t.Errorf("%s: missing property enum array items", schemaName) + } + // only check the array items type if it's not a reference + if messageField.itemsRef == "" && (property.Items.Value.Type != messageField.itemsType) { + t.Errorf("%s: expected %s items type %q but got %q", schemaName, messageField.name, messageField.itemsType, property.Items.Value.Type) + } + // check the array items reference schema + if property.Items.Ref != messageField.itemsRef { + t.Errorf("%s: expected %s items ref %q but got %q", schemaName, messageField.name, messageField.itemsRef, property.Items.Ref) + } + } + } + } + }) +} diff --git a/cmd/twirp-openapi-gen/internal/generator/handlers.go b/cmd/twirp-openapi-gen/internal/generator/handlers.go new file mode 100644 index 0000000..c11eb46 --- /dev/null +++ b/cmd/twirp-openapi-gen/internal/generator/handlers.go @@ -0,0 +1,562 @@ +package generator + +import ( + "encoding/json" + "fmt" + "log" + "log/slog" + "path/filepath" + "strings" + "unicode" + + "github.com/emicklei/proto" + "github.com/getkin/kin-openapi/openapi3" +) + +const ( + googleAnyType = "google.protobuf.Any" + googleListValueType = "google.protobuf.ListValue" + googleStructType = "google.protobuf.Struct" + googleValueType = "google.protobuf.Value" + + googleMoneyType = "google.type.Money" +) + +var ( + successDescription = "Success" +) + +func (gen *generator) Handlers() []proto.Handler { + return []proto.Handler{ + proto.WithPackage(gen.Package), + proto.WithImport(gen.Import), + proto.WithRPC(gen.RPC), + proto.WithEnum(gen.Enum), + proto.WithMessage(gen.Message), + } +} + +func (gen *generator) Package(pkg *proto.Package) { + slog.Debug("Package handler", "package", pkg.Name) + gen.packageName = pkg.Name +} + +func (gen *generator) Import(i *proto.Import) { + slog.Debug("Import handler", "package", gen.packageName, "filename", i.Filename) + + if _, ok := gen.importedFiles[i.Filename]; ok { + return + } + gen.importedFiles[i.Filename] = struct{}{} + + // Instead of loading and generating the OpenAPI docs for the google proto definitions, + // its known types are mapped to OpenAPI types; see aliases.go. + if strings.Contains(i.Filename, "google/") { + return + } + + protoFile, err := readProtoFile(i.Filename, gen.conf.protoPaths) + if err != nil { + slog.Error("could not import file", "filename", i.Filename, "error", err) + return + } + + oldPackageName := gen.packageName + + // Override the package name for the next round of Walk calls to preserve the types full import path + withPackage := func(pkg *proto.Package) { + gen.packageName = pkg.Name + } + + // additional files walked for messages and imports only + proto.Walk(protoFile, + proto.WithPackage(withPackage), + proto.WithImport(gen.Import), + proto.WithRPC(gen.RPC), + proto.WithEnum(gen.Enum), + proto.WithMessage(gen.Message), + ) + + gen.packageName = oldPackageName +} + +func (gen *generator) RPC(rpc *proto.RPC) { + slog.Debug("RPC handler", "package", gen.packageName, "rpc", rpc.Name, "requestType", rpc.RequestType, "returnsType", rpc.ReturnsType) + + parent, ok := rpc.Parent.(*proto.Service) + if !ok { + log.Panicf("parent is not proto.service") + } + pathName := filepath.Join("/"+gen.conf.pathPrefix+"/", gen.packageName+"."+parent.Name, rpc.Name) + + var reqMediaType *openapi3.MediaType + switch rpc.RequestType { + case "google.protobuf.Empty": + reqMediaType = openapi3.NewMediaType() |
