diff --git a/.gitignore b/.gitignore index 6036ee6..1001142 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,6 @@ docs/.jekyll-metadata .ruby-version /.tmp/ + +# asdf +.tool-versions diff --git a/README.md b/README.md index 6c16170..c675018 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,14 @@ # Google API Linter [![ci](https://github.com/aep-dev/api-linter/actions/workflows/ci.yaml/badge.svg)](https://github.com/aep-dev/api-linter/actions/workflows/ci.yaml) -![latest release](https://img.shields.io/github/v/release/googleapis/api-linter) -![go version](https://img.shields.io/github/go-mod/go-version/googleapis/api-linter) +![latest release](https://img.shields.io/github/v/release/aep-dev/api-linter) +![go version](https://img.shields.io/github/go-mod/go-version/aep-dev/api-linter) -The API linter provides real-time checks for compliance with many of Google's -API standards, documented using [API Improvement Proposals][]. It operates on -API surfaces defined in [protocol buffers][]. +The API linter provides real-time checks for compliance with many of the API +standards, documented using [API Enhancement Proposals][]. It operates on API +surfaces defined in [protocol buffers][]. For APIs defined in +[OpenAPI specification][] an equivalent [OpenAPI specification linter][] is +available. It identifies common mistakes and inconsistencies in API surfaces: @@ -20,11 +22,11 @@ message GetBookRequest { When able, it also offers a suggestion for the correct fix. -[_Read more ≫_](https://linter.aip.dev/) +[_Read more ≫_](docs/index.md) ## Versioning -The Google API linter does **not** follow semantic versioning. Semantic +The AEP API linter does **not** follow semantic versioning. Semantic versioning is challenging for a tool like a linter because the addition or correction of virtually any rule is "breaking" (in the sense that a file that previously reported no problems may now do so). @@ -45,7 +47,7 @@ being useful. ## Contributing -If you are interested in contributing to the API linter, please review the [contributing guide](https://linter.aip.dev/contributing) to learn more. +If you are interested in contributing to the API linter, please review the [contributing guide](https://aep.dev/contributing/) to learn more. ## License @@ -54,4 +56,5 @@ This software is made available under the [Apache 2.0][] license. [apache 2.0]: https://www.apache.org/licenses/LICENSE-2.0 [api improvement proposals]: https://aip.dev/ [protocol buffers]: https://developers.google.com/protocol-buffers -[rule documentation]: ./rules/index.md +[OpenAPI specification]: https://www.openapis.org/ +[OpenAPI specification linter]: https://github.com/aep-dev/aep-openapi-linter diff --git a/docs/index.md b/docs/index.md index 6ecd968..9e829f9 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,15 +1,17 @@ --- --- -# Google API Linter +# AEP API Linter ![ci](https://github.com/aep-dev/api-linter/workflows/ci/badge.svg) ![latest release](https://img.shields.io/github/v/release/googleapis/api-linter) ![go version](https://img.shields.io/github/go-mod/go-version/googleapis/api-linter) -The API linter provides real-time checks for compliance with many of Google's -API standards, documented using [API Improvement Proposals][]. It operates on -API surfaces defined in [protocol buffers][]. +The API linter provides real-time checks for compliance with many of the API +standards, documented using [API Enhancement Proposals][]. It operates on API +surfaces defined in [protocol buffers][]. For APIs defined in +[OpenAPI specification][] an equivalent [OpenAPI specification linter][] is +available. It identifies common mistakes and inconsistencies in API surfaces: @@ -43,9 +45,6 @@ It will install `api-linter` into your local Go binary directory `$HOME/go/bin`. Ensure that your operating system's `PATH` contains the Go binary directory. -**Note:** For working in Google-internal source control, you should use the -released binary `/google/bin/releases/api-linter/api-linter`. - ## Usage ```sh @@ -80,12 +79,51 @@ Usage of api-linter: --version Print version and exit. ``` +### Usage with Buf + +[Buf][] builds tooling to make schema-driven, Protobuf-based API development +reliable and user-friendly for service producers and consumers. +This includes the `buf lint` command, which can be used to lint Protobuf files. +The API linter can be used as a plugin for `buf lint`. + +To install the plugin, run: + +```sh +go install github.com/aep-dev/api-linter/cmd/buf-plugin-aep@latest +``` + +It will install `buf-plugin-aep` into your local Go binary directory +`$HOME/go/bin`. Ensure that your operating system's `PATH` contains the Go +binary directory. + +Then, integrate the following into your `buf.yaml` file: + +```yaml +lint: + use: + - AEP +plugins: + - plugin: buf-plugin-aep +``` + +Now, you can run `buf lint` to lint your Protobuf files against the AEP rules. + +An example of building and linting with Buf can be found in the +[example](./example) directory. + +More information on using Buf to lint Protobuf files can be found in the +[Buf lint documentation][]. + ## License This software is made available under the [Apache 2.0][] license. [apache 2.0]: https://www.apache.org/licenses/LICENSE-2.0 -[api improvement proposals]: https://aep.dev/ +[API Enhancement Proposals]: https://aep.dev/ [configuration]: ./configuration.md [protocol buffers]: https://developers.google.com/protocol-buffers [rule documentation]: ./rules/index.md +[OpenAPI specification]: https://www.openapis.org/ +[OpenAPI specification linter]: https://github.com/aep-dev/aep-openapi-linter +[Buf]: https://buf.build/ +[Buf lint documentation]: https://buf.build/docs/lint/overview/ diff --git a/example/.gitignore b/example/.gitignore new file mode 100644 index 0000000..01715ea --- /dev/null +++ b/example/.gitignore @@ -0,0 +1,5 @@ +# Buf +buf.lock + +# Generated code +gen/ diff --git a/example/buf.gen.yaml b/example/buf.gen.yaml new file mode 100644 index 0000000..f9fd366 --- /dev/null +++ b/example/buf.gen.yaml @@ -0,0 +1,11 @@ +version: v2 +managed: + enabled: true +plugins: + - remote: buf.build/grpc/java:v1.69.0 + out: gen + # dependencies + - remote: buf.build/protocolbuffers/java:v29.1 + out: gen +inputs: + - directory: proto diff --git a/example/buf.yaml b/example/buf.yaml new file mode 100644 index 0000000..e3d4cd7 --- /dev/null +++ b/example/buf.yaml @@ -0,0 +1,12 @@ +# For details on buf.yaml configuration, visit https://buf.build/docs/configuration/v2/buf-yaml +version: v2 +modules: + - path: proto +deps: + - buf.build/googleapis/googleapis +lint: + use: + - AEP +plugins: + - plugin: buf-plugin-aep + diff --git a/example/proto/example/bookstore/v1/example.proto b/example/proto/example/bookstore/v1/example.proto new file mode 100644 index 0000000..cf4ec07 --- /dev/null +++ b/example/proto/example/bookstore/v1/example.proto @@ -0,0 +1,547 @@ +syntax = "proto3"; + +package example.bookstore.v1; + +import "google/api/annotations.proto"; + +import "google/api/client.proto"; + +import "google/api/field_behavior.proto"; + +import "google/api/resource.proto"; + +import "google/protobuf/empty.proto"; + +import "google/protobuf/field_mask.proto"; + +option go_package = "/bookstore"; + +// A service. +service Bookstore { + // An aep-compliant Create method for book. + rpc CreateBook ( CreateBookRequest ) returns ( Book ) { + option (google.api.http) = { + post: "/{parent=publishers/*}/books", + body: "book" + }; + + option (google.api.method_signature) = "parent,book"; + } + + // An aep-compliant Get method for 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 ) { + option (google.api.http) = { + patch: "/{path=publishers/*/books/*}", + body: "book" + }; + + option (google.api.method_signature) = "book,update_mask"; + } + + // An aep-compliant Delete method for book. + 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 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 ) { + 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 isbn. + rpc CreateIsbn ( CreateIsbnRequest ) returns ( Isbn ) { + option (google.api.http) = { post: "/{parent=isbns}", body: "isbn" }; + + option (google.api.method_signature) = "isbn"; + } + + // An aep-compliant Get method for isbn. + rpc GetIsbn ( GetIsbnRequest ) returns ( Isbn ) { + option (google.api.http) = { get: "/{path=isbns/*}" }; + + option (google.api.method_signature) = "path"; + } + + // An aep-compliant List method for isbns. + rpc ListIsbns ( ListIsbnsRequest ) returns ( ListIsbnsResponse ) { + option (google.api.http) = { get: "/{parent=isbns}" }; + + option (google.api.method_signature) = "parent"; + } + + // An aep-compliant Create method for publisher. + rpc CreatePublisher ( CreatePublisherRequest ) returns ( Publisher ) { + option (google.api.http) = { + post: "/{parent=publishers}", + body: "publisher" + }; + + option (google.api.method_signature) = "publisher"; + } + + // An aep-compliant Get method for 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 ) { + option (google.api.http) = { + patch: "/{path=publishers/*}", + body: "publisher" + }; + + option (google.api.method_signature) = "publisher,update_mask"; + } + + // An aep-compliant Delete method for publisher. + 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 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 ) { + option (google.api.http) = { put: "/{path=publishers/*}", body: "publisher" }; + } + + // An aep-compliant custom method Archive method for publisher. + rpc ArchivePublisher ( ArchivePublisherRequest ) returns ( ArchivePublisherResponse ) { + option (google.api.http) = { post: "/{path=publishers/*}:archive" }; + } +} + +// A Book. +message Book { + option (google.api.resource) = { + type: "bookstore.example.com/book", + pattern: [ "publishers/{publisher}/books/{book}" ], + plural: "books", + singular: "book" + }; + + // A Author. + message Author { + // Field for firstName. + string firstName = 1; + + // Field for lastName. + string lastName = 2; + } + + // Field for author. + repeated Author author = 5; + + // Field for isbn. + repeated string isbn = 1 [(google.api.field_behavior) = REQUIRED]; + + // Field for price. + float price = 2 [(google.api.field_behavior) = REQUIRED]; + + // Field for published. + bool published = 3 [(google.api.field_behavior) = REQUIRED]; + + // Field for edition. + int32 edition = 4; + + // Field for path. + string path = 10000; +} + +// A Create request for a book resource. +message CreateBookRequest { + // A field for the parent of book + 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. + Book book = 10015 [(google.api.field_behavior) = REQUIRED]; +} + +// Request message for the Getbook method +message GetBookRequest { + // The globally unique identifier for the resource + string path = 10018 [ + (google.api.field_behavior) = REQUIRED, + (google.api.resource_reference) = { type: "bookstore.example.com/book" } + ]; +} + +// Request message for the UpdateBook method +message UpdateBookRequest { + // The globally unique identifier for the resource + string path = 10018 [ + (google.api.field_behavior) = REQUIRED, + (google.api.resource_reference) = { type: "bookstore.example.com/book" } + ]; + + // The resource to perform the operation on. + 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 { + // The globally unique identifier for the resource + string path = 10018 [ + (google.api.field_behavior) = REQUIRED, + (google.api.resource_reference) = { type: "bookstore.example.com/book" } + ]; + + // If true, the resource will be deleted even if it still has children. + bool force = 10020; +} + +// Request message for the Listbook method +message ListBooksRequest { + // A field for the parent of book + 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 method +message ListBooksResponse { + // A list of books + repeated Book results = 10016; + + // The page token indicating the ending point of this response. + string next_page_token = 10011; +} + +// Request message for the Applybook method +message ApplyBookRequest { + // The globally unique identifier for the resource + string path = 10018 [ + (google.api.field_behavior) = REQUIRED, + (google.api.resource_reference) = { type: "bookstore.example.com/book" } + ]; + + // The resource to perform the operation on. + Book book = 10015 [(google.api.field_behavior) = REQUIRED]; +} + +// A BookEdition. +message BookEdition { + option (google.api.resource) = { + type: "bookstore.example.com/book-edition", + pattern: [ + "publishers/{publisher}/books/{book}/editions/{book-edition}" + ], + plural: "book-editions", + singular: "book-edition" + }; + + // Field for displayname. + string displayname = 1 [(google.api.field_behavior) = REQUIRED]; + + // Field for path. + string path = 10000; +} + +// 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 Isbn. +message Isbn { + option (google.api.resource) = { + type: "bookstore.example.com/isbn", + pattern: [ "isbns/{isbn}" ], + plural: "isbns", + singular: "isbn" + }; + + // Field for path. + string path = 10000; +} + +// A Create request for a isbn resource. +message CreateIsbnRequest { + // A field for the parent of isbn + string parent = 10013 [ + (google.api.field_behavior) = REQUIRED, + (google.api.resource_reference) = { } + ]; + + // The resource to perform the operation on. + Isbn isbn = 10015 [(google.api.field_behavior) = REQUIRED]; +} + +// Request message for the Getisbn method +message GetIsbnRequest { + // The globally unique identifier for the resource + string path = 10018 [ + (google.api.field_behavior) = REQUIRED, + (google.api.resource_reference) = { type: "bookstore.example.com/isbn" } + ]; +} + +// Request message for the Listisbn method +message ListIsbnsRequest { + // A field for the parent of isbn + 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 Listisbn method +message ListIsbnsResponse { + // A list of isbns + repeated Isbn results = 10016; + + // The page token indicating the ending point of this response. + string next_page_token = 10011; +} + +// A Publisher. +message Publisher { + option (google.api.resource) = { + type: "bookstore.example.com/publisher", + pattern: [ "publishers/{publisher}" ], + plural: "publishers", + singular: "publisher" + }; + + // Field for description. + string description = 1; + + // Field for path. + string path = 10000; +} + +// A Create request for a publisher resource. +message CreatePublisherRequest { + // A field for the parent of publisher + 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. + Publisher publisher = 10015 [(google.api.field_behavior) = REQUIRED]; +} + +// Request message for the Getpublisher method +message GetPublisherRequest { + // The globally unique identifier for the resource + string path = 10018 [ + (google.api.field_behavior) = REQUIRED, + (google.api.resource_reference) = { type: "bookstore.example.com/publisher" } + ]; +} + +// Request message for the UpdatePublisher method +message UpdatePublisherRequest { + // The globally unique identifier for the resource + string path = 10018 [ + (google.api.field_behavior) = REQUIRED, + (google.api.resource_reference) = { type: "bookstore.example.com/publisher" } + ]; + + // The resource to perform the operation on. + 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 { + // The globally unique identifier for the resource + string path = 10018 [ + (google.api.field_behavior) = REQUIRED, + (google.api.resource_reference) = { type: "bookstore.example.com/publisher" } + ]; + + // If true, the resource will be deleted even if it still has children. + bool force = 10020; +} + +// Request message for the Listpublisher method +message ListPublishersRequest { + // A field for the parent of publisher + 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 Listpublisher method +message ListPublishersResponse { + // A list of publishers + repeated Publisher results = 10016; + + // The page token indicating the ending point of this response. + string next_page_token = 10011; +} + +// Request message for the Applypublisher method +message ApplyPublisherRequest { + // The globally unique identifier for the resource + string path = 10018 [ + (google.api.field_behavior) = REQUIRED, + (google.api.resource_reference) = { type: "bookstore.example.com/publisher" } + ]; + + // The resource to perform the operation on. + Publisher publisher = 10015 [(google.api.field_behavior) = REQUIRED]; +} + +// Request message for the Archivepublisher method +message ArchivePublisherRequest { + // The globally unique identifier for the resource + string path = 10018 [ + (google.api.field_behavior) = REQUIRED, + (google.api.resource_reference) = { type: "bookstore.example.com/publisher" } + ]; +} + +// Response message for the Archivepublisher method +message ArchivePublisherResponse { +}