diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..723ef36 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.idea \ No newline at end of file diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md new file mode 100644 index 0000000..bc37192 --- /dev/null +++ b/DEVELOPMENT.md @@ -0,0 +1,9 @@ +# Development + +## Building aepc + +The standard GoLang toolchain is used, with the addition of protobuf for +compiling the resource definition. + +1. `protoc ./schema/resourcedefinition.proto --go_opt paths=source_relative --go_out=.` +2. `go build main.go` \ No newline at end of file diff --git a/README.md b/README.md index e0a8f4a..f769ab3 100644 --- a/README.md +++ b/README.md @@ -1 +1,30 @@ -# aepc \ No newline at end of file +# aepc + +Generates AEP-compliant RPCs from proto messages. See the main README.md for usage. + +## Purpose + +aepc is designed to primarily work off of a resource model: rather than having individual RPCs / methods on a resource, the user declares *resources* that live under a *service*. The common operations against a resource (Create, Read, Update, List, and Delete) are generatable based on the desired control plane standard, such as the AEP standard or custom resource definitions for the Kubernetes Resource Model. + +## Design + +aepc works off of an internal "hub" representation of a resource, while each of the consumers and producers is a "spoke", using the resource information for generation of service, clients, or documentation: + +```mermaid +flowchart LR + hub("unified service and resource hub") + protoResources("proto messages") + proto("protobuf") + crd("Custom Resource Definitions (K8S)") + http("HTTP REST APIs") + protoResources --> hub + hub --> proto + hub --> http + hub --> crd +``` + +## User Guide + +``` +go run main.go -i ./examples/bookstore.yaml -o ./examples/bookstore.yaml.output.proto +``` \ No newline at end of file diff --git a/cmd/root.go b/cmd/root.go new file mode 100644 index 0000000..faf795f --- /dev/null +++ b/cmd/root.go @@ -0,0 +1,140 @@ +// Copyright 2023 Yusuke Fredrick Tsutsumi +// +// 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 +// +// https://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 cmd + +import ( + "fmt" + "io" + "log" + "os" + "path/filepath" + + "github.com/ghodss/yaml" + + "github.com/aep-dev/aepc/loader" + "github.com/aep-dev/aepc/parser" + "github.com/aep-dev/aepc/schema" + "github.com/aep-dev/aepc/validator" + "github.com/aep-dev/aepc/writer/proto" + "github.com/spf13/cobra" + "google.golang.org/protobuf/encoding/protojson" +) + +func NewCommand() *cobra.Command { + var inputFile string + var outputFile string + + c := &cobra.Command{ + Use: "aepc", + Short: "aepc compiles resource representations to full proto rpcs", + Long: "aepc compiles resource representations to full proto rpcs", + Run: func(cmd *cobra.Command, args []string) { + // TODO: error handling + s := &schema.Service{} + input, err := readFile(inputFile) + fmt.Printf("input: %s\n", string(input)) + if err != nil { + log.Fatalf("unable to read file: %v", err) + } + ext := filepath.Ext(inputFile) + err = unmarshal(ext, input, s) + if err != nil { + log.Fatal(err) + } + errors := validator.ValidateService(s) + if len(errors) > 0 { + log.Fatalf("error validating service: %v", errors) + } + ps, err := parser.NewParsedService(s) + if err != nil { + log.Fatal(err) + } + proto, _ := proto.WriteServiceToProto(ps) + + err = writeFile(outputFile, proto) + if err != nil { + log.Fatal(err) + } + fmt.Printf("output file: %s\n", outputFile) + fmt.Printf("output proto: %s\n", proto) + }, + } + c.Flags().StringVarP(&inputFile, "input", "i", "", "input files with resource") + c.Flags().StringVarP(&outputFile, "output", "o", "", "output file to use") + return c +} + +func unmarshal(ext string, b []byte, s *schema.Service) error { + switch ext { + case ".proto": + if err := loader.ReadServiceFromProto(b, s); err != nil { + return fmt.Errorf("unable to decode proto %q: %w", string(b), err) + } + case ".yaml": + asJson, err := yaml.YAMLToJSON(b) + if err != nil { + return fmt.Errorf("unable to decode yaml to JSON %q: %w", string(b), err) + } + if err := protojson.Unmarshal(asJson, s); err != nil { + log.Fatal(fmt.Errorf("unable to decode proto %q: %w", string(b), err)) + } + case ".json": + if err := protojson.Unmarshal(b, s); err != nil { + return fmt.Errorf("unable to decode json %q: %w", string(b), err) + } + default: + return fmt.Errorf("extension %v is unsupported", ext) + } + return nil +} + +func readFile(fileName string) ([]byte, error) { + var value []byte + f, err := os.OpenFile(fileName, os.O_RDONLY, 0644) + if err != nil { + return nil, err + } + bytesRead := 1 + for bytesRead > 0 { + readBytes := make([]byte, 10000) + bytesRead, err = f.Read(readBytes) + if bytesRead > 0 { + value = append(value, readBytes[:bytesRead]...) + } + if err != io.EOF && err != nil { + return nil, err + } + } + err = f.Close() + if err != nil { + return nil, err + } + return value, nil +} + +func writeFile(fileName string, value []byte) error { + f, err := os.OpenFile(fileName, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644) + if err != nil { + return err + } + _, err = f.Write(value) + if err != nil { + return err + } + err = f.Close() + if err != nil { + return err + } + return nil +} diff --git a/examples/bookstore.proto b/examples/bookstore.proto new file mode 100644 index 0000000..b34fd42 --- /dev/null +++ b/examples/bookstore.proto @@ -0,0 +1,20 @@ +// NOTE: since we are missing some proto annotations that +// must be added, the proto representation is outdated. See bookstore.yaml +syntax = "proto3"; +package tutorial; +option go_package = "proto"; + +// bookstore.examples.com +service Bookstore { +} + +message Book { + + // represents the isbn. + string isbn = 3; + message Properties {} + message Status {} + + Properties properties = 1; + Status status = 2; +} diff --git a/examples/bookstore.proto.output.proto b/examples/bookstore.proto.output.proto new file mode 100644 index 0000000..2a22ed9 --- /dev/null +++ b/examples/bookstore.proto.output.proto @@ -0,0 +1,29 @@ +syntax = "proto3"; + +import "google/api/annotations.proto"; + +option go_package = "/newbookstore"; + +message book { + string path = 1; +} + +message CreatebookRequest { + string id = 1; + + book resource = 2; +} + +message ReadbookRequest { + string path = 1; +} + +service NewBookStore { + rpc Createbook ( CreatebookRequest ) returns ( book ) { + option (google.api.http) = { post: "/book" }; + } + + rpc Readbook ( ReadbookRequest ) returns ( book ) { + option (google.api.http) = { get: "/{path=book/*}" }; + } +} diff --git a/examples/bookstore.yaml b/examples/bookstore.yaml new file mode 100644 index 0000000..43e1c67 --- /dev/null +++ b/examples/bookstore.yaml @@ -0,0 +1,28 @@ +name: "bookstore.example.com" +resources: + - kind: "Book" + properties: + isbn: + type: STRING + number: 1 + parents: + - "bookstore.example.com/Publisher" + methods: + create: {} + read: {} + update: {} + delete: {} + list: {} + - kind: "Publisher" + methods: + read: {} + list: {} + - kind: "Author" + properties: + name: + type: STRING + number: 1 + parents: + - "Publisher" + methods: + read: {} diff --git a/examples/bookstore.yaml.output.proto b/examples/bookstore.yaml.output.proto new file mode 100644 index 0000000..3d520e5 --- /dev/null +++ b/examples/bookstore.yaml.output.proto @@ -0,0 +1,101 @@ +syntax = "proto3"; + +import "google/api/annotations.proto"; + +import "google/protobuf/empty.proto"; + +option go_package = "/bookstore"; + +message Author { + string path = 10000; + + string name = 1; +} + +message ReadAuthorRequest { + string path = 1; +} + +message Book { + string path = 10000; + + string isbn = 1; +} + +message CreateBookRequest { + string id = 1; + + Book resource = 2; +} + +message ReadBookRequest { + string path = 1; +} + +message UpdateBookRequest { + string path = 1; + + UpdateBookRequest resource = 2; +} + +message DeleteBookRequest { + string path = 1; +} + +message ListBookRequest { + string path = 1; +} + +message ListBookResponse { + repeated Book resources = 1; +} + +message Publisher { + string path = 10000; +} + +message ReadPublisherRequest { + string path = 1; +} + +message ListPublisherRequest { + string path = 1; +} + +message ListPublisherResponse { + repeated Publisher resources = 1; +} + +service Bookstore { + rpc ReadAuthor ( ReadAuthorRequest ) returns ( Author ) { + option (google.api.http) = { get: "/{path=publisher/*/author/*}" }; + } + + rpc CreateBook ( CreateBookRequest ) returns ( Book ) { + option (google.api.http) = { post: "/{parent=publisher/*}/book" }; + } + + rpc ReadBook ( ReadBookRequest ) returns ( Book ) { + option (google.api.http) = { get: "/{path=publisher/*/book/*}" }; + } + + rpc UpdateBook ( UpdateBookRequest ) returns ( Book ) { + option (google.api.http) = { get: "/{resource.name=publisher/*/book/*}" }; + } + + rpc DeleteBook ( DeleteBookRequest ) returns ( google.protobuf.Empty ) { + option (google.api.http) = { delete: "/{path=publisher/*/book/*}" }; + } + + rpc ListBook ( ListBookRequest ) returns ( ListBookResponse ) { + option (google.api.http) = { get: "/{parent=publisher/*}/book" }; + } + + rpc ReadPublisher ( ReadPublisherRequest ) returns ( Publisher ) { + option (google.api.http) = { get: "/{path=publisher/*}" }; + } + + rpc ListPublisher ( ListPublisherRequest ) returns ( ListPublisherResponse ) { + option (google.api.http) = { get: "/publisher" }; + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..4a25e05 --- /dev/null +++ b/go.mod @@ -0,0 +1,30 @@ +module github.com/aep-dev/aepc + +go 1.20 + +require ( + github.com/jhump/protoreflect v1.15.2 + github.com/spf13/cobra v1.7.0 + google.golang.org/genproto/googleapis/api v0.0.0-20230822172742-b8732ec3820d + google.golang.org/protobuf v1.31.0 +) + +require ( + github.com/bufbuild/protocompile v0.6.0 // indirect + github.com/golang/protobuf v1.5.3 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/kr/pretty v0.3.1 // indirect + github.com/spf13/pflag v1.0.5 // indirect + golang.org/x/net v0.14.0 // indirect + golang.org/x/sync v0.3.0 // indirect + google.golang.org/genproto v0.0.0-20230803162519-f966b187b2e5 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d // indirect + gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect +) + +require ( + github.com/ghodss/yaml v1.0.0 + golang.org/x/sys v0.12.0 // indirect + golang.org/x/text v0.12.0 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..588b355 --- /dev/null +++ b/go.sum @@ -0,0 +1,60 @@ +github.com/bufbuild/protocompile v0.6.0 h1:Uu7WiSQ6Yj9DbkdnOe7U4mNKp58y9WDMKDn28/ZlunY= +github.com/bufbuild/protocompile v0.6.0/go.mod h1:YNP35qEYoYGme7QMtz5SBCoN4kL4g12jTtjuzRNdjpE= +github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/jhump/protoreflect v1.15.2 h1:7YppbATX94jEt9KLAc5hICx4h6Yt3SaavhQRsIUEHP0= +github.com/jhump/protoreflect v1.15.2/go.mod h1:4ORHmSBmlCW8fh3xHmJMGyul1zNqZK4Elxc8qKP+p1k= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= +github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +golang.org/x/net v0.14.0 h1:BONx9s002vGdD9umnlX1Po8vOZmrgH34qlHcD1MfK14= +golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= +golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E= +golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/text v0.12.0 h1:k+n5B8goJNdU7hSvEtMUz3d1Q6D/XW4COJSJR6fN0mc= +golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/genproto v0.0.0-20230803162519-f966b187b2e5 h1:L6iMMGrtzgHsWofoFcihmDEMYeDR9KN/ThbPWGrh++g= +google.golang.org/genproto v0.0.0-20230803162519-f966b187b2e5/go.mod h1:oH/ZOT02u4kWEp7oYBGYFFkCdKS/uYR9Z7+0/xuuFp8= +google.golang.org/genproto/googleapis/api v0.0.0-20230822172742-b8732ec3820d h1:DoPTO70H+bcDXcd39vOqb2viZxgqeBeSGtZ55yZU4/Q= +google.golang.org/genproto/googleapis/api v0.0.0-20230822172742-b8732ec3820d/go.mod h1:KjSP20unUpOx5kyQUFa7k4OJg0qeJ7DEZflGDu2p6Bk= +google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d h1:uvYuEyMHKNt+lT4K3bN6fGswmK8qSvcreM3BwjDh+y4= +google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d/go.mod h1:+Bk1OCOj40wS2hwAMA+aCW9ypzm63QTBBHp6lQ3p+9M= +google.golang.org/grpc v1.57.0 h1:kfzNeI/klCGD2YPMUlaGNT3pxvYfga7smW3Vth8Zsiw= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= +google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +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/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +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/loader/proto.go b/loader/proto.go new file mode 100644 index 0000000..ba77944 --- /dev/null +++ b/loader/proto.go @@ -0,0 +1,68 @@ +// Copyright 2023 Yusuke Fredrick Tsutsumi +// +// 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 +// +// https://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 loader + +import ( + "log" + + "github.com/aep-dev/aepc/schema" + "github.com/jhump/protoreflect/desc" + "github.com/jhump/protoreflect/desc/protoparse" +) + +const resourcesFile = "resources.proto" + +func ReadServiceFromProto(b []byte, s *schema.Service) error { + // Create a new proto parser. + accessor := protoparse.FileContentsFromMap(map[string]string{ + resourcesFile: string(b), + }) + parser := protoparse.Parser{ + Accessor: accessor, + } + + // Parse the proto file. + files, err := parser.ParseFiles(resourcesFile) + if err != nil { + log.Fatal(err) + } + + resources := []*schema.Resource{} + + for _, fd := range files { + // find all services + services := fd.GetServices() + for _, protoS := range services { + s.Name = protoS.GetName() + } + // find all messages + messages := fd.GetMessageTypes() + for _, m := range messages { + r, err := MessageToResource(m) + if err != nil { + return nil + } + resources = append(resources, r) + } + } + s.Resources = resources + return nil +} + +func MessageToResource(m *desc.MessageDescriptor) (*schema.Resource, error) { + r := &schema.Resource{ + Kind: m.GetName(), + } + return r, nil +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..8bc67fb --- /dev/null +++ b/main.go @@ -0,0 +1,28 @@ +// Copyright 2023 Yusuke Fredrick Tsutsumi +// +// 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 +// +// https://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 ( + "fmt" + "os" + + "github.com/aep-dev/aepc/cmd" +) + +func main() { + if err := cmd.NewCommand().Execute(); err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } +} diff --git a/parser/parser.go b/parser/parser.go new file mode 100644 index 0000000..c6029b9 --- /dev/null +++ b/parser/parser.go @@ -0,0 +1,63 @@ +// package parser converts the schema +// into a full-fledged struct that provides +// more functionality for discovering resource references, etc. +package parser + +import ( + "fmt" + "strings" + + "github.com/aep-dev/aepc/schema" +) + +// ParsedService wraps schema.Service, but includes +// helper functions for things like retrieving the resource +// definitions within a service. +type ParsedService struct { + *schema.Service + ResourceByType map[string]*ParsedResource +} + +type ParsedResource struct { + *schema.Resource + Parents []*ParsedResource +} + +func NewParsedService(s *schema.Service) (*ParsedService, error) { + resourceByType, err := loadResourceByType(s) + if err != nil { + return nil, fmt.Errorf("unable to build service %q: %w", s, err) + } + ps := ParsedService{ + Service: s, + ResourceByType: resourceByType, + } + return &ps, nil +} + +func loadResourceByType(s *schema.Service) (map[string]*ParsedResource, error) { + resourceByType := map[string]*ParsedResource{} + for _, r := range s.Resources { + name := fmt.Sprintf("%s/%s", s.Name, r.Kind) + resourceByType[name] = &ParsedResource{ + Resource: r, + Parents: []*ParsedResource{}, + } + } + // populate resource parents + for _, r := range resourceByType { + for _, p := range r.Resource.Parents { + // if the string is a shorthand resource type (sans service), + // build it before checking it's existence. + if !strings.Contains(p, "/") { + p = strings.Join([]string{s.Name, p}, "/") + } + parentResource, exists := resourceByType[p] + if !exists { + return nil, fmt.Errorf("parent %q for resource %q not found", p, r.Kind) + } + r.Parents = append(r.Parents, parentResource) + } + } + return resourceByType, nil +} diff --git a/schema/resourcedefinition.pb.go b/schema/resourcedefinition.pb.go new file mode 100644 index 0000000..b131788 --- /dev/null +++ b/schema/resourcedefinition.pb.go @@ -0,0 +1,776 @@ +// resourcedefinition contains the +// schema of the resource definition. +// regenerate with +// +// protoc ./aepc/schema/resourcedefinition.proto --go_opt paths=source_relative\ +// --go_out=. + +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.31.0 +// protoc v3.6.1 +// source: schema/resourcedefinition.proto + +package schema + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type Type int32 + +const ( + Type_UNSPECIFIED Type = 0 + Type_STRING Type = 1 +) + +// Enum value maps for Type. +var ( + Type_name = map[int32]string{ + 0: "UNSPECIFIED", + 1: "STRING", + } + Type_value = map[string]int32{ + "UNSPECIFIED": 0, + "STRING": 1, + } +) + +func (x Type) Enum() *Type { + p := new(Type) + *p = x + return p +} + +func (x Type) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (Type) Descriptor() protoreflect.EnumDescriptor { + return file_schema_resourcedefinition_proto_enumTypes[0].Descriptor() +} + +func (Type) Type() protoreflect.EnumType { + return &file_schema_resourcedefinition_proto_enumTypes[0] +} + +func (x Type) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use Type.Descriptor instead. +func (Type) EnumDescriptor() ([]byte, []int) { + return file_schema_resourcedefinition_proto_rawDescGZIP(), []int{0} +} + +type Service struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + Resources []*Resource `protobuf:"bytes,2,rep,name=resources,proto3" json:"resources,omitempty"` +} + +func (x *Service) Reset() { + *x = Service{} + if protoimpl.UnsafeEnabled { + mi := &file_schema_resourcedefinition_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Service) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Service) ProtoMessage() {} + +func (x *Service) ProtoReflect() protoreflect.Message { + mi := &file_schema_resourcedefinition_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Service.ProtoReflect.Descriptor instead. +func (*Service) Descriptor() ([]byte, []int) { + return file_schema_resourcedefinition_proto_rawDescGZIP(), []int{0} +} + +func (x *Service) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *Service) GetResources() []*Resource { + if x != nil { + return x.Resources + } + return nil +} + +type Resource struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // The type of the resource. Used to programmatically + // refer to and identify the resource. + Kind string `protobuf:"bytes,1,opt,name=kind,proto3" json:"kind,omitempty"` + // The plural of the resource. Used in documentation. + Plural string `protobuf:"bytes,2,opt,name=plural,proto3" json:"plural,omitempty"` + // The list of parent resources, referred to via the kind. + Parents []string `protobuf:"bytes,3,rep,name=parents,proto3" json:"parents,omitempty"` + // Properties + Properties map[string]*Property `protobuf:"bytes,4,rep,name=properties,proto3" json:"properties,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` + // method support + Methods *Methods `protobuf:"bytes,5,opt,name=methods,proto3" json:"methods,omitempty"` +} + +func (x *Resource) Reset() { + *x = Resource{} + if protoimpl.UnsafeEnabled { + mi := &file_schema_resourcedefinition_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Resource) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Resource) ProtoMessage() {} + +func (x *Resource) ProtoReflect() protoreflect.Message { + mi := &file_schema_resourcedefinition_proto_msgTypes[1] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Resource.ProtoReflect.Descriptor instead. +func (*Resource) Descriptor() ([]byte, []int) { + return file_schema_resourcedefinition_proto_rawDescGZIP(), []int{1} +} + +func (x *Resource) GetKind() string { + if x != nil { + return x.Kind + } + return "" +} + +func (x *Resource) GetPlural() string { + if x != nil { + return x.Plural + } + return "" +} + +func (x *Resource) GetParents() []string { + if x != nil { + return x.Parents + } + return nil +} + +func (x *Resource) GetProperties() map[string]*Property { + if x != nil { + return x.Properties + } + return nil +} + +func (x *Resource) GetMethods() *Methods { + if x != nil { + return x.Methods + } + return nil +} + +type Methods struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Create *Methods_CreateMethod `protobuf:"bytes,1,opt,name=create,proto3" json:"create,omitempty"` + Read *Methods_ReadMethod `protobuf:"bytes,2,opt,name=read,proto3" json:"read,omitempty"` + Update *Methods_UpdateMethod `protobuf:"bytes,3,opt,name=update,proto3" json:"update,omitempty"` + Delete *Methods_DeleteMethod `protobuf:"bytes,4,opt,name=delete,proto3" json:"delete,omitempty"` + List *Methods_ListMethod `protobuf:"bytes,5,opt,name=list,proto3" json:"list,omitempty"` +} + +func (x *Methods) Reset() { + *x = Methods{} + if protoimpl.UnsafeEnabled { + mi := &file_schema_resourcedefinition_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Methods) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Methods) ProtoMessage() {} + +func (x *Methods) ProtoReflect() protoreflect.Message { + mi := &file_schema_resourcedefinition_proto_msgTypes[2] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Methods.ProtoReflect.Descriptor instead. +func (*Methods) Descriptor() ([]byte, []int) { + return file_schema_resourcedefinition_proto_rawDescGZIP(), []int{2} +} + +func (x *Methods) GetCreate() *Methods_CreateMethod { + if x != nil { + return x.Create + } + return nil +} + +func (x *Methods) GetRead() *Methods_ReadMethod { + if x != nil { + return x.Read + } + return nil +} + +func (x *Methods) GetUpdate() *Methods_UpdateMethod { + if x != nil { + return x.Update + } + return nil +} + +func (x *Methods) GetDelete() *Methods_DeleteMethod { + if x != nil { + return x.Delete + } + return nil +} + +func (x *Methods) GetList() *Methods_ListMethod { + if x != nil { + return x.List + } + return nil +} + +type Property struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Type Type `protobuf:"varint,1,opt,name=type,proto3,enum=Type" json:"type,omitempty"` + // field number used for protobuf or other systems where fields must + // be explicitly enumerated. + Number int32 `protobuf:"varint,2,opt,name=number,proto3" json:"number,omitempty"` +} + +func (x *Property) Reset() { + *x = Property{} + if protoimpl.UnsafeEnabled { + mi := &file_schema_resourcedefinition_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Property) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Property) ProtoMessage() {} + +func (x *Property) ProtoReflect() protoreflect.Message { + mi := &file_schema_resourcedefinition_proto_msgTypes[3] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Property.ProtoReflect.Descriptor instead. +func (*Property) Descriptor() ([]byte, []int) { + return file_schema_resourcedefinition_proto_rawDescGZIP(), []int{3} +} + +func (x *Property) GetType() Type { + if x != nil { + return x.Type + } + return Type_UNSPECIFIED +} + +func (x *Property) GetNumber() int32 { + if x != nil { + return x.Number + } + return 0 +} + +type Methods_CreateMethod struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields +} + +func (x *Methods_CreateMethod) Reset() { + *x = Methods_CreateMethod{} + if protoimpl.UnsafeEnabled { + mi := &file_schema_resourcedefinition_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Methods_CreateMethod) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Methods_CreateMethod) ProtoMessage() {} + +func (x *Methods_CreateMethod) ProtoReflect() protoreflect.Message { + mi := &file_schema_resourcedefinition_proto_msgTypes[5] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Methods_CreateMethod.ProtoReflect.Descriptor instead. +func (*Methods_CreateMethod) Descriptor() ([]byte, []int) { + return file_schema_resourcedefinition_proto_rawDescGZIP(), []int{2, 0} +} + +type Methods_ReadMethod struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields +} + +func (x *Methods_ReadMethod) Reset() { + *x = Methods_ReadMethod{} + if protoimpl.UnsafeEnabled { + mi := &file_schema_resourcedefinition_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Methods_ReadMethod) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Methods_ReadMethod) ProtoMessage() {} + +func (x *Methods_ReadMethod) ProtoReflect() protoreflect.Message { + mi := &file_schema_resourcedefinition_proto_msgTypes[6] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Methods_ReadMethod.ProtoReflect.Descriptor instead. +func (*Methods_ReadMethod) Descriptor() ([]byte, []int) { + return file_schema_resourcedefinition_proto_rawDescGZIP(), []int{2, 1} +} + +type Methods_UpdateMethod struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields +} + +func (x *Methods_UpdateMethod) Reset() { + *x = Methods_UpdateMethod{} + if protoimpl.UnsafeEnabled { + mi := &file_schema_resourcedefinition_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Methods_UpdateMethod) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Methods_UpdateMethod) ProtoMessage() {} + +func (x *Methods_UpdateMethod) ProtoReflect() protoreflect.Message { + mi := &file_schema_resourcedefinition_proto_msgTypes[7] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Methods_UpdateMethod.ProtoReflect.Descriptor instead. +func (*Methods_UpdateMethod) Descriptor() ([]byte, []int) { + return file_schema_resourcedefinition_proto_rawDescGZIP(), []int{2, 2} +} + +type Methods_DeleteMethod struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields +} + +func (x *Methods_DeleteMethod) Reset() { + *x = Methods_DeleteMethod{} + if protoimpl.UnsafeEnabled { + mi := &file_schema_resourcedefinition_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Methods_DeleteMethod) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Methods_DeleteMethod) ProtoMessage() {} + +func (x *Methods_DeleteMethod) ProtoReflect() protoreflect.Message { + mi := &file_schema_resourcedefinition_proto_msgTypes[8] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Methods_DeleteMethod.ProtoReflect.Descriptor instead. +func (*Methods_DeleteMethod) Descriptor() ([]byte, []int) { + return file_schema_resourcedefinition_proto_rawDescGZIP(), []int{2, 3} +} + +type Methods_ListMethod struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields +} + +func (x *Methods_ListMethod) Reset() { + *x = Methods_ListMethod{} + if protoimpl.UnsafeEnabled { + mi := &file_schema_resourcedefinition_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Methods_ListMethod) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Methods_ListMethod) ProtoMessage() {} + +func (x *Methods_ListMethod) ProtoReflect() protoreflect.Message { + mi := &file_schema_resourcedefinition_proto_msgTypes[9] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Methods_ListMethod.ProtoReflect.Descriptor instead. +func (*Methods_ListMethod) Descriptor() ([]byte, []int) { + return file_schema_resourcedefinition_proto_rawDescGZIP(), []int{2, 4} +} + +var File_schema_resourcedefinition_proto protoreflect.FileDescriptor + +var file_schema_resourcedefinition_proto_rawDesc = []byte{ + 0x0a, 0x1f, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, + 0x65, 0x64, 0x65, 0x66, 0x69, 0x6e, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x22, 0x46, 0x0a, 0x07, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x12, 0x0a, 0x04, + 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, + 0x12, 0x27, 0x0a, 0x09, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x18, 0x02, 0x20, + 0x03, 0x28, 0x0b, 0x32, 0x09, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x09, + 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x22, 0xf9, 0x01, 0x0a, 0x08, 0x52, 0x65, + 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6b, 0x69, 0x6e, 0x64, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6b, 0x69, 0x6e, 0x64, 0x12, 0x16, 0x0a, 0x06, 0x70, 0x6c, + 0x75, 0x72, 0x61, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x70, 0x6c, 0x75, 0x72, + 0x61, 0x6c, 0x12, 0x18, 0x0a, 0x07, 0x70, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x73, 0x18, 0x03, 0x20, + 0x03, 0x28, 0x09, 0x52, 0x07, 0x70, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x73, 0x12, 0x39, 0x0a, 0x0a, + 0x70, 0x72, 0x6f, 0x70, 0x65, 0x72, 0x74, 0x69, 0x65, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, + 0x32, 0x19, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x50, 0x72, 0x6f, 0x70, + 0x65, 0x72, 0x74, 0x69, 0x65, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x0a, 0x70, 0x72, 0x6f, + 0x70, 0x65, 0x72, 0x74, 0x69, 0x65, 0x73, 0x12, 0x22, 0x0a, 0x07, 0x6d, 0x65, 0x74, 0x68, 0x6f, + 0x64, 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x08, 0x2e, 0x4d, 0x65, 0x74, 0x68, 0x6f, + 0x64, 0x73, 0x52, 0x07, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x73, 0x1a, 0x48, 0x0a, 0x0f, 0x50, + 0x72, 0x6f, 0x70, 0x65, 0x72, 0x74, 0x69, 0x65, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, + 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, + 0x12, 0x1f, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x09, 0x2e, 0x50, 0x72, 0x6f, 0x70, 0x65, 0x72, 0x74, 0x79, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, + 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0xb4, 0x02, 0x0a, 0x07, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, + 0x73, 0x12, 0x2d, 0x0a, 0x06, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x15, 0x2e, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x73, 0x2e, 0x43, 0x72, 0x65, 0x61, + 0x74, 0x65, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x52, 0x06, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, + 0x12, 0x27, 0x0a, 0x04, 0x72, 0x65, 0x61, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, + 0x2e, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x73, 0x2e, 0x52, 0x65, 0x61, 0x64, 0x4d, 0x65, 0x74, + 0x68, 0x6f, 0x64, 0x52, 0x04, 0x72, 0x65, 0x61, 0x64, 0x12, 0x2d, 0x0a, 0x06, 0x75, 0x70, 0x64, + 0x61, 0x74, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x4d, 0x65, 0x74, 0x68, + 0x6f, 0x64, 0x73, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, + 0x52, 0x06, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x12, 0x2d, 0x0a, 0x06, 0x64, 0x65, 0x6c, 0x65, + 0x74, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x4d, 0x65, 0x74, 0x68, 0x6f, + 0x64, 0x73, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x52, + 0x06, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x12, 0x27, 0x0a, 0x04, 0x6c, 0x69, 0x73, 0x74, 0x18, + 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x73, 0x2e, + 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x52, 0x04, 0x6c, 0x69, 0x73, 0x74, + 0x1a, 0x0e, 0x0a, 0x0c, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, + 0x1a, 0x0c, 0x0a, 0x0a, 0x52, 0x65, 0x61, 0x64, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x1a, 0x0e, + 0x0a, 0x0c, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x1a, 0x0e, + 0x0a, 0x0c, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x1a, 0x0c, + 0x0a, 0x0a, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x22, 0x3d, 0x0a, 0x08, + 0x50, 0x72, 0x6f, 0x70, 0x65, 0x72, 0x74, 0x79, 0x12, 0x19, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x05, 0x2e, 0x54, 0x79, 0x70, 0x65, 0x52, 0x04, 0x74, + 0x79, 0x70, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x6e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x05, 0x52, 0x06, 0x6e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x2a, 0x23, 0x0a, 0x04, 0x54, + 0x79, 0x70, 0x65, 0x12, 0x0f, 0x0a, 0x0b, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, + 0x45, 0x44, 0x10, 0x00, 0x12, 0x0a, 0x0a, 0x06, 0x53, 0x54, 0x52, 0x49, 0x4e, 0x47, 0x10, 0x01, + 0x42, 0x09, 0x5a, 0x07, 0x2f, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x62, 0x06, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x33, +} + +var ( + file_schema_resourcedefinition_proto_rawDescOnce sync.Once + file_schema_resourcedefinition_proto_rawDescData = file_schema_resourcedefinition_proto_rawDesc +) + +func file_schema_resourcedefinition_proto_rawDescGZIP() []byte { + file_schema_resourcedefinition_proto_rawDescOnce.Do(func() { + file_schema_resourcedefinition_proto_rawDescData = protoimpl.X.CompressGZIP(file_schema_resourcedefinition_proto_rawDescData) + }) + return file_schema_resourcedefinition_proto_rawDescData +} + +var file_schema_resourcedefinition_proto_enumTypes = make([]protoimpl.EnumInfo, 1) +var file_schema_resourcedefinition_proto_msgTypes = make([]protoimpl.MessageInfo, 10) +var file_schema_resourcedefinition_proto_goTypes = []interface{}{ + (Type)(0), // 0: Type + (*Service)(nil), // 1: Service + (*Resource)(nil), // 2: Resource + (*Methods)(nil), // 3: Methods + (*Property)(nil), // 4: Property + nil, // 5: Resource.PropertiesEntry + (*Methods_CreateMethod)(nil), // 6: Methods.CreateMethod + (*Methods_ReadMethod)(nil), // 7: Methods.ReadMethod + (*Methods_UpdateMethod)(nil), // 8: Methods.UpdateMethod + (*Methods_DeleteMethod)(nil), // 9: Methods.DeleteMethod + (*Methods_ListMethod)(nil), // 10: Methods.ListMethod +} +var file_schema_resourcedefinition_proto_depIdxs = []int32{ + 2, // 0: Service.resources:type_name -> Resource + 5, // 1: Resource.properties:type_name -> Resource.PropertiesEntry + 3, // 2: Resource.methods:type_name -> Methods + 6, // 3: Methods.create:type_name -> Methods.CreateMethod + 7, // 4: Methods.read:type_name -> Methods.ReadMethod + 8, // 5: Methods.update:type_name -> Methods.UpdateMethod + 9, // 6: Methods.delete:type_name -> Methods.DeleteMethod + 10, // 7: Methods.list:type_name -> Methods.ListMethod + 0, // 8: Property.type:type_name -> Type + 4, // 9: Resource.PropertiesEntry.value:type_name -> Property + 10, // [10:10] is the sub-list for method output_type + 10, // [10:10] is the sub-list for method input_type + 10, // [10:10] is the sub-list for extension type_name + 10, // [10:10] is the sub-list for extension extendee + 0, // [0:10] is the sub-list for field type_name +} + +func init() { file_schema_resourcedefinition_proto_init() } +func file_schema_resourcedefinition_proto_init() { + if File_schema_resourcedefinition_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_schema_resourcedefinition_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Service); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_schema_resourcedefinition_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Resource); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_schema_resourcedefinition_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Methods); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_schema_resourcedefinition_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Property); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_schema_resourcedefinition_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Methods_CreateMethod); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_schema_resourcedefinition_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Methods_ReadMethod); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_schema_resourcedefinition_proto_msgTypes[7].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Methods_UpdateMethod); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_schema_resourcedefinition_proto_msgTypes[8].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Methods_DeleteMethod); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_schema_resourcedefinition_proto_msgTypes[9].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Methods_ListMethod); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_schema_resourcedefinition_proto_rawDesc, + NumEnums: 1, + NumMessages: 10, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_schema_resourcedefinition_proto_goTypes, + DependencyIndexes: file_schema_resourcedefinition_proto_depIdxs, + EnumInfos: file_schema_resourcedefinition_proto_enumTypes, + MessageInfos: file_schema_resourcedefinition_proto_msgTypes, + }.Build() + File_schema_resourcedefinition_proto = out.File + file_schema_resourcedefinition_proto_rawDesc = nil + file_schema_resourcedefinition_proto_goTypes = nil + file_schema_resourcedefinition_proto_depIdxs = nil +} diff --git a/schema/resourcedefinition.proto b/schema/resourcedefinition.proto new file mode 100644 index 0000000..5bb3a28 --- /dev/null +++ b/schema/resourcedefinition.proto @@ -0,0 +1,53 @@ +// resourcedefinition contains the +// schema of the resource definition. +// regenerate with +// +// protoc ./aepc/schema/resourcedefinition.proto --go_opt paths=source_relative\ +// --go_out=. +syntax = "proto3"; +option go_package = "/schema"; + +message Service { + string name = 1; + repeated Resource resources = 2; +} + +message Resource { + // The type of the resource. Used to programmatically + // refer to and identify the resource. + string kind = 1; + // The plural of the resource. Used in documentation. + string plural = 2; + // The list of parent resources, referred to via the kind. + repeated string parents = 3; + // Properties + map properties = 4; + // method support + Methods methods = 5; +} + +message Methods { + message CreateMethod {} + message ReadMethod {} + message UpdateMethod {} + message DeleteMethod {} + message ListMethod {} + + CreateMethod create = 1; + ReadMethod read = 2; + UpdateMethod update = 3; + DeleteMethod delete = 4; + ListMethod list = 5; +} + +enum Type { + UNSPECIFIED = 0; + STRING = 1; +} + +message Property { + Type type = 1; + // field number used for protobuf or other systems where fields must + // be explicitly enumerated. + int32 number = 2; +} \ No newline at end of file diff --git a/validator/validator.go b/validator/validator.go new file mode 100644 index 0000000..6ac24cc --- /dev/null +++ b/validator/validator.go @@ -0,0 +1,48 @@ +// Package validator exposes validation functionality for the +// resource definition. +package validator + +import ( + "fmt" + "regexp" + + "github.com/aep-dev/aepc/schema" +) + +const ( + RESOURCE_KIND_REGEX_STRING = "[A-Z]+[a-zA-Z0-9]*" +) + +var RESOURCE_KIND_REGEX regexp.Regexp + +func init() { + // compile regex via init rather than const to use the other string + // constants. + RESOURCE_KIND_REGEX = *regexp.MustCompile(RESOURCE_KIND_REGEX_STRING) +} + +// ValidateService returns one or more errors +// with a service. +func ValidateService(s *schema.Service) []error { + errors := []error{} + for _, r := range s.Resources { + for _, err := range validateResource(r) { + errors = append(errors, fmt.Errorf("error validating resource %q: %w", r.Kind, err)) + } + } + return errors +} + +// validateResource returns any validation errors +// with a resource. +func validateResource(r *schema.Resource) []error { + regex := regexp.MustCompile(RESOURCE_KIND_REGEX_STRING) + errors := []error{} + if !regex.MatchString(r.Kind) { + errors = append( + errors, + fmt.Errorf("kind must match regex %q", RESOURCE_KIND_REGEX_STRING), + ) + } + return errors +} diff --git a/writer/proto/constants.go b/writer/proto/constants.go new file mode 100644 index 0000000..96165ad --- /dev/null +++ b/writer/proto/constants.go @@ -0,0 +1,19 @@ +// Copyright 2023 Yusuke Fredrick Tsutsumi +// +// 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 +// +// https://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 proto + +const FIELD_NAME_PATH = "path" +const FIELD_NAME_RESOURCE = "resource" +const FIELD_NAME_RESOURCES = "resources" +const FIELD_NAME_ID = "id" diff --git a/writer/proto/proto.go b/writer/proto/proto.go new file mode 100644 index 0000000..dad55b2 --- /dev/null +++ b/writer/proto/proto.go @@ -0,0 +1,85 @@ +// Copyright 2023 Yusuke Fredrick Tsutsumi +// +// 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 +// +// https://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 proto + +import ( + "bytes" + "fmt" + "sort" + "strings" + + "github.com/aep-dev/aepc/parser" + "github.com/jhump/protoreflect/desc/builder" + "github.com/jhump/protoreflect/desc/protoprint" + "golang.org/x/text/cases" + "golang.org/x/text/language" + "google.golang.org/protobuf/types/descriptorpb" +) + +var capitalizer cases.Caser + +func init() { + capitalizer = cases.Title(language.AmericanEnglish) +} + +func WriteServiceToProto(ps *parser.ParsedService) ([]byte, error) { + fb := builder.NewFile("test.proto") + fb.IsProto3 = true + fb.SetComments(builder.Comments{ + LeadingComment: "this file is autogenerated by aepc", + }) + pServiceName := toProtoServiceName(ps.Name) + serviceNameAsLower := fmt.Sprintf("/%s", strings.ToLower(pServiceName)) + fo := &descriptorpb.FileOptions{ + GoPackage: &serviceNameAsLower, + } + fb.SetOptions(fo) + sb := builder.NewService(pServiceName) + fb.AddService(sb) + for _, r := range getSortedResources(ps.ResourceByType) { + err := AddResource(r, fb, sb) + if err != nil { + return []byte{}, fmt.Errorf("adding resource %v failed: %w", r.Kind, err) + } + } + fd, err := fb.Build() + if err != nil { + return []byte{}, fmt.Errorf("unable to build service file %v: %w", fb.GetName(), err) + } + printer := protoprint.Printer{} + var output bytes.Buffer + err = printer.PrintProtoFile(fd, &output) + if err != nil { + return []byte{}, err + } + return output.Bytes(), nil +} + +func toProtoServiceName(serviceName string) string { + parts := strings.SplitN(serviceName, ".", 2) + return capitalizer.String(parts[0]) +} + +func getSortedResources(prsByString map[string]*parser.ParsedResource) []*parser.ParsedResource { + keys := []string{} + for k := range prsByString { + keys = append(keys, k) + } + sort.Strings(keys) + resources := make([]*parser.ParsedResource, 0, len(keys)) + for _, k := range keys { + resources = append(resources, prsByString[k]) + } + return resources +} diff --git a/writer/proto/resource.go b/writer/proto/resource.go new file mode 100644 index 0000000..f9b294d --- /dev/null +++ b/writer/proto/resource.go @@ -0,0 +1,246 @@ +// Copyright 2023 Yusuke Fredrick Tsutsumi +// +// 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 +// +// https://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 proto + +import ( + "fmt" + "strings" + + "github.com/aep-dev/aepc/parser" + "github.com/aep-dev/aepc/schema" + "github.com/jhump/protoreflect/desc" + "github.com/jhump/protoreflect/desc/builder" + "google.golang.org/genproto/googleapis/api/annotations" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/types/descriptorpb" +) + +// AddResource adds a resource's protos and RPCs to a file and service. +func AddResource(r *parser.ParsedResource, fb *builder.FileBuilder, sb *builder.ServiceBuilder) error { + resourceMb, err := GeneratedResourceMessage(r) + if err != nil { + return fmt.Errorf("unable to generated resource %v: %w", r.Kind, err) + } + fb.AddMessage(resourceMb) + if r.Methods != nil { + if r.Methods.Create != nil { + err = AddCreate(r, resourceMb, fb, sb) + if err != nil { + return err + } + } + if r.Methods.Read != nil { + err = AddRead(r, resourceMb, fb, sb) + if err != nil { + return err + } + } + if r.Methods.Update != nil { + err = AddUpdate(r, resourceMb, fb, sb) + if err != nil { + return err + } + } + if r.Methods.Delete != nil { + err = AddDelete(r, resourceMb, fb, sb) + if err != nil { + return err + } + } + if r.Methods.List != nil { + err = AddList(r, resourceMb, fb, sb) + if err != nil { + return err + } + } + } + return nil +} + +// GenerateResourceMesssage adds the resource message. +func GeneratedResourceMessage(r *parser.ParsedResource) (*builder.MessageBuilder, error) { + mb := builder.NewMessage(r.Kind) + // standard fields start at 10k, in the range until 11k. + mb.AddField( + builder.NewField(FIELD_NAME_PATH, builder.FieldTypeString()).SetNumber(10000), + ) + // standard fields are added afterward. + for n, p := range r.Properties { + typ := builder.FieldTypeBool() + switch p.Type { + case schema.Type_STRING: + typ = builder.FieldTypeString() + } + mb.AddField(builder.NewField(n, typ).SetNumber(p.Number)) + } + mb.SetOptions( + &descriptorpb.MessageOptions{}, + // annotations.ResourceDescriptor{ + // "type": sb.GetName() + "/" + r.Kind, + //}, + ) + // md.GetMessageOptions().ProtoReflect().Set(protoreflect.FieldDescriptor, protoreflect.Value) + // mb.AddNestedExtension( + // builder.NewExtension("google.api.http", tag int32, typ *builder.FieldType, extendee *builder.MessageBuilder) + // ) + return mb, nil +} + +func AddCreate(r *parser.ParsedResource, resourceMb *builder.MessageBuilder, fb *builder.FileBuilder, sb *builder.ServiceBuilder) error { + // add the resource message + // create request messages + mb := builder.NewMessage("Create" + r.Kind + "Request") + mb.AddField(builder.NewField(FIELD_NAME_ID, builder.FieldTypeString()).SetNumber(1)) + mb.AddField(builder.NewField(FIELD_NAME_RESOURCE, builder.FieldTypeMessage(resourceMb)).SetNumber(2)) + fb.AddMessage(mb) + method := builder.NewMethod("Create"+r.Kind, + builder.RpcTypeMessage(mb, false), + builder.RpcTypeMessage(resourceMb, false), + ) + options := &descriptorpb.MethodOptions{} + proto.SetExtension(options, annotations.E_Http, &annotations.HttpRule{ + Pattern: &annotations.HttpRule_Post{ + Post: generateParentHTTPPath(r), + }, + }) + method.SetOptions(options) + sb.AddMethod(method) + return nil +} + +// AddRead adds a read method for the resource, along with +// any required messages. +func AddRead(r *parser.ParsedResource, resourceMb *builder.MessageBuilder, fb *builder.FileBuilder, sb *builder.ServiceBuilder) error { + mb := builder.NewMessage("Read" + r.Kind + "Request") + mb.AddField( + builder.NewField(FIELD_NAME_PATH, builder.FieldTypeString()).SetNumber(1), + ) + fb.AddMessage(mb) + method := builder.NewMethod("Read"+r.Kind, + builder.RpcTypeMessage(mb, false), + builder.RpcTypeMessage(resourceMb, false), + ) + options := &descriptorpb.MethodOptions{} + proto.SetExtension(options, annotations.E_Http, &annotations.HttpRule{ + Pattern: &annotations.HttpRule_Get{ + Get: fmt.Sprintf("/{path=%v}", generateHTTPPath(r)), + }, + }) + method.SetOptions(options) + sb.AddMethod(method) + return nil +} + +// AddRead adds a read method for the resource, along with +// any required messages. +func AddUpdate(r *parser.ParsedResource, resourceMb *builder.MessageBuilder, fb *builder.FileBuilder, sb *builder.ServiceBuilder) error { + mb := builder.NewMessage("Update" + r.Kind + "Request") + mb.AddField( + builder.NewField(FIELD_NAME_PATH, builder.FieldTypeString()).SetNumber(1), + ).AddField( + builder.NewField(FIELD_NAME_RESOURCE, builder.FieldTypeMessage(mb)).SetNumber(2), + ) + fb.AddMessage(mb) + method := builder.NewMethod("Update"+r.Kind, + builder.RpcTypeMessage(mb, false), + builder.RpcTypeMessage(resourceMb, false), + ) + options := &descriptorpb.MethodOptions{} + proto.SetExtension(options, annotations.E_Http, &annotations.HttpRule{ + Pattern: &annotations.HttpRule_Get{ + Get: fmt.Sprintf("/{resource.name=%v}", generateHTTPPath(r)), + }, + }) + method.SetOptions(options) + sb.AddMethod(method) + return nil +} + +func AddDelete(r *parser.ParsedResource, resourceMb *builder.MessageBuilder, fb *builder.FileBuilder, sb *builder.ServiceBuilder) error { + // add the resource message + // create request messages + mb := builder.NewMessage("Delete" + r.Kind + "Request") + mb.AddField( + builder.NewField(FIELD_NAME_PATH, builder.FieldTypeString()).SetNumber(1), + ) + fb.AddMessage(mb) + emptyMd, err := desc.LoadMessageDescriptor("google.protobuf.Empty") + if err != nil { + return err + } + method := builder.NewMethod("Delete"+r.Kind, + builder.RpcTypeMessage(mb, false), + builder.RpcTypeImportedMessage(emptyMd, false), + ) + options := &descriptorpb.MethodOptions{} + proto.SetExtension(options, annotations.E_Http, &annotations.HttpRule{ + Pattern: &annotations.HttpRule_Delete{ + Delete: fmt.Sprintf("/{path=%v}", generateHTTPPath(r)), + }, + }) + method.SetOptions(options) + sb.AddMethod(method) + return nil +} + +func AddList(r *parser.ParsedResource, resourceMb *builder.MessageBuilder, fb *builder.FileBuilder, sb *builder.ServiceBuilder) error { + // add the resource message + // create request messages + reqMb := builder.NewMessage("List" + r.Kind + "Request") + reqMb.AddField( + builder.NewField(FIELD_NAME_PATH, builder.FieldTypeString()).SetNumber(1), + ) + fb.AddMessage(reqMb) + respMb := builder.NewMessage("List" + r.Kind + "Response") + respMb.AddField( + builder.NewField(FIELD_NAME_RESOURCES, builder.FieldTypeMessage(resourceMb)).SetRepeated().SetNumber(1), + ) + fb.AddMessage(respMb) + method := builder.NewMethod("List"+r.Kind, + builder.RpcTypeMessage(reqMb, false), + builder.RpcTypeMessage(respMb, false), + ) + options := &descriptorpb.MethodOptions{} + proto.SetExtension(options, annotations.E_Http, &annotations.HttpRule{ + Pattern: &annotations.HttpRule_Get{ + Get: generateParentHTTPPath(r), + }, + }) + method.SetOptions(options) + sb.AddMethod(method) + return nil +} + +func generateHTTPPath(r *parser.ParsedResource) string { + elements := []string{strings.ToLower(r.Kind)} + if len(r.Parents) > 0 { + // TODO: handle multiple parents + p := r.Parents[0] + for p != nil { + elements = append([]string{strings.ToLower(p.Kind)}, elements...) + if len(p.Parents) == 0 { + break + } + } + } + return fmt.Sprintf("%v/*", strings.Join(elements, "/*/")) +} + +func generateParentHTTPPath(r *parser.ParsedResource) string { + parentPath := "" + if len(r.Parents) > 0 { + parentPath = fmt.Sprintf("{parent=%v}/", generateHTTPPath(r.Parents[0])) + } + return fmt.Sprintf("/%v%v", parentPath, strings.ToLower(r.Kind)) +}