From bb8797c869d96d75f25c19a62266eb109509ace8 Mon Sep 17 00:00:00 2001 From: Yusuke Tsutsumi Date: Tue, 22 Oct 2024 22:59:34 -0700 Subject: [PATCH] feat: multiple nesting, deduped collection names The AEPs allow deduplicating collection names (for example, removing the "book" from "book-editions" in the path). Since this is a fairly common practice, make it something that aepc will do for you by default. Also fixing a few places where multiple nested children was not working, as well as additional issues that came from a hyphen in the resource singular. Also started building out an internal/ package - eventually most packages will move there, as aepc is a first and foremost a command-line interface and intended to be used as a binary. A utils library was added to do basic casing, as it is common to do so. --- example/bookstore/v1/bookstore.proto | 182 +++++++++++++++----- example/bookstore/v1/bookstore.yaml | 24 ++- example/bookstore/v1/bookstore_openapi.json | 176 +++++++++++++++++++ example/bookstore/v1/bookstore_openapi.yaml | 116 +++++++++++++ go.mod | 2 +- go.sum | 2 + internal/utils/cases.go | 19 ++ writer/openapi/openapi.go | 33 +++- writer/proto/proto.go | 3 +- writer/proto/resource.go | 48 +++--- writer/writer_utils/utils.go | 23 +++ 11 files changed, 555 insertions(+), 73 deletions(-) create mode 100644 internal/utils/cases.go create mode 100644 writer/writer_utils/utils.go diff --git a/example/bookstore/v1/bookstore.proto b/example/bookstore/v1/bookstore.proto index da0fbef..ea42140 100644 --- a/example/bookstore/v1/bookstore.proto +++ b/example/bookstore/v1/bookstore.proto @@ -19,7 +19,7 @@ option go_package = "/bookstore"; // A service. service Bookstore { // An aep-compliant Create method for book. - rpc Createbook ( CreatebookRequest ) returns ( book ) { + rpc CreateBook ( CreateBookRequest ) returns ( Book ) { option (google.api.http) = { post: "/{parent=publishers/*}/books", body: "book" @@ -29,14 +29,14 @@ service Bookstore { } // An aep-compliant Get method for book. - rpc Getbook ( GetbookRequest ) returns ( book ) { + rpc GetBook ( GetBookRequest ) returns ( Book ) { option (google.api.http) = { get: "/{path=publishers/*/books/*}" }; option (google.api.method_signature) = "path"; } // An aep-compliant Update method for book. - rpc Updatebook ( UpdatebookRequest ) returns ( book ) { + rpc UpdateBook ( UpdateBookRequest ) returns ( Book ) { option (google.api.http) = { patch: "/{path=publishers/*/books/*}", body: "book" @@ -46,29 +46,62 @@ service Bookstore { } // An aep-compliant Delete method for book. - rpc Deletebook ( DeletebookRequest ) returns ( google.protobuf.Empty ) { + rpc DeleteBook ( DeleteBookRequest ) returns ( google.protobuf.Empty ) { option (google.api.http) = { delete: "/{path=publishers/*/books/*}" }; option (google.api.method_signature) = "path"; } // An aep-compliant List method for books. - rpc Listbook ( ListbookRequest ) returns ( ListbookResponse ) { + rpc ListBooks ( ListBooksRequest ) returns ( ListBooksResponse ) { option (google.api.http) = { get: "/{parent=publishers/*}/books" }; option (google.api.method_signature) = "parent"; } // An aep-compliant Apply method for books. - rpc Applybook ( ApplybookRequest ) returns ( book ) { + rpc Applybook ( ApplybookRequest ) returns ( Book ) { option (google.api.http) = { put: "/{path=publishers/*/books/*}", body: "book" }; } + // An aep-compliant Create method for book-edition. + rpc CreateBookEdition ( CreateBookEditionRequest ) returns ( BookEdition ) { + option (google.api.http) = { + post: "/{parent=publishers/*/books/*}/editions", + body: "book_edition" + }; + + option (google.api.method_signature) = "parent,book_edition"; + } + + // An aep-compliant Get method for book-edition. + rpc GetBookEdition ( GetBookEditionRequest ) returns ( BookEdition ) { + option (google.api.http) = { get: "/{path=publishers/*/books/*/editions/*}" }; + + option (google.api.method_signature) = "path"; + } + + // An aep-compliant Delete method for book-edition. + rpc DeleteBookEdition ( DeleteBookEditionRequest ) returns ( google.protobuf.Empty ) { + option (google.api.http) = { + delete: "/{path=publishers/*/books/*/editions/*}" + }; + + option (google.api.method_signature) = "path"; + } + + // An aep-compliant List method for book-editions. + rpc ListBookEditions ( ListBookEditionsRequest ) returns ( ListBookEditionsResponse ) { + option (google.api.http) = { get: "/{parent=publishers/*/books/*}/editions" }; + + option (google.api.method_signature) = "parent"; + } + // An aep-compliant Create method for publisher. - rpc Createpublisher ( CreatepublisherRequest ) returns ( publisher ) { + rpc CreatePublisher ( CreatePublisherRequest ) returns ( Publisher ) { option (google.api.http) = { post: "/{parent=publishers}", body: "publisher" @@ -78,14 +111,14 @@ service Bookstore { } // An aep-compliant Get method for publisher. - rpc Getpublisher ( GetpublisherRequest ) returns ( publisher ) { + rpc GetPublisher ( GetPublisherRequest ) returns ( Publisher ) { option (google.api.http) = { get: "/{path=publishers/*}" }; option (google.api.method_signature) = "path"; } // An aep-compliant Update method for publisher. - rpc Updatepublisher ( UpdatepublisherRequest ) returns ( publisher ) { + rpc UpdatePublisher ( UpdatePublisherRequest ) returns ( Publisher ) { option (google.api.http) = { patch: "/{path=publishers/*}", body: "publisher" @@ -95,27 +128,27 @@ service Bookstore { } // An aep-compliant Delete method for publisher. - rpc Deletepublisher ( DeletepublisherRequest ) returns ( google.protobuf.Empty ) { + rpc DeletePublisher ( DeletePublisherRequest ) returns ( google.protobuf.Empty ) { option (google.api.http) = { delete: "/{path=publishers/*}" }; option (google.api.method_signature) = "path"; } // An aep-compliant List method for publishers. - rpc Listpublisher ( ListpublisherRequest ) returns ( ListpublisherResponse ) { + rpc ListPublishers ( ListPublishersRequest ) returns ( ListPublishersResponse ) { option (google.api.http) = { get: "/{parent=publishers}" }; option (google.api.method_signature) = "parent"; } // An aep-compliant Apply method for publishers. - rpc Applypublisher ( ApplypublisherRequest ) returns ( publisher ) { + rpc Applypublisher ( ApplypublisherRequest ) returns ( Publisher ) { option (google.api.http) = { put: "/{path=publishers/*}", body: "publisher" }; } } -// A book. -message book { +// A Book. +message Book { // A Author. message Author { // Field for firstName. @@ -148,7 +181,7 @@ message book { } // A Create request for a book resource. -message CreatebookRequest { +message CreateBookRequest { // A field for the parent of book string parent = 10013 [ (google.api.field_behavior) = REQUIRED, @@ -159,11 +192,11 @@ message CreatebookRequest { string id = 10014; // The resource to perform the operation on. - book book = 10015 [(google.api.field_behavior) = REQUIRED]; + Book book = 10015 [(google.api.field_behavior) = REQUIRED]; } // Request message for the Getbook method -message GetbookRequest { +message GetBookRequest { // The globally unique identifier for the resource string path = 10018 [ (google.api.field_behavior) = REQUIRED, @@ -171,8 +204,8 @@ message GetbookRequest { ]; } -// Request message for the Updatebook method -message UpdatebookRequest { +// Request message for the UpdateBook method +message UpdateBookRequest { // The globally unique identifier for the resource string path = 10018 [ (google.api.field_behavior) = REQUIRED, @@ -180,14 +213,14 @@ message UpdatebookRequest { ]; // The resource to perform the operation on. - book book = 10015 [(google.api.field_behavior) = REQUIRED]; + Book book = 10015 [(google.api.field_behavior) = REQUIRED]; // The update mask for the resource google.protobuf.FieldMask update_mask = 10012; } -// Request message for the Deletebook method -message DeletebookRequest { +// Request message for the DeleteBook method +message DeleteBookRequest { // The globally unique identifier for the resource string path = 10018 [ (google.api.field_behavior) = REQUIRED, @@ -196,7 +229,7 @@ message DeletebookRequest { } // Request message for the Listbook method -message ListbookRequest { +message ListBooksRequest { // A field for the parent of book string parent = 10013 [ (google.api.field_behavior) = REQUIRED, @@ -211,9 +244,9 @@ message ListbookRequest { } // Response message for the Listbook method -message ListbookResponse { +message ListBooksResponse { // A list of books - repeated book results = 10016; + repeated Book results = 10016; // The page token indicating the ending point of this response. string next_page_token = 10011; @@ -228,11 +261,80 @@ message ApplybookRequest { ]; // The resource to perform the operation on. - book book = 10015 [(google.api.field_behavior) = REQUIRED]; + Book book = 10015 [(google.api.field_behavior) = REQUIRED]; +} + +// A BookEdition. +message BookEdition { + // Field for name. + string name = 1 [(google.api.field_behavior) = REQUIRED]; + + // Field for path. + string path = 10000; + + // Field for id. + string id = 10001; +} + +// A Create request for a book-edition resource. +message CreateBookEditionRequest { + // A field for the parent of book-edition + string parent = 10013 [ + (google.api.field_behavior) = REQUIRED, + (google.api.resource_reference) = { } + ]; + + // An id that uniquely identifies the resource within the collection + string id = 10014; + + // The resource to perform the operation on. + BookEdition book_edition = 10015 [(google.api.field_behavior) = REQUIRED]; +} + +// Request message for the Getbook-edition method +message GetBookEditionRequest { + // The globally unique identifier for the resource + string path = 10018 [ + (google.api.field_behavior) = REQUIRED, + (google.api.resource_reference) = { type: "bookstore.example.com/book-edition" } + ]; +} + +// Request message for the DeleteBookEdition method +message DeleteBookEditionRequest { + // The globally unique identifier for the resource + string path = 10018 [ + (google.api.field_behavior) = REQUIRED, + (google.api.resource_reference) = { type: "bookstore.example.com/book-edition" } + ]; +} + +// Request message for the Listbook-edition method +message ListBookEditionsRequest { + // A field for the parent of book-edition + string parent = 10013 [ + (google.api.field_behavior) = REQUIRED, + (google.api.resource_reference) = { } + ]; + + // The page token indicating the starting point of the page + string page_token = 10010; + + // The maximum number of resources to return in a single page. + int32 max_page_size = 10017; +} + +// Response message for the Listbook-edition method +message ListBookEditionsResponse { + // A list of book-editions + repeated BookEdition results = 10016; + + // The page token indicating the ending point of this response. + string next_page_token = 10011; } -// A publisher. -message publisher { +// A Publisher. +message Publisher { // Field for description. string description = 1; @@ -244,7 +346,7 @@ message publisher { } // A Create request for a publisher resource. -message CreatepublisherRequest { +message CreatePublisherRequest { // A field for the parent of publisher string parent = 10013 [ (google.api.field_behavior) = REQUIRED, @@ -255,11 +357,11 @@ message CreatepublisherRequest { string id = 10014; // The resource to perform the operation on. - publisher publisher = 10015 [(google.api.field_behavior) = REQUIRED]; + Publisher publisher = 10015 [(google.api.field_behavior) = REQUIRED]; } // Request message for the Getpublisher method -message GetpublisherRequest { +message GetPublisherRequest { // The globally unique identifier for the resource string path = 10018 [ (google.api.field_behavior) = REQUIRED, @@ -267,8 +369,8 @@ message GetpublisherRequest { ]; } -// Request message for the Updatepublisher method -message UpdatepublisherRequest { +// Request message for the UpdatePublisher method +message UpdatePublisherRequest { // The globally unique identifier for the resource string path = 10018 [ (google.api.field_behavior) = REQUIRED, @@ -276,14 +378,14 @@ message UpdatepublisherRequest { ]; // The resource to perform the operation on. - publisher publisher = 10015 [(google.api.field_behavior) = REQUIRED]; + Publisher publisher = 10015 [(google.api.field_behavior) = REQUIRED]; // The update mask for the resource google.protobuf.FieldMask update_mask = 10012; } -// Request message for the Deletepublisher method -message DeletepublisherRequest { +// Request message for the DeletePublisher method +message DeletePublisherRequest { // The globally unique identifier for the resource string path = 10018 [ (google.api.field_behavior) = REQUIRED, @@ -292,7 +394,7 @@ message DeletepublisherRequest { } // Request message for the Listpublisher method -message ListpublisherRequest { +message ListPublishersRequest { // A field for the parent of publisher string parent = 10013 [ (google.api.field_behavior) = REQUIRED, @@ -307,9 +409,9 @@ message ListpublisherRequest { } // Response message for the Listpublisher method -message ListpublisherResponse { +message ListPublishersResponse { // A list of publishers - repeated publisher results = 10016; + repeated Publisher results = 10016; // The page token indicating the ending point of this response. string next_page_token = 10011; @@ -324,5 +426,5 @@ message ApplypublisherRequest { ]; // The resource to perform the operation on. - publisher publisher = 10015 [(google.api.field_behavior) = REQUIRED]; + Publisher publisher = 10015 [(google.api.field_behavior) = REQUIRED]; } diff --git a/example/bookstore/v1/bookstore.yaml b/example/bookstore/v1/bookstore.yaml index 442d67a..44b36c9 100644 --- a/example/bookstore/v1/bookstore.yaml +++ b/example/bookstore/v1/bookstore.yaml @@ -58,6 +58,24 @@ resources: delete: {} list: {} apply: {} # do not uncomment until there is an AEP on apply. - # other example resources that might be interesting to add: - # authors, which could be a reference for book - # authors could have a reference to publishers too + # other example resources that might be interesting to add: + # authors, which could be a reference for book + # authors could have a reference to publishers too + # example of a child resource, with a redudant type name. + # aepc will remove the redundant component in the path pattern + - kind: "book-edition" + plural: "book-editions" + # the parents should specify the parents of the resource. It takes in the + # kind. + parents: + - "book" + properties: + name: + type: STRING + number: 1 + required: true + methods: + create: {} + read: {} + list: {} + delete: {} diff --git a/example/bookstore/v1/bookstore_openapi.json b/example/bookstore/v1/bookstore_openapi.json index 3b2556f..585b8f7 100644 --- a/example/bookstore/v1/bookstore_openapi.json +++ b/example/bookstore/v1/bookstore_openapi.json @@ -320,6 +320,152 @@ } ] } + }, + "/publishers/{publisher_id}/books/{book_id}/editions": { + "get": { + "responses": { + "200": { + "schema": { + "items": { + "$ref": "#/components/schemas/book-edition" + } + } + } + }, + "parameters": [ + { + "in": "path", + "name": "publisher_id", + "schema": {}, + "required": true, + "type": "string" + }, + { + "in": "path", + "name": "book_id", + "schema": {}, + "required": true, + "type": "string" + }, + { + "in": "query", + "name": "max_page_size", + "schema": {}, + "required": true, + "type": "integer" + }, + { + "in": "query", + "name": "page_token", + "schema": {}, + "required": true, + "type": "string" + } + ] + }, + "post": { + "responses": { + "200": { + "schema": { + "$ref": "#/components/schemas/book-edition" + } + } + }, + "parameters": [ + { + "in": "path", + "name": "publisher_id", + "schema": {}, + "required": true, + "type": "string" + }, + { + "in": "path", + "name": "book_id", + "schema": {}, + "required": true, + "type": "string" + }, + { + "in": "body", + "name": "body", + "schema": { + "$ref": "#/components/schemas/book-edition" + } + }, + { + "in": "query", + "name": "id", + "schema": {}, + "required": true, + "type": "string" + } + ] + } + }, + "/publishers/{publisher_id}/books/{book_id}/editions/{book_edition_id}": { + "delete": { + "responses": { + "200": { + "schema": {} + } + }, + "parameters": [ + { + "in": "path", + "name": "publisher_id", + "schema": {}, + "required": true, + "type": "string" + }, + { + "in": "path", + "name": "book_id", + "schema": {}, + "required": true, + "type": "string" + }, + { + "in": "path", + "name": "book_edition_id", + "schema": {}, + "required": true, + "type": "string" + } + ] + }, + "get": { + "responses": { + "200": { + "schema": { + "$ref": "#/components/schemas/book-edition" + } + } + }, + "parameters": [ + { + "in": "path", + "name": "publisher_id", + "schema": {}, + "required": true, + "type": "string" + }, + { + "in": "path", + "name": "book_id", + "schema": {}, + "required": true, + "type": "string" + }, + { + "in": "path", + "name": "book_edition_id", + "schema": {}, + "required": true, + "type": "string" + } + ] + } } }, "components": { @@ -376,6 +522,36 @@ ] } }, + "book-edition": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "id": { + "type": "string", + "readOnly": true, + "x-terraform-id": true + }, + "name": { + "type": "string" + }, + "path": { + "type": "string", + "readOnly": true + } + }, + "x-aep-resource": { + "singular": "book-edition", + "plural": "book-editions", + "patterns": [ + "/publishers/{publisher_id}/books/{book_id}/editions/{book_edition_id}" + ], + "parents": [ + "book" + ] + } + }, "publisher": { "type": "object", "properties": { diff --git a/example/bookstore/v1/bookstore_openapi.yaml b/example/bookstore/v1/bookstore_openapi.yaml index 0fa1547..d8bc02e 100644 --- a/example/bookstore/v1/bookstore_openapi.yaml +++ b/example/bookstore/v1/bookstore_openapi.yaml @@ -37,6 +37,27 @@ components: - /publishers/{publisher_id}/books/{book_id} plural: books singular: book + book-edition: + properties: + id: + readOnly: true + type: string + x-terraform-id: true + name: + type: string + path: + readOnly: true + type: string + required: + - name + type: object + x-aep-resource: + parents: + - book + patterns: + - /publishers/{publisher_id}/books/{book_id}/editions/{book_edition_id} + plural: book-editions + singular: book-edition publisher: properties: description: @@ -248,6 +269,101 @@ paths: "200": schema: $ref: '#/components/schemas/book' + /publishers/{publisher_id}/books/{book_id}/editions: + get: + parameters: + - in: path + name: publisher_id + required: true + schema: {} + type: string + - in: path + name: book_id + required: true + schema: {} + type: string + - in: query + name: max_page_size + required: true + schema: {} + type: integer + - in: query + name: page_token + required: true + schema: {} + type: string + responses: + "200": + schema: + items: + $ref: '#/components/schemas/book-edition' + post: + parameters: + - in: path + name: publisher_id + required: true + schema: {} + type: string + - in: path + name: book_id + required: true + schema: {} + type: string + - in: body + name: body + schema: + $ref: '#/components/schemas/book-edition' + - in: query + name: id + required: true + schema: {} + type: string + responses: + "200": + schema: + $ref: '#/components/schemas/book-edition' + /publishers/{publisher_id}/books/{book_id}/editions/{book_edition_id}: + delete: + parameters: + - in: path + name: publisher_id + required: true + schema: {} + type: string + - in: path + name: book_id + required: true + schema: {} + type: string + - in: path + name: book_edition_id + required: true + schema: {} + type: string + responses: + "200": + schema: {} + get: + parameters: + - in: path + name: publisher_id + required: true + schema: {} + type: string + - in: path + name: book_id + required: true + schema: {} + type: string + - in: path + name: book_edition_id + required: true + schema: {} + type: string + responses: + "200": + schema: + $ref: '#/components/schemas/book-edition' schemes: - http servers: diff --git a/go.mod b/go.mod index a12c739..c4af91d 100644 --- a/go.mod +++ b/go.mod @@ -55,7 +55,7 @@ require ( github.com/hashicorp/terraform-registry-address v0.0.0-20210412075316-9b2996cce896 // indirect github.com/hashicorp/terraform-svchost v0.0.0-20200729002733-f050f53b9734 // indirect github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d // indirect - github.com/iancoleman/strcase v0.0.0-20180726023541-3605ed457bf7 // indirect + github.com/iancoleman/strcase v0.3.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329 // indirect github.com/mattn/go-colorable v0.1.4 // indirect diff --git a/go.sum b/go.sum index ebd2dd2..055ba94 100644 --- a/go.sum +++ b/go.sum @@ -296,6 +296,8 @@ github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpO github.com/huandu/xstrings v1.3.2/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= github.com/iancoleman/strcase v0.0.0-20180726023541-3605ed457bf7 h1:ux/56T2xqZO/3cP1I2F86qpeoYPCOzk+KF/UH/Ar+lk= github.com/iancoleman/strcase v0.0.0-20180726023541-3605ed457bf7/go.mod h1:SK73tn/9oHe+/Y0h39VT4UCxmurVJkR5NA7kMEAOgSE= +github.com/iancoleman/strcase v0.3.0 h1:nTXanmYxhfFAMjZL34Ov6gkzEsSJZ5DbhxWjvSASxEI= +github.com/iancoleman/strcase v0.3.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU= diff --git a/internal/utils/cases.go b/internal/utils/cases.go new file mode 100644 index 0000000..9fd3063 --- /dev/null +++ b/internal/utils/cases.go @@ -0,0 +1,19 @@ +package utils + +import ( + "strings" +) + +func KebabToCamelCase(s string) string { + parts := strings.Split(s, "-") + for i := range parts { + if len(parts[i]) > 0 { + parts[i] = strings.ToUpper(string(parts[i][0])) + parts[i][1:] + } + } + return strings.Join(parts, "") +} + +func KebabToSnakeCase(s string) string { + return strings.ReplaceAll(s, "-", "_") +} diff --git a/writer/openapi/openapi.go b/writer/openapi/openapi.go index ce6ce7d..b9e17f1 100644 --- a/writer/openapi/openapi.go +++ b/writer/openapi/openapi.go @@ -3,10 +3,13 @@ package openapi import ( "encoding/json" "fmt" + "strings" "github.com/aep-dev/aepc/constants" + "github.com/aep-dev/aepc/internal/utils" "github.com/aep-dev/aepc/parser" "github.com/aep-dev/aepc/schema" + "github.com/aep-dev/aepc/writer/writer_utils" "golang.org/x/text/cases" "golang.org/x/text/language" ) @@ -55,6 +58,8 @@ func convertToOpenAPI(service *parser.ParsedService) (*OpenAPI, error) { } patterns := []string{} schemaRef := fmt.Sprintf("#/components/schemas/%v", r.Kind) + singular := utils.KebabToSnakeCase(r.Kind) + collection := writer_utils.CollectionName(r) // declare some commonly used objects, to be used later. bodyParam := ParameterInfo{ In: "body", @@ -65,7 +70,7 @@ func convertToOpenAPI(service *parser.ParsedService) (*OpenAPI, error) { } idParam := ParameterInfo{ In: "path", - Name: fmt.Sprintf("%s_id", lowerizer.String(r.Kind)), + Name: fmt.Sprintf("%s_id", singular), Required: true, Type: "string", } @@ -75,10 +80,10 @@ func convertToOpenAPI(service *parser.ParsedService) (*OpenAPI, error) { }, } for _, pwp := range *parentPWPS { - resourcePath := fmt.Sprintf("%s/%s/{%s_id}", pwp.Pattern, lowerizer.String(r.Plural), lowerizer.String(r.Kind)) + resourcePath := fmt.Sprintf("%s/%s/{%s_id}", pwp.Pattern, collection, singular) patterns = append(patterns, resourcePath) if r.Methods.List != nil { - listPath := fmt.Sprintf("%s/%s", pwp.Pattern, lowerizer.String(r.Plural)) + listPath := fmt.Sprintf("%s/%s", pwp.Pattern, collection) addMethodToPath(paths, listPath, "get", MethodInfo{ Parameters: append(pwp.Params, ParameterInfo{ @@ -106,7 +111,7 @@ func convertToOpenAPI(service *parser.ParsedService) (*OpenAPI, error) { }) } if r.Methods.Create != nil { - createPath := fmt.Sprintf("%s/%s", pwp.Pattern, lowerizer.String(r.Plural)) + createPath := fmt.Sprintf("%s/%s", pwp.Pattern, collection) params := append(pwp.Params, bodyParam, ParameterInfo{ In: "query", @@ -264,7 +269,7 @@ func generateParentPatternsWithParams(r *parser.ParsedResource) *[]PathWithParam } pwps := []PathWithParams{} for _, parent := range r.ParsedParents { - basePattern := fmt.Sprintf("/%s/{%s_id}", lowerizer.String(parent.Plural), lowerizer.String(parent.Kind)) + basePattern := fmt.Sprintf("/%s/{%s_id}", writer_utils.CollectionName(parent), lowerizer.String(parent.Kind)) baseParam := ParameterInfo{ In: "path", Name: fmt.Sprintf("%s_id", lowerizer.String(parent.Kind)), @@ -279,7 +284,7 @@ func generateParentPatternsWithParams(r *parser.ParsedResource) *[]PathWithParam } else { for _, parentPWP := range *generateParentPatternsWithParams(parent) { params := append(parentPWP.Params, baseParam) - pattern := fmt.Sprintf("{%s}{%s}", parentPWP.Pattern, basePattern) + pattern := fmt.Sprintf("%s%s", parentPWP.Pattern, basePattern) pwps = append(pwps, PathWithParams{Pattern: pattern, Params: params}) } } @@ -296,6 +301,22 @@ func addMethodToPath(paths Paths, path, method string, methodInfo MethodInfo) { methods[method] = methodInfo } +// return the collection name of the resource, but deduplicate +// the name of the previous parent +// e.g: +// - book-editions becomes editions under the parent resource book. +func collectionName(r *parser.ParsedResource) string { + collectionName := r.Plural + if len(r.ParsedParents) > 0 { + parent := r.ParsedParents[0].Kind + // if collectionName has a prefix of parent, remove it + if strings.HasPrefix(collectionName, parent) { + collectionName = strings.TrimPrefix(collectionName, parent+"-") + } + } + return collectionName +} + type OpenAPI struct { Swagger string `json:"swagger"` Servers []Server `json:"servers,omitempty"` diff --git a/writer/proto/proto.go b/writer/proto/proto.go index 34b0908..c5f9283 100644 --- a/writer/proto/proto.go +++ b/writer/proto/proto.go @@ -21,6 +21,7 @@ import ( "sort" "strings" + "github.com/aep-dev/aepc/internal/utils" "github.com/aep-dev/aepc/parser" "github.com/jhump/protoreflect/desc/builder" "github.com/jhump/protoreflect/desc/protoprint" @@ -125,7 +126,7 @@ func toProtoServiceName(serviceName string) string { } func toMessageName(resource string) string { - return capitalizer.String(resource) + return utils.KebabToCamelCase(resource) } func getSortedResources(prsByString map[string]*parser.ParsedResource) []*parser.ParsedResource { diff --git a/writer/proto/resource.go b/writer/proto/resource.go index 4dae03e..ecb1da9 100644 --- a/writer/proto/resource.go +++ b/writer/proto/resource.go @@ -19,8 +19,10 @@ import ( "strings" "github.com/aep-dev/aepc/constants" + "github.com/aep-dev/aepc/internal/utils" "github.com/aep-dev/aepc/parser" "github.com/aep-dev/aepc/schema" + "github.com/aep-dev/aepc/writer/writer_utils" "github.com/jhump/protoreflect/desc" "github.com/jhump/protoreflect/desc/builder" "google.golang.org/genproto/googleapis/api/annotations" @@ -204,7 +206,7 @@ func GenerateMessage(properties []*parser.ParsedProperty, name string, s *parser // GenerateResourceMesssage adds the resource message. func GeneratedResourceMessage(r *parser.ParsedResource, s *parser.ParsedService, m *MessageStorage) (*builder.MessageBuilder, error) { - mb, err := GenerateMessage(r.GetPropertiesSortedByNumber(), r.Kind, s, m) + mb, err := GenerateMessage(r.GetPropertiesSortedByNumber(), toMessageName(r.Kind), s, m) if err != nil { return nil, err } @@ -215,7 +217,7 @@ func GeneratedResourceMessage(r *parser.ParsedResource, s *parser.ParsedService, 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 := builder.NewMessage("Create" + toMessageName(r.Kind) + "Request") mb.SetComments(builder.Comments{ LeadingComment: fmt.Sprintf("A Create request for a %v resource.", r.Kind), }) @@ -223,7 +225,7 @@ func AddCreate(r *parser.ParsedResource, resourceMb *builder.MessageBuilder, fb addIdField(r, mb) addResourceField(r, resourceMb, mb) fb.AddMessage(mb) - method := builder.NewMethod("Create"+r.Kind, + method := builder.NewMethod("Create"+toMessageName(r.Kind), builder.RpcTypeMessage(mb, false), builder.RpcTypeMessage(resourceMb, false), ) @@ -231,15 +233,15 @@ func AddCreate(r *parser.ParsedResource, resourceMb *builder.MessageBuilder, fb LeadingComment: fmt.Sprintf("An aep-compliant Create method for %v.", r.Kind), }) options := &descriptorpb.MethodOptions{} + bodyField := utils.KebabToSnakeCase(r.Kind) proto.SetExtension(options, annotations.E_Http, &annotations.HttpRule{ Pattern: &annotations.HttpRule_Post{ - // TODO(yft): switch this over to use "id" in the path. Post: generateParentHTTPPath(r), }, - Body: strings.ToLower(r.Kind), + Body: bodyField, }) proto.SetExtension(options, annotations.E_MethodSignature, []string{ - strings.Join([]string{constants.FIELD_PARENT_NAME, strings.ToLower(r.Kind)}, ","), + strings.Join([]string{constants.FIELD_PARENT_NAME, bodyField}, ","), }) method.SetOptions(options) sb.AddMethod(method) @@ -249,13 +251,13 @@ func AddCreate(r *parser.ParsedResource, resourceMb *builder.MessageBuilder, fb // AddGet adds a read method for the resource, along with // any required messages. func AddGet(r *parser.ParsedResource, resourceMb *builder.MessageBuilder, fb *builder.FileBuilder, sb *builder.ServiceBuilder) error { - mb := builder.NewMessage("Get" + r.Kind + "Request") + mb := builder.NewMessage("Get" + toMessageName(r.Kind) + "Request") mb.SetComments(builder.Comments{ LeadingComment: fmt.Sprintf("Request message for the Get%v method", r.Kind), }) addPathField(r, mb) fb.AddMessage(mb) - method := builder.NewMethod("Get"+r.Kind, + method := builder.NewMethod("Get"+toMessageName(r.Kind), builder.RpcTypeMessage(mb, false), builder.RpcTypeMessage(resourceMb, false), ) @@ -279,9 +281,9 @@ func AddGet(r *parser.ParsedResource, resourceMb *builder.MessageBuilder, fb *bu // 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 := builder.NewMessage("Update" + toMessageName(r.Kind) + "Request") mb.SetComments(builder.Comments{ - LeadingComment: fmt.Sprintf("Request message for the Update%v method", r.Kind), + LeadingComment: fmt.Sprintf("Request message for the Update%v method", toMessageName(r.Kind)), }) addPathField(r, mb) addResourceField(r, resourceMb, mb) @@ -295,7 +297,7 @@ func AddUpdate(r *parser.ParsedResource, resourceMb *builder.MessageBuilder, fb })) fb.AddMessage(mb) - method := builder.NewMethod("Update"+r.Kind, + method := builder.NewMethod("Update"+toMessageName(r.Kind), builder.RpcTypeMessage(mb, false), builder.RpcTypeMessage(resourceMb, false), ) @@ -303,14 +305,15 @@ func AddUpdate(r *parser.ParsedResource, resourceMb *builder.MessageBuilder, fb LeadingComment: fmt.Sprintf("An aep-compliant Update method for %v.", r.Kind), }) options := &descriptorpb.MethodOptions{} + body_field := utils.KebabToSnakeCase(r.Kind) proto.SetExtension(options, annotations.E_Http, &annotations.HttpRule{ Pattern: &annotations.HttpRule_Patch{ Patch: fmt.Sprintf("/{path=%v}", generateHTTPPath(r)), }, - Body: strings.ToLower(r.Kind), + Body: body_field, }) proto.SetExtension(options, annotations.E_MethodSignature, []string{ - strings.Join([]string{strings.ToLower(r.Kind), constants.FIELD_UPDATE_MASK_NAME}, ","), + strings.Join([]string{body_field, constants.FIELD_UPDATE_MASK_NAME}, ","), }) method.SetOptions(options) sb.AddMethod(method) @@ -320,9 +323,9 @@ func AddUpdate(r *parser.ParsedResource, resourceMb *builder.MessageBuilder, fb 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 := builder.NewMessage("Delete" + toMessageName(r.Kind) + "Request") mb.SetComments(builder.Comments{ - LeadingComment: fmt.Sprintf("Request message for the Delete%v method", r.Kind), + LeadingComment: fmt.Sprintf("Request message for the Delete%v method", toMessageName(r.Kind)), }) addPathField(r, mb) fb.AddMessage(mb) @@ -330,7 +333,7 @@ func AddDelete(r *parser.ParsedResource, resourceMb *builder.MessageBuilder, fb if err != nil { return err } - method := builder.NewMethod("Delete"+r.Kind, + method := builder.NewMethod("Delete"+toMessageName(r.Kind), builder.RpcTypeMessage(mb, false), builder.RpcTypeImportedMessage(emptyMd, false), ) @@ -354,7 +357,7 @@ func AddDelete(r *parser.ParsedResource, resourceMb *builder.MessageBuilder, fb 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 := builder.NewMessage("List" + toMessageName(r.Plural) + "Request") reqMb.SetComments(builder.Comments{ LeadingComment: fmt.Sprintf("Request message for the List%v method", r.Kind), }) @@ -366,14 +369,14 @@ func AddList(r *parser.ParsedResource, resourceMb *builder.MessageBuilder, fb *b LeadingComment: fmt.Sprintf("The maximum number of resources to return in a single page."), })) fb.AddMessage(reqMb) - respMb := builder.NewMessage("List" + r.Kind + "Response") + respMb := builder.NewMessage("List" + toMessageName(r.Plural) + "Response") respMb.SetComments(builder.Comments{ LeadingComment: fmt.Sprintf("Response message for the List%v method", r.Kind), }) addResourcesField(r, resourceMb, respMb) addNextPageToken(r, respMb) fb.AddMessage(respMb) - method := builder.NewMethod("List"+r.Kind, + method := builder.NewMethod("List"+toMessageName(r.Plural), builder.RpcTypeMessage(reqMb, false), builder.RpcTypeMessage(respMb, false), ) @@ -457,7 +460,7 @@ func AddApply(r *parser.ParsedResource, resourceMb *builder.MessageBuilder, fb * } func generateHTTPPath(r *parser.ParsedResource) string { - elements := []string{strings.ToLower(r.Plural)} + elements := []string{writer_utils.CollectionName(r)} if len(r.ParsedParents) > 0 { // TODO: handle multiple parents p := r.ParsedParents[0] @@ -466,6 +469,7 @@ func generateHTTPPath(r *parser.ParsedResource) string { if len(p.ParsedParents) == 0 { break } + p = p.ParsedParents[0] } } return fmt.Sprintf("%v/*", strings.Join(elements, "/*/")) @@ -479,7 +483,7 @@ func generateParentHTTPPath(r *parser.ParsedResource) string { if len(r.ParsedParents) > 0 { parentPath = fmt.Sprintf("%v", generateHTTPPath(r.ParsedParents[0])) } - return fmt.Sprintf("/{parent=%v}/%v", parentPath, strings.ToLower(r.Plural)) + return fmt.Sprintf("/{parent=%v}/%v", parentPath, writer_utils.CollectionName(r)) } func addParentField(r *parser.ParsedResource, mb *builder.MessageBuilder) { @@ -521,7 +525,7 @@ func addPathField(r *parser.ParsedResource, mb *builder.MessageBuilder) { func addResourceField(r *parser.ParsedResource, resourceMb, mb *builder.MessageBuilder) { o := &descriptorpb.FieldOptions{} proto.SetExtension(o, annotations.E_FieldBehavior, []annotations.FieldBehavior{annotations.FieldBehavior_REQUIRED}) - f := builder.NewField(strings.ToLower(r.Kind), builder.FieldTypeMessage(resourceMb)). + f := builder.NewField(utils.KebabToSnakeCase(r.Kind), builder.FieldTypeMessage(resourceMb)). SetNumber(constants.FIELD_RESOURCE_NUMBER). SetComments(builder.Comments{ LeadingComment: fmt.Sprintf("The resource to perform the operation on."), diff --git a/writer/writer_utils/utils.go b/writer/writer_utils/utils.go new file mode 100644 index 0000000..ce770f3 --- /dev/null +++ b/writer/writer_utils/utils.go @@ -0,0 +1,23 @@ +package writer_utils + +import ( + "strings" + + "github.com/aep-dev/aepc/parser" +) + +// return the collection name of the resource, but deduplicate +// the name of the previous parent +// e.g: +// - book-editions becomes editions under the parent resource book. +func CollectionName(r *parser.ParsedResource) string { + collectionName := r.Plural + if len(r.ParsedParents) > 0 { + parent := r.ParsedParents[0].Kind + // if collectionName has a prefix of parent, remove it + if strings.HasPrefix(collectionName, parent) { + collectionName = strings.TrimPrefix(collectionName, parent+"-") + } + } + return collectionName +}