diff --git a/rules/aip0131/http_body.go b/rules/aip0131/http_body.go index 2eaa781db..d1f3e1751 100644 --- a/rules/aip0131/http_body.go +++ b/rules/aip0131/http_body.go @@ -17,24 +17,11 @@ package aip0131 import ( "github.com/googleapis/api-linter/lint" "github.com/googleapis/api-linter/rules/internal/utils" - "github.com/jhump/protoreflect/desc" ) // Get methods should not have an HTTP body. var httpBody = &lint.MethodRule{ - Name: lint.NewRuleName(131, "http-body"), - OnlyIf: isGetMethod, - LintMethod: func(m *desc.MethodDescriptor) []lint.Problem { - // Establish that the RPC has no HTTP body. - for _, httpRule := range utils.GetHTTPRules(m) { - if httpRule.Body != "" { - return []lint.Problem{{ - Message: "Get methods should not have an HTTP body.", - Descriptor: m, - }} - } - } - - return nil - }, + Name: lint.NewRuleName(131, "http-body"), + OnlyIf: isGetMethod, + LintMethod: utils.LintNoHTTPBody, } diff --git a/rules/aip0131/http_method.go b/rules/aip0131/http_method.go index 5da9fdcec..83684caad 100644 --- a/rules/aip0131/http_method.go +++ b/rules/aip0131/http_method.go @@ -17,24 +17,11 @@ package aip0131 import ( "github.com/googleapis/api-linter/lint" "github.com/googleapis/api-linter/rules/internal/utils" - "github.com/jhump/protoreflect/desc" ) // Get methods should use the HTTP GET verb. var httpMethod = &lint.MethodRule{ - Name: lint.NewRuleName(131, "http-method"), - OnlyIf: isGetMethod, - LintMethod: func(m *desc.MethodDescriptor) []lint.Problem { - // Rule check: Establish that the RPC uses HTTP GET. - for _, httpRule := range utils.GetHTTPRules(m) { - if httpRule.Method != "GET" { - return []lint.Problem{{ - Message: "Get methods must use the HTTP GET verb.", - Descriptor: m, - }} - } - } - - return nil - }, + Name: lint.NewRuleName(131, "http-method"), + OnlyIf: isGetMethod, + LintMethod: utils.LintHTTPMethod("GET"), } diff --git a/rules/aip0131/request_name_behavior.go b/rules/aip0131/request_name_behavior.go index 392ad8f5f..160b13675 100644 --- a/rules/aip0131/request_name_behavior.go +++ b/rules/aip0131/request_name_behavior.go @@ -25,13 +25,5 @@ var requestNameBehavior = &lint.FieldRule{ OnlyIf: func(f *desc.FieldDescriptor) bool { return isGetRequestMessage(f.GetOwner()) && f.GetName() == "name" }, - LintField: func(f *desc.FieldDescriptor) []lint.Problem { - if !utils.GetFieldBehavior(f).Contains("REQUIRED") { - return []lint.Problem{{ - Message: "Get requests: The `name` field should include `(google.api.field_behavior) = REQUIRED`.", - Descriptor: f, - }} - } - return nil - }, + LintField: utils.LintRequiredField, } diff --git a/rules/aip0131/request_name_reference.go b/rules/aip0131/request_name_reference.go index 20add0c76..4826dbd57 100644 --- a/rules/aip0131/request_name_reference.go +++ b/rules/aip0131/request_name_reference.go @@ -25,13 +25,5 @@ var requestNameReference = &lint.FieldRule{ OnlyIf: func(f *desc.FieldDescriptor) bool { return isGetRequestMessage(f.GetOwner()) && f.GetName() == "name" }, - LintField: func(f *desc.FieldDescriptor) []lint.Problem { - if ref := utils.GetResourceReference(f); ref == nil { - return []lint.Problem{{ - Message: "Get methods: The `name` field should include a `google.api.resource_reference` annotation.", - Descriptor: f, - }} - } - return nil - }, + LintField: utils.LintFieldResourceReference, } diff --git a/rules/aip0132/request_parent_behavior.go b/rules/aip0132/request_parent_behavior.go index d1fe3b9e2..9a4731dfb 100644 --- a/rules/aip0132/request_parent_behavior.go +++ b/rules/aip0132/request_parent_behavior.go @@ -25,13 +25,5 @@ var requestParentBehavior = &lint.FieldRule{ OnlyIf: func(f *desc.FieldDescriptor) bool { return isListRequestMessage(f.GetOwner()) && f.GetName() == "parent" }, - LintField: func(f *desc.FieldDescriptor) []lint.Problem { - if !utils.GetFieldBehavior(f).Contains("REQUIRED") { - return []lint.Problem{{ - Message: "List requests: The `parent` field should include `(google.api.field_behavior) = REQUIRED`.", - Descriptor: f, - }} - } - return nil - }, + LintField: utils.LintRequiredField, } diff --git a/rules/aip0132/request_parent_field.go b/rules/aip0132/request_parent_field.go index 358b55ece..0b2131f18 100644 --- a/rules/aip0132/request_parent_field.go +++ b/rules/aip0132/request_parent_field.go @@ -16,9 +16,8 @@ package aip0132 import ( "github.com/googleapis/api-linter/lint" - "github.com/googleapis/api-linter/locations" + "github.com/googleapis/api-linter/rules/internal/utils" "github.com/jhump/protoreflect/desc" - "github.com/jhump/protoreflect/desc/builder" ) // The type of the parent field in the List request message should @@ -28,16 +27,5 @@ var requestParentField = &lint.FieldRule{ OnlyIf: func(f *desc.FieldDescriptor) bool { return isListRequestMessage(f.GetOwner()) && f.GetName() == "parent" }, - LintField: func(f *desc.FieldDescriptor) []lint.Problem { - if f.GetType() != builder.FieldTypeString().GetType() { - return []lint.Problem{{ - Message: "`parent` field on List RPCs must be a string", - Suggestion: "string", - Descriptor: f, - Location: locations.FieldType(f), - }} - } - - return nil - }, + LintField: utils.LintStringField, } diff --git a/rules/aip0132/request_parent_reference.go b/rules/aip0132/request_parent_reference.go index f73f5c911..ec9c511ce 100644 --- a/rules/aip0132/request_parent_reference.go +++ b/rules/aip0132/request_parent_reference.go @@ -25,13 +25,5 @@ var requestParentReference = &lint.FieldRule{ OnlyIf: func(f *desc.FieldDescriptor) bool { return isListRequestMessage(f.GetOwner()) && f.GetName() == "parent" }, - LintField: func(f *desc.FieldDescriptor) []lint.Problem { - if ref := utils.GetResourceReference(f); ref == nil { - return []lint.Problem{{ - Message: "List methods: The `parent` field should include a `google.api.resource_reference` annotation.", - Descriptor: f, - }} - } - return nil - }, + LintField: utils.LintFieldResourceReference, } diff --git a/rules/aip0133/http_method.go b/rules/aip0133/http_method.go index 28e4de312..e11bbe488 100644 --- a/rules/aip0133/http_method.go +++ b/rules/aip0133/http_method.go @@ -17,24 +17,11 @@ package aip0133 import ( "github.com/googleapis/api-linter/lint" "github.com/googleapis/api-linter/rules/internal/utils" - "github.com/jhump/protoreflect/desc" ) // Create methods should use the HTTP POST verb. var httpMethod = &lint.MethodRule{ - Name: lint.NewRuleName(133, "http-method"), - OnlyIf: isCreateMethod, - LintMethod: func(m *desc.MethodDescriptor) []lint.Problem { - // Rule check: Establish that the RPC uses HTTP POST. - for _, httpRule := range utils.GetHTTPRules(m) { - if httpRule.Method != "POST" { - return []lint.Problem{{ - Message: "Create methods must use the HTTP POST verb.", - Descriptor: m, - }} - } - } - - return nil - }, + Name: lint.NewRuleName(133, "http-method"), + OnlyIf: isCreateMethod, + LintMethod: utils.LintHTTPMethod("POST"), } diff --git a/rules/aip0133/request_parent_behavior.go b/rules/aip0133/request_parent_behavior.go index 4319fd8a2..6368cd50e 100644 --- a/rules/aip0133/request_parent_behavior.go +++ b/rules/aip0133/request_parent_behavior.go @@ -25,13 +25,5 @@ var requestParentBehavior = &lint.FieldRule{ OnlyIf: func(f *desc.FieldDescriptor) bool { return isCreateRequestMessage(f.GetOwner()) && f.GetName() == "parent" }, - LintField: func(f *desc.FieldDescriptor) []lint.Problem { - if !utils.GetFieldBehavior(f).Contains("REQUIRED") { - return []lint.Problem{{ - Message: "Create requests: The `parent` field should include `(google.api.field_behavior) = REQUIRED`.", - Descriptor: f, - }} - } - return nil - }, + LintField: utils.LintRequiredField, } diff --git a/rules/aip0133/request_parent_field.go b/rules/aip0133/request_parent_field.go index dc8781851..b2fe0bed7 100644 --- a/rules/aip0133/request_parent_field.go +++ b/rules/aip0133/request_parent_field.go @@ -16,9 +16,8 @@ package aip0133 import ( "github.com/googleapis/api-linter/lint" - "github.com/googleapis/api-linter/locations" + "github.com/googleapis/api-linter/rules/internal/utils" "github.com/jhump/protoreflect/desc" - "github.com/jhump/protoreflect/desc/builder" ) // The type of the parent field in a creat request should be string. @@ -27,16 +26,5 @@ var requestParentField = &lint.FieldRule{ OnlyIf: func(f *desc.FieldDescriptor) bool { return isCreateRequestMessage(f.GetOwner()) && f.GetName() == "parent" }, - LintField: func(f *desc.FieldDescriptor) []lint.Problem { - if f.GetType() != builder.FieldTypeString().GetType() { - return []lint.Problem{{ - Message: "`parent` field on create request message must be a string", - Suggestion: "string", - Descriptor: f, - Location: locations.FieldType(f), - }} - } - - return nil - }, + LintField: utils.LintStringField, } diff --git a/rules/aip0133/request_parent_reference.go b/rules/aip0133/request_parent_reference.go index 41d436f24..c966baafb 100644 --- a/rules/aip0133/request_parent_reference.go +++ b/rules/aip0133/request_parent_reference.go @@ -25,13 +25,5 @@ var requestParentReference = &lint.FieldRule{ OnlyIf: func(f *desc.FieldDescriptor) bool { return isCreateRequestMessage(f.GetOwner()) && f.GetName() == "parent" }, - LintField: func(f *desc.FieldDescriptor) []lint.Problem { - if ref := utils.GetResourceReference(f); ref == nil { - return []lint.Problem{{ - Message: "Create methods: The `parent` field should include a `google.api.resource_reference` annotation.", - Descriptor: f, - }} - } - return nil - }, + LintField: utils.LintFieldResourceReference, } diff --git a/rules/aip0133/request_resource_behavior.go b/rules/aip0133/request_resource_behavior.go index 9691a3279..b49db303f 100644 --- a/rules/aip0133/request_resource_behavior.go +++ b/rules/aip0133/request_resource_behavior.go @@ -15,8 +15,6 @@ package aip0133 import ( - "fmt" - "github.com/googleapis/api-linter/lint" "github.com/googleapis/api-linter/rules/internal/utils" "github.com/jhump/protoreflect/desc" @@ -35,16 +33,5 @@ var requestResourceBehavior = &lint.FieldRule{ } return false }, - LintField: func(f *desc.FieldDescriptor) []lint.Problem { - if !utils.GetFieldBehavior(f).Contains("REQUIRED") { - return []lint.Problem{{ - Message: fmt.Sprintf( - "Create requests: The `%s` field should include `(google.api.field_behavior) = REQUIRED`.", - f.GetName(), - ), - Descriptor: f, - }} - } - return nil - }, + LintField: utils.LintRequiredField, } diff --git a/rules/aip0134/http_method.go b/rules/aip0134/http_method.go index de74f3510..cb68ddebe 100644 --- a/rules/aip0134/http_method.go +++ b/rules/aip0134/http_method.go @@ -17,24 +17,11 @@ package aip0134 import ( "github.com/googleapis/api-linter/lint" "github.com/googleapis/api-linter/rules/internal/utils" - "github.com/jhump/protoreflect/desc" ) // Update methods should use the HTTP PATCH verb. var httpMethod = &lint.MethodRule{ - Name: lint.NewRuleName(134, "http-method"), - OnlyIf: isUpdateMethod, - LintMethod: func(m *desc.MethodDescriptor) []lint.Problem { - // Rule check: Establish that the RPC uses HTTP PATCH. - for _, httpRule := range utils.GetHTTPRules(m) { - if httpRule.Method != "PATCH" { - return []lint.Problem{{ - Message: "Update methods must use the HTTP PATCH verb.", - Descriptor: m, - }} - } - } - - return nil - }, + Name: lint.NewRuleName(134, "http-method"), + OnlyIf: isUpdateMethod, + LintMethod: utils.LintHTTPMethod("PATCH"), } diff --git a/rules/aip0135/http_body.go b/rules/aip0135/http_body.go index 1541c3310..eaba7bb9d 100644 --- a/rules/aip0135/http_body.go +++ b/rules/aip0135/http_body.go @@ -17,24 +17,11 @@ package aip0135 import ( "github.com/googleapis/api-linter/lint" "github.com/googleapis/api-linter/rules/internal/utils" - "github.com/jhump/protoreflect/desc" ) // Delete methods should not have an HTTP body. var httpBody = &lint.MethodRule{ - Name: lint.NewRuleName(135, "http-body"), - OnlyIf: isDeleteMethod, - LintMethod: func(m *desc.MethodDescriptor) []lint.Problem { - // Establish that the RPC has no HTTP body. - for _, httpRule := range utils.GetHTTPRules(m) { - if httpRule.Body != "" { - return []lint.Problem{{ - Message: "Delete methods should not have an HTTP body.", - Descriptor: m, - }} - } - } - - return nil - }, + Name: lint.NewRuleName(135, "http-body"), + OnlyIf: isDeleteMethod, + LintMethod: utils.LintNoHTTPBody, } diff --git a/rules/aip0135/http_method.go b/rules/aip0135/http_method.go index d5a07aa10..36f41f9d7 100644 --- a/rules/aip0135/http_method.go +++ b/rules/aip0135/http_method.go @@ -17,24 +17,11 @@ package aip0135 import ( "github.com/googleapis/api-linter/lint" "github.com/googleapis/api-linter/rules/internal/utils" - "github.com/jhump/protoreflect/desc" ) // Delete methods should use the HTTP DELETE method. var httpMethod = &lint.MethodRule{ - Name: lint.NewRuleName(135, "http-method"), - OnlyIf: isDeleteMethod, - LintMethod: func(m *desc.MethodDescriptor) []lint.Problem { - // Rule check: Establish that the RPC uses HTTP DELETE. - for _, httpRule := range utils.GetHTTPRules(m) { - if httpRule.Method != "DELETE" { - return []lint.Problem{{ - Message: "Delete methods must use the HTTP DELETE verb.", - Descriptor: m, - }} - } - } - - return nil - }, + Name: lint.NewRuleName(135, "http-method"), + OnlyIf: isDeleteMethod, + LintMethod: utils.LintHTTPMethod("DELETE"), } diff --git a/rules/aip0135/request_name_behavior.go b/rules/aip0135/request_name_behavior.go index cff9ff65b..c642695e4 100644 --- a/rules/aip0135/request_name_behavior.go +++ b/rules/aip0135/request_name_behavior.go @@ -25,13 +25,5 @@ var requestNameBehavior = &lint.FieldRule{ OnlyIf: func(f *desc.FieldDescriptor) bool { return isDeleteRequestMessage(f.GetOwner()) && f.GetName() == "name" }, - LintField: func(f *desc.FieldDescriptor) []lint.Problem { - if !utils.GetFieldBehavior(f).Contains("REQUIRED") { - return []lint.Problem{{ - Message: "Delete requests: The `name` field should include `(google.api.field_behavior) = REQUIRED`.", - Descriptor: f, - }} - } - return nil - }, + LintField: utils.LintRequiredField, } diff --git a/rules/aip0135/request_name_reference.go b/rules/aip0135/request_name_reference.go index a1a83e1d1..9b7cc8c9c 100644 --- a/rules/aip0135/request_name_reference.go +++ b/rules/aip0135/request_name_reference.go @@ -25,13 +25,5 @@ var requestNameReference = &lint.FieldRule{ OnlyIf: func(f *desc.FieldDescriptor) bool { return isDeleteRequestMessage(f.GetOwner()) && f.GetName() == "name" }, - LintField: func(f *desc.FieldDescriptor) []lint.Problem { - if ref := utils.GetResourceReference(f); ref == nil { - return []lint.Problem{{ - Message: "Delete methods: The `name` field should include a `google.api.resource_reference` annotation.", - Descriptor: f, - }} - } - return nil - }, + LintField: utils.LintFieldResourceReference, } diff --git a/rules/aip0164/http_body.go b/rules/aip0164/http_body.go index b7313c8f0..d4b172493 100644 --- a/rules/aip0164/http_body.go +++ b/rules/aip0164/http_body.go @@ -17,24 +17,11 @@ package aip0164 import ( "github.com/googleapis/api-linter/lint" "github.com/googleapis/api-linter/rules/internal/utils" - "github.com/jhump/protoreflect/desc" ) -// Undelete methods should not have an HTTP body. +// Undelete methods should have "*" as the HTTP body. var httpBody = &lint.MethodRule{ - Name: lint.NewRuleName(164, "http-body"), - OnlyIf: isUndeleteMethod, - LintMethod: func(m *desc.MethodDescriptor) []lint.Problem { - // Establish that the RPC has correct HTTP body. - for _, httpRule := range utils.GetHTTPRules(m) { - if httpRule.Body != "*" { - return []lint.Problem{{ - Message: `Undelete methods should use "*" as the HTTP body.`, - Descriptor: m, - }} - } - } - - return nil - }, + Name: lint.NewRuleName(164, "http-body"), + OnlyIf: isUndeleteMethod, + LintMethod: utils.LintWildcardHTTPBody, } diff --git a/rules/aip0164/http_method.go b/rules/aip0164/http_method.go index 362da1bfe..ed1c50c07 100644 --- a/rules/aip0164/http_method.go +++ b/rules/aip0164/http_method.go @@ -17,24 +17,11 @@ package aip0164 import ( "github.com/googleapis/api-linter/lint" "github.com/googleapis/api-linter/rules/internal/utils" - "github.com/jhump/protoreflect/desc" ) // Undelete methods should use the HTTP POST method. var httpMethod = &lint.MethodRule{ - Name: lint.NewRuleName(164, "http-method"), - OnlyIf: isUndeleteMethod, - LintMethod: func(m *desc.MethodDescriptor) []lint.Problem { - // Rule check: Establish that the RPC uses HTTP POST. - for _, httpRule := range utils.GetHTTPRules(m) { - if httpRule.Method != "POST" { - return []lint.Problem{{ - Message: "Undelete methods must use the HTTP POST verb.", - Descriptor: m, - }} - } - } - - return nil - }, + Name: lint.NewRuleName(164, "http-method"), + OnlyIf: isUndeleteMethod, + LintMethod: utils.LintHTTPMethod("POST"), } diff --git a/rules/aip0164/request_name_behavior.go b/rules/aip0164/request_name_behavior.go index e07b3e363..69bfaba2c 100644 --- a/rules/aip0164/request_name_behavior.go +++ b/rules/aip0164/request_name_behavior.go @@ -25,13 +25,5 @@ var requestNameBehavior = &lint.FieldRule{ OnlyIf: func(f *desc.FieldDescriptor) bool { return isUndeleteRequestMessage(f.GetOwner()) && f.GetName() == "name" }, - LintField: func(f *desc.FieldDescriptor) []lint.Problem { - if !utils.GetFieldBehavior(f).Contains("REQUIRED") { - return []lint.Problem{{ - Message: "Undelete requests: The `name` field should include `(google.api.field_behavior) = REQUIRED`.", - Descriptor: f, - }} - } - return nil - }, + LintField: utils.LintRequiredField, } diff --git a/rules/aip0164/request_name_reference.go b/rules/aip0164/request_name_reference.go index df51ad9a7..bdfe438f4 100644 --- a/rules/aip0164/request_name_reference.go +++ b/rules/aip0164/request_name_reference.go @@ -25,13 +25,5 @@ var requestNameReference = &lint.FieldRule{ OnlyIf: func(f *desc.FieldDescriptor) bool { return isUndeleteRequestMessage(f.GetOwner()) && f.GetName() == "name" }, - LintField: func(f *desc.FieldDescriptor) []lint.Problem { - if ref := utils.GetResourceReference(f); ref == nil { - return []lint.Problem{{ - Message: "Undelete methods: The `name` field should include a `google.api.resource_reference` annotation.", - Descriptor: f, - }} - } - return nil - }, + LintField: utils.LintFieldResourceReference, } diff --git a/rules/aip0231/http_body.go b/rules/aip0231/http_body.go index fe1cb2f5c..6083b3408 100644 --- a/rules/aip0231/http_body.go +++ b/rules/aip0231/http_body.go @@ -17,24 +17,11 @@ package aip0231 import ( "github.com/googleapis/api-linter/lint" "github.com/googleapis/api-linter/rules/internal/utils" - "github.com/jhump/protoreflect/desc" ) -// Get methods should not have an HTTP body. +// Batch Get methods should not have an HTTP body. var httpBody = &lint.MethodRule{ - Name: lint.NewRuleName(231, "http-body"), - OnlyIf: isBatchGetMethod, - LintMethod: func(m *desc.MethodDescriptor) []lint.Problem { - // Establish that the RPC has no HTTP body. - for _, httpRule := range utils.GetHTTPRules(m) { - if httpRule.Body != "" { - return []lint.Problem{{ - Message: "Batch Get methods should not have an HTTP body.", - Descriptor: m, - }} - } - } - - return nil - }, + Name: lint.NewRuleName(231, "http-body"), + OnlyIf: isBatchGetMethod, + LintMethod: utils.LintNoHTTPBody, } diff --git a/rules/aip0231/http_body_test.go b/rules/aip0231/http_body_test.go index deaa56019..0fe793cb9 100644 --- a/rules/aip0231/http_body_test.go +++ b/rules/aip0231/http_body_test.go @@ -28,7 +28,7 @@ func TestHttpBody(t *testing.T) { problems testutils.Problems }{ {"Valid", "", "BatchGetBooks", nil}, - {"Invalid", "*", "BatchGetBooks", testutils.Problems{{Message: "Batch Get methods should not have an HTTP body."}}}, + {"Invalid", "*", "BatchGetBooks", testutils.Problems{{Message: "HTTP body"}}}, {"Irrelevant", "*", "AcquireBook", nil}, } diff --git a/rules/aip0231/http_method.go b/rules/aip0231/http_method.go index 3c327c9bc..bb837dd62 100644 --- a/rules/aip0231/http_method.go +++ b/rules/aip0231/http_method.go @@ -17,24 +17,11 @@ package aip0231 import ( "github.com/googleapis/api-linter/lint" "github.com/googleapis/api-linter/rules/internal/utils" - "github.com/jhump/protoreflect/desc" ) // Batch Get methods should use the HTTP GET verb. var httpVerb = &lint.MethodRule{ - Name: lint.NewRuleName(231, "http-method"), - OnlyIf: isBatchGetMethod, - LintMethod: func(m *desc.MethodDescriptor) []lint.Problem { - // Rule check: Establish that the RPC uses HTTP GET. - for _, httpRule := range utils.GetHTTPRules(m) { - if httpRule.Method != "GET" { - return []lint.Problem{{ - Message: "Batch Get methods must use the HTTP GET verb.", - Descriptor: m, - }} - } - } - - return nil - }, + Name: lint.NewRuleName(231, "http-method"), + OnlyIf: isBatchGetMethod, + LintMethod: utils.LintHTTPMethod("GET"), } diff --git a/rules/aip0231/http_method_test.go b/rules/aip0231/http_method_test.go index 0aec9e99f..27420aaa0 100644 --- a/rules/aip0231/http_method_test.go +++ b/rules/aip0231/http_method_test.go @@ -29,7 +29,7 @@ func TestHttpVerb(t *testing.T) { problems testutils.Problems }{ {"Valid", "get", "BatchGetBooks", nil}, - {"Invalid", "post", "BatchGetBooks", testutils.Problems{{Message: "Batch Get methods must use the HTTP GET verb."}}}, + {"Invalid", "post", "BatchGetBooks", testutils.Problems{{Message: "HTTP GET"}}}, {"Irrelevant", "post", "AcquireBook", nil}, } diff --git a/rules/aip0231/request_names_behavior.go b/rules/aip0231/request_names_behavior.go index f73d7d50d..6210af618 100644 --- a/rules/aip0231/request_names_behavior.go +++ b/rules/aip0231/request_names_behavior.go @@ -25,13 +25,5 @@ var requestNamesBehavior = &lint.FieldRule{ OnlyIf: func(f *desc.FieldDescriptor) bool { return isBatchGetRequestMessage(f.GetOwner()) && f.GetName() == "names" }, - LintField: func(f *desc.FieldDescriptor) []lint.Problem { - if !utils.GetFieldBehavior(f).Contains("REQUIRED") { - return []lint.Problem{{ - Message: "Batch Get requests: The `names` field should include `(google.api.field_behavior) = REQUIRED`.", - Descriptor: f, - }} - } - return nil - }, + LintField: utils.LintRequiredField, } diff --git a/rules/aip0231/request_names_reference.go b/rules/aip0231/request_names_reference.go index 5f197d26f..da0c426ae 100644 --- a/rules/aip0231/request_names_reference.go +++ b/rules/aip0231/request_names_reference.go @@ -25,13 +25,5 @@ var requestNamesReference = &lint.FieldRule{ OnlyIf: func(f *desc.FieldDescriptor) bool { return isBatchGetRequestMessage(f.GetOwner()) && f.GetName() == "names" }, - LintField: func(f *desc.FieldDescriptor) []lint.Problem { - if ref := utils.GetResourceReference(f); ref == nil { - return []lint.Problem{{ - Message: "Batch Get methods: The `names` field should include a `google.api.resource_reference` annotation.", - Descriptor: f, - }} - } - return nil - }, + LintField: utils.LintFieldResourceReference, } diff --git a/rules/aip0231/request_parent_reference.go b/rules/aip0231/request_parent_reference.go index 79101a68f..d6db396c1 100644 --- a/rules/aip0231/request_parent_reference.go +++ b/rules/aip0231/request_parent_reference.go @@ -25,13 +25,5 @@ var requestParentReference = &lint.FieldRule{ OnlyIf: func(f *desc.FieldDescriptor) bool { return isBatchGetRequestMessage(f.GetOwner()) && f.GetName() == "parent" }, - LintField: func(f *desc.FieldDescriptor) []lint.Problem { - if ref := utils.GetResourceReference(f); ref == nil { - return []lint.Problem{{ - Message: "Batch Get methods: The `parent` field should include a `google.api.resource_reference` annotation.", - Descriptor: f, - }} - } - return nil - }, + LintField: utils.LintFieldResourceReference, } diff --git a/rules/aip0233/http_body.go b/rules/aip0233/http_body.go index 69e1f5bb5..e4ddc5900 100644 --- a/rules/aip0233/http_body.go +++ b/rules/aip0233/http_body.go @@ -17,24 +17,11 @@ package aip0233 import ( "github.com/googleapis/api-linter/lint" "github.com/googleapis/api-linter/rules/internal/utils" - "github.com/jhump/protoreflect/desc" ) -// Batch create methods should use "*" as the HTTP body. +// Batch Create methods should use "*" as the HTTP body. var httpBody = &lint.MethodRule{ - Name: lint.NewRuleName(233, "http-body"), - OnlyIf: isBatchCreateMethod, - LintMethod: func(m *desc.MethodDescriptor) []lint.Problem { - // Establish that the RPC has correct HTTP body. - for _, httpRule := range utils.GetHTTPRules(m) { - if httpRule.Body != "*" { - return []lint.Problem{{ - Message: `Batch Create methods should use "*" as the HTTP body.`, - Descriptor: m, - }} - } - } - - return nil - }, + Name: lint.NewRuleName(233, "http-body"), + OnlyIf: isBatchCreateMethod, + LintMethod: utils.LintWildcardHTTPBody, } diff --git a/rules/aip0233/http_method.go b/rules/aip0233/http_method.go index cd80a98b6..00b1328f4 100644 --- a/rules/aip0233/http_method.go +++ b/rules/aip0233/http_method.go @@ -17,24 +17,11 @@ package aip0233 import ( "github.com/googleapis/api-linter/lint" "github.com/googleapis/api-linter/rules/internal/utils" - "github.com/jhump/protoreflect/desc" ) // Batch Create methods should use the HTTP POST verb. var httpVerb = &lint.MethodRule{ - Name: lint.NewRuleName(233, "http-method"), - OnlyIf: isBatchCreateMethod, - LintMethod: func(m *desc.MethodDescriptor) []lint.Problem { - // Rule check: Establish that the RPC uses HTTP POST. - for _, httpRule := range utils.GetHTTPRules(m) { - if httpRule.Method != "POST" { - return []lint.Problem{{ - Message: "Batch Create methods must use the HTTP POST verb.", - Descriptor: m, - }} - } - } - - return nil - }, + Name: lint.NewRuleName(233, "http-method"), + OnlyIf: isBatchCreateMethod, + LintMethod: utils.LintHTTPMethod("POST"), } diff --git a/rules/aip0233/request_parent_reference.go b/rules/aip0233/request_parent_reference.go index d3a016a7e..eb0468a37 100644 --- a/rules/aip0233/request_parent_reference.go +++ b/rules/aip0233/request_parent_reference.go @@ -25,13 +25,5 @@ var requestParentReference = &lint.FieldRule{ OnlyIf: func(f *desc.FieldDescriptor) bool { return isBatchCreateRequestMessage(f.GetOwner()) && f.GetName() == "parent" }, - LintField: func(f *desc.FieldDescriptor) []lint.Problem { - if ref := utils.GetResourceReference(f); ref == nil { - return []lint.Problem{{ - Message: "Batch Create methods: The `parent` field should include a `google.api.resource_reference` annotation.", - Descriptor: f, - }} - } - return nil - }, + LintField: utils.LintFieldResourceReference, } diff --git a/rules/aip0233/request_requests_behavior.go b/rules/aip0233/request_requests_behavior.go index 639e4edd6..8061b2f32 100644 --- a/rules/aip0233/request_requests_behavior.go +++ b/rules/aip0233/request_requests_behavior.go @@ -25,13 +25,5 @@ var requestRequestsBehavior = &lint.FieldRule{ OnlyIf: func(f *desc.FieldDescriptor) bool { return isBatchCreateRequestMessage(f.GetOwner()) && f.GetName() == "requests" }, - LintField: func(f *desc.FieldDescriptor) []lint.Problem { - if !utils.GetFieldBehavior(f).Contains("REQUIRED") { - return []lint.Problem{{ - Message: "Batch Create requests: The `requests` field should include `(google.api.field_behavior) = REQUIRED`.", - Descriptor: f, - }} - } - return nil - }, + LintField: utils.LintRequiredField, } diff --git a/rules/aip0234/http_body.go b/rules/aip0234/http_body.go index 6bffe130c..cc79f6ffe 100644 --- a/rules/aip0234/http_body.go +++ b/rules/aip0234/http_body.go @@ -17,24 +17,11 @@ package aip0234 import ( "github.com/googleapis/api-linter/lint" "github.com/googleapis/api-linter/rules/internal/utils" - "github.com/jhump/protoreflect/desc" ) // Batch Update methods should use "*" as the HTTP body. var httpBody = &lint.MethodRule{ - Name: lint.NewRuleName(234, "http-body"), - OnlyIf: isBatchUpdateMethod, - LintMethod: func(m *desc.MethodDescriptor) []lint.Problem { - // Establish that the RPC has correct HTTP body. - for _, httpRule := range utils.GetHTTPRules(m) { - if httpRule.Body != "*" { - return []lint.Problem{{ - Message: `Batch Update methods should use "*" as the HTTP body.`, - Descriptor: m, - }} - } - } - - return nil - }, + Name: lint.NewRuleName(234, "http-body"), + OnlyIf: isBatchUpdateMethod, + LintMethod: utils.LintWildcardHTTPBody, } diff --git a/rules/aip0234/http_method.go b/rules/aip0234/http_method.go index 1428bb98a..1e0acc220 100644 --- a/rules/aip0234/http_method.go +++ b/rules/aip0234/http_method.go @@ -17,24 +17,11 @@ package aip0234 import ( "github.com/googleapis/api-linter/lint" "github.com/googleapis/api-linter/rules/internal/utils" - "github.com/jhump/protoreflect/desc" ) // Batch Create methods should use the HTTP POST verb. var httpMethod = &lint.MethodRule{ - Name: lint.NewRuleName(234, "http-method"), - OnlyIf: isBatchUpdateMethod, - LintMethod: func(m *desc.MethodDescriptor) []lint.Problem { - // Rule check: Establish that the RPC uses HTTP POST. - for _, httpRule := range utils.GetHTTPRules(m) { - if httpRule.Method != "POST" { - return []lint.Problem{{ - Message: "Batch Update methods must use the HTTP POST verb.", - Descriptor: m, - }} - } - } - - return nil - }, + Name: lint.NewRuleName(234, "http-method"), + OnlyIf: isBatchUpdateMethod, + LintMethod: utils.LintHTTPMethod("POST"), } diff --git a/rules/aip0234/request_parent_reference.go b/rules/aip0234/request_parent_reference.go index 9438d1fbe..26a5434da 100644 --- a/rules/aip0234/request_parent_reference.go +++ b/rules/aip0234/request_parent_reference.go @@ -25,13 +25,5 @@ var requestParentReference = &lint.FieldRule{ OnlyIf: func(f *desc.FieldDescriptor) bool { return isBatchUpdateRequestMessage(f.GetOwner()) && f.GetName() == "parent" }, - LintField: func(f *desc.FieldDescriptor) []lint.Problem { - if ref := utils.GetResourceReference(f); ref == nil { - return []lint.Problem{{ - Message: "Batch Update methods: The `parent` field should include a `google.api.resource_reference` annotation.", - Descriptor: f, - }} - } - return nil - }, + LintField: utils.LintFieldResourceReference, } diff --git a/rules/aip0234/request_requests_behavior.go b/rules/aip0234/request_requests_behavior.go index dde5f4992..3bd85b89a 100644 --- a/rules/aip0234/request_requests_behavior.go +++ b/rules/aip0234/request_requests_behavior.go @@ -25,13 +25,5 @@ var requestRequestsBehavior = &lint.FieldRule{ OnlyIf: func(f *desc.FieldDescriptor) bool { return isBatchUpdateRequestMessage(f.GetOwner()) && f.GetName() == "requests" }, - LintField: func(f *desc.FieldDescriptor) []lint.Problem { - if !utils.GetFieldBehavior(f).Contains("REQUIRED") { - return []lint.Problem{{ - Message: "Batch Update requests: The `requests` field should include `(google.api.field_behavior) = REQUIRED`.", - Descriptor: f, - }} - } - return nil - }, + LintField: utils.LintRequiredField, } diff --git a/rules/aip0235/http_body.go b/rules/aip0235/http_body.go index f6fd1647b..9f5a86d51 100644 --- a/rules/aip0235/http_body.go +++ b/rules/aip0235/http_body.go @@ -17,24 +17,11 @@ package aip0235 import ( "github.com/googleapis/api-linter/lint" "github.com/googleapis/api-linter/rules/internal/utils" - "github.com/jhump/protoreflect/desc" ) // Batch Delete methods should use "*" as the HTTP body. var httpBody = &lint.MethodRule{ - Name: lint.NewRuleName(235, "http-body"), - OnlyIf: isBatchDeleteMethod, - LintMethod: func(m *desc.MethodDescriptor) []lint.Problem { - // Establish that the RPC has correct HTTP body. - for _, httpRule := range utils.GetHTTPRules(m) { - if httpRule.Body != "*" { - return []lint.Problem{{ - Message: `Batch Delete methods should use "*" as the HTTP body.`, - Descriptor: m, - }} - } - } - - return nil - }, + Name: lint.NewRuleName(235, "http-body"), + OnlyIf: isBatchDeleteMethod, + LintMethod: utils.LintWildcardHTTPBody, } diff --git a/rules/aip0235/http_method.go b/rules/aip0235/http_method.go index 2d841ddb9..e1686d786 100644 --- a/rules/aip0235/http_method.go +++ b/rules/aip0235/http_method.go @@ -17,24 +17,11 @@ package aip0235 import ( "github.com/googleapis/api-linter/lint" "github.com/googleapis/api-linter/rules/internal/utils" - "github.com/jhump/protoreflect/desc" ) // Batch Delete methods should use the HTTP POST method. var httpMethod = &lint.MethodRule{ - Name: lint.NewRuleName(235, "http-method"), - OnlyIf: isBatchDeleteMethod, - LintMethod: func(m *desc.MethodDescriptor) []lint.Problem { - // Rule check: Establish that the RPC uses HTTP POST. - for _, httpRule := range utils.GetHTTPRules(m) { - if httpRule.Method != "POST" { - return []lint.Problem{{ - Message: "Batch Delete methods must use the HTTP POST verb.", - Descriptor: m, - }} - } - } - - return nil - }, + Name: lint.NewRuleName(235, "http-method"), + OnlyIf: isBatchDeleteMethod, + LintMethod: utils.LintHTTPMethod("POST"), } diff --git a/rules/aip0235/request_names_behavior.go b/rules/aip0235/request_names_behavior.go index 6318c60c3..faa58fe89 100644 --- a/rules/aip0235/request_names_behavior.go +++ b/rules/aip0235/request_names_behavior.go @@ -25,13 +25,5 @@ var requestNamesBehavior = &lint.FieldRule{ OnlyIf: func(f *desc.FieldDescriptor) bool { return isBatchDeleteRequestMessage(f.GetOwner()) && f.GetName() == "names" }, - LintField: func(f *desc.FieldDescriptor) []lint.Problem { - if !utils.GetFieldBehavior(f).Contains("REQUIRED") { - return []lint.Problem{{ - Message: "Batch Delete requests: The `names` field should include `(google.api.field_behavior) = REQUIRED`.", - Descriptor: f, - }} - } - return nil - }, + LintField: utils.LintRequiredField, } diff --git a/rules/aip0235/request_names_reference.go b/rules/aip0235/request_names_reference.go index da87cbb58..810289e20 100644 --- a/rules/aip0235/request_names_reference.go +++ b/rules/aip0235/request_names_reference.go @@ -25,13 +25,5 @@ var requestNamesReference = &lint.FieldRule{ OnlyIf: func(f *desc.FieldDescriptor) bool { return isBatchDeleteRequestMessage(f.GetOwner()) && f.GetName() == "names" }, - LintField: func(f *desc.FieldDescriptor) []lint.Problem { - if ref := utils.GetResourceReference(f); ref == nil { - return []lint.Problem{{ - Message: "Batch Delete methods: The `names` field should include a `google.api.resource_reference` annotation.", - Descriptor: f, - }} - } - return nil - }, + LintField: utils.LintFieldResourceReference, } diff --git a/rules/aip0235/request_parent_reference.go b/rules/aip0235/request_parent_reference.go index 105464c07..93fcb14ba 100644 --- a/rules/aip0235/request_parent_reference.go +++ b/rules/aip0235/request_parent_reference.go @@ -25,13 +25,5 @@ var requestParentReference = &lint.FieldRule{ OnlyIf: func(f *desc.FieldDescriptor) bool { return isBatchDeleteRequestMessage(f.GetOwner()) && f.GetName() == "parent" }, - LintField: func(f *desc.FieldDescriptor) []lint.Problem { - if ref := utils.GetResourceReference(f); ref == nil { - return []lint.Problem{{ - Message: "Batch Delete methods: The `parent` field should include a `google.api.resource_reference` annotation.", - Descriptor: f, - }} - } - return nil - }, + LintField: utils.LintFieldResourceReference, } diff --git a/rules/aip0235/request_requests_behavior.go b/rules/aip0235/request_requests_behavior.go index 6fd312c16..b5526b838 100644 --- a/rules/aip0235/request_requests_behavior.go +++ b/rules/aip0235/request_requests_behavior.go @@ -25,13 +25,5 @@ var requestRequestsBehavior = &lint.FieldRule{ OnlyIf: func(f *desc.FieldDescriptor) bool { return isBatchDeleteRequestMessage(f.GetOwner()) && f.GetName() == "requests" }, - LintField: func(f *desc.FieldDescriptor) []lint.Problem { - if !utils.GetFieldBehavior(f).Contains("REQUIRED") { - return []lint.Problem{{ - Message: "Batch Delete requests: The `requests` field should include `(google.api.field_behavior) = REQUIRED`.", - Descriptor: f, - }} - } - return nil - }, + LintField: utils.LintRequiredField, } diff --git a/rules/internal/utils/common_lints.go b/rules/internal/utils/common_lints.go new file mode 100644 index 000000000..b9cf0afa8 --- /dev/null +++ b/rules/internal/utils/common_lints.go @@ -0,0 +1,96 @@ +// Copyright 2020 Google LLC +// +// 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 utils + +import ( + "fmt" + + "github.com/googleapis/api-linter/lint" + "github.com/googleapis/api-linter/locations" + "github.com/jhump/protoreflect/desc" + "github.com/jhump/protoreflect/desc/builder" +) + +// LintStringField returns a problem if the field is not a string. +func LintStringField(f *desc.FieldDescriptor) []lint.Problem { + if f.GetType() != builder.FieldTypeString().GetType() { + return []lint.Problem{{ + Message: fmt.Sprintf("The `%s` field must be a string.", f.GetType()), + Suggestion: "string", + Descriptor: f, + Location: locations.FieldType(f), + }} + } + return nil +} + +// LintRequiredField returns a problem if the field's behavior is not REQUIRED. +func LintRequiredField(f *desc.FieldDescriptor) []lint.Problem { + if !GetFieldBehavior(f).Contains("REQUIRED") { + return []lint.Problem{{ + Message: fmt.Sprintf("The `%s` field should include `(google.api.field_behavior) = REQUIRED`.", f.GetName()), + Descriptor: f, + }} + } + return nil +} + +// LintFieldResourceReference returns a problem if the field does not have a resource reference annotation. +func LintFieldResourceReference(f *desc.FieldDescriptor) []lint.Problem { + if ref := GetResourceReference(f); ref == nil { + return []lint.Problem{{ + Message: fmt.Sprintf("The `%s` field should include a `google.api.resource_reference` annotation.", f.GetName()), + Descriptor: f, + }} + } + return nil +} + +func lintHTTPBody(m *desc.MethodDescriptor, want, msg string) []lint.Problem { + for _, httpRule := range GetHTTPRules(m) { + if httpRule.Body != want { + return []lint.Problem{{ + Message: fmt.Sprintf("The `%s` method should %s HTTP body.", m.GetName(), msg), + Descriptor: m, + }} + } + } + return nil +} + +// LintNoHTTPBody returns a problem for each HTTP rule whose body is not "". +func LintNoHTTPBody(m *desc.MethodDescriptor) []lint.Problem { + return lintHTTPBody(m, "", "not have an") +} + +// LintWildcardHTTPBody returns a problem for each HTTP rule whose body is not "*". +func LintWildcardHTTPBody(m *desc.MethodDescriptor) []lint.Problem { + return lintHTTPBody(m, "*", `use "*" as the`) +} + +// LintHTTPMethod returns a problem for each HTTP rule whose HTTP method is not the given one. +func LintHTTPMethod(verb string) func(*desc.MethodDescriptor) []lint.Problem { + return func(m *desc.MethodDescriptor) []lint.Problem { + for _, httpRule := range GetHTTPRules(m) { + if httpRule.Method != verb { + return []lint.Problem{{ + Message: fmt.Sprintf("The `%s` method should use the HTTP %s verb.", m.GetName(), verb), + Descriptor: m, + }} + } + } + return nil + } +} diff --git a/rules/internal/utils/common_lints_test.go b/rules/internal/utils/common_lints_test.go new file mode 100644 index 000000000..b14230d6a --- /dev/null +++ b/rules/internal/utils/common_lints_test.go @@ -0,0 +1,176 @@ +package utils + +import ( + "testing" + + "github.com/googleapis/api-linter/rules/internal/testutils" +) + +func TestLintStringField(t *testing.T) { + for _, test := range []struct { + testName string + FieldType string + problems testutils.Problems + }{ + {"Valid", `string`, nil}, + {"Invalid", `int32`, testutils.Problems{{Suggestion: "string"}}}, + } { + t.Run(test.testName, func(t *testing.T) { + f := testutils.ParseProto3Tmpl(t, ` + message Message { + {{.FieldType}} foo = 1; + } + `, test) + field := f.GetMessageTypes()[0].GetFields()[0] + problems := LintStringField(field) + if diff := test.problems.SetDescriptor(field).Diff(problems); diff != "" { + t.Error(diff) + } + }) + } +} + +func TestLintRequiredField(t *testing.T) { + for _, test := range []struct { + testName string + Annotation string + problems testutils.Problems + }{ + {"Valid", `[(google.api.field_behavior) = REQUIRED]`, nil}, + {"Invalid", ``, testutils.Problems{{Message: "REQUIRED"}}}, + } { + t.Run(test.testName, func(t *testing.T) { + f := testutils.ParseProto3Tmpl(t, ` + import "google/api/field_behavior.proto"; + message Message { + string foo = 1 {{.Annotation}}; + } + `, test) + field := f.GetMessageTypes()[0].GetFields()[0] + problems := LintRequiredField(field) + if diff := test.problems.SetDescriptor(field).Diff(problems); diff != "" { + t.Error(diff) + } + }) + } +} + +func TestLintFieldResourceReference(t *testing.T) { + for _, test := range []struct { + testName string + Annotation string + problems testutils.Problems + }{ + {"Valid", `[(google.api.resource_reference).type = "bar"]`, nil}, + {"Invalid", ``, testutils.Problems{{Message: "resource_reference"}}}, + } { + t.Run(test.testName, func(t *testing.T) { + f := testutils.ParseProto3Tmpl(t, ` + import "google/api/resource.proto"; + message Message { + string foo = 1 {{.Annotation}}; + } + `, test) + field := f.GetMessageTypes()[0].GetFields()[0] + problems := LintFieldResourceReference(field) + if diff := test.problems.SetDescriptor(field).Diff(problems); diff != "" { + t.Error(diff) + } + }) + } +} + +func TestLintNoHTTPBody(t *testing.T) { + for _, test := range []struct { + testName string + Body string + problems testutils.Problems + }{ + {"Valid", ``, nil}, + {"Invalid", `*`, testutils.Problems{{Message: "not have an HTTP body"}}}, + } { + t.Run(test.testName, func(t *testing.T) { + f := testutils.ParseProto3Tmpl(t, ` + import "google/api/annotations.proto"; + service Library { + rpc GetBook(GetBookRequest) returns (Book) { + option (google.api.http) = { + get: "/v1/{name=publishers/*/books/*}" + body: "{{.Body}}" + }; + } + } + message Book {} + message GetBookRequest {} + `, test) + method := f.GetServices()[0].GetMethods()[0] + problems := LintNoHTTPBody(method) + if diff := test.problems.SetDescriptor(method).Diff(problems); diff != "" { + t.Error(diff) + } + }) + } +} + +func TestLintWildcardHTTPBody(t *testing.T) { + for _, test := range []struct { + testName string + Body string + problems testutils.Problems + }{ + {"Valid", `*`, nil}, + {"Invalid", ``, testutils.Problems{{Message: `use "*" as the HTTP body`}}}, + } { + t.Run(test.testName, func(t *testing.T) { + f := testutils.ParseProto3Tmpl(t, ` + import "google/api/annotations.proto"; + service Library { + rpc ArchiveBook(ArchiveBookRequest) returns (Book) { + option (google.api.http) = { + post: "/v1/{name=publishers/*/books/*}:archive" + body: "{{.Body}}" + }; + } + } + message Book {} + message ArchiveBookRequest {} + `, test) + method := f.GetServices()[0].GetMethods()[0] + problems := LintWildcardHTTPBody(method) + if diff := test.problems.SetDescriptor(method).Diff(problems); diff != "" { + t.Error(diff) + } + }) + } +} + +func TestLintHTTPMethod(t *testing.T) { + for _, test := range []struct { + testName string + Method string + problems testutils.Problems + }{ + {"Valid", `get`, nil}, + {"Invalid", `delete`, testutils.Problems{{Message: `HTTP GET`}}}, + } { + t.Run(test.testName, func(t *testing.T) { + f := testutils.ParseProto3Tmpl(t, ` + import "google/api/annotations.proto"; + service Library { + rpc GetBook(GetBookRequest) returns (Book) { + option (google.api.http) = { + {{.Method}}: "/v1/{name=publishers/*/books/*}" + }; + } + } + message Book {} + message GetBookRequest {} + `, test) + method := f.GetServices()[0].GetMethods()[0] + problems := LintHTTPMethod("GET")(method) + if diff := test.problems.SetDescriptor(method).Diff(problems); diff != "" { + t.Error(diff) + } + }) + } +}