github.com/googleapis/api-linter@v1.65.2/rules/aip0127/http_template_pattern.go (about)

     1  // Copyright 2022 Google LLC
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     https://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package aip0127
    16  
    17  import (
    18  	"fmt"
    19  	"regexp"
    20  	"strings"
    21  
    22  	"github.com/googleapis/api-linter/lint"
    23  	"github.com/googleapis/api-linter/locations"
    24  	"github.com/googleapis/api-linter/rules/internal/utils"
    25  	"github.com/jhump/protoreflect/desc"
    26  )
    27  
    28  var (
    29  	resourcePatternSegment     = `[^/]+`
    30  	resourcePatternAnySegments = fmt.Sprintf("((%s/)*%s)?", resourcePatternSegment, resourcePatternSegment)
    31  	pathTemplateToRegex        = strings.NewReplacer("**", resourcePatternAnySegments, "*", resourcePatternSegment)
    32  )
    33  
    34  type resourceReference struct {
    35  	// The path of the field with the `google.api.resource_reference`. This is
    36  	// provided as a variable in the HTTPRule.
    37  	fieldPath string
    38  	// A template that the resource's pattern string must adhere to. This is
    39  	// provided by the variable's template in the HTTPRule.
    40  	pathTemplate string
    41  	// The name of the resource type. This is used to look up the resource
    42  	// message.
    43  	resourceRefName string
    44  }
    45  
    46  // Returns a list of resourceReferences for each variable in all the method's
    47  // HTTPRule's.
    48  func methodResourceReferences(m *desc.MethodDescriptor) []resourceReference {
    49  	resourceRefs := []resourceReference{}
    50  	for _, httpRule := range utils.GetHTTPRules(m) {
    51  		resourceRefs = append(resourceRefs, httpResourceReferences(httpRule, m.GetInputType())...)
    52  	}
    53  	return resourceRefs
    54  }
    55  
    56  // Returns a resourceReference for every variable in the given HTTPRule.
    57  func httpResourceReferences(httpRule *utils.HTTPRule, msg *desc.MessageDescriptor) []resourceReference {
    58  	resourceRefs := []resourceReference{}
    59  	for fieldPath, template := range httpRule.GetVariables() {
    60  		// Find the (sub-)field in the message corresponding to the variable's
    61  		// field path.
    62  		field := utils.FindFieldDotNotation(msg, fieldPath)
    63  		if field == nil {
    64  			continue
    65  		}
    66  
    67  		// Extract the name of the resource referenced by this field.
    68  		ref := utils.GetResourceReference(field)
    69  		if ref == nil || ref.GetChildType() != "" {
    70  			// TODO(#1047): Support the case where a resource has
    71  			// multiple parent resources.
    72  			continue
    73  		}
    74  
    75  		resourceRefs = append(resourceRefs, resourceReference{
    76  			fieldPath:       fieldPath,
    77  			pathTemplate:    template,
    78  			resourceRefName: ref.GetType(),
    79  		})
    80  	}
    81  	return resourceRefs
    82  }
    83  
    84  // Constructs a regex from the HTTPRule's path template representing resource
    85  // patterns that it will match against.
    86  func compilePathTemplateRegex(pathTemplate string) (*regexp.Regexp, error) {
    87  	pattern := fmt.Sprintf("^%s$", pathTemplateToRegex.Replace(pathTemplate))
    88  	return regexp.Compile(pattern)
    89  }
    90  
    91  func anyMatch(regex *regexp.Regexp, strs []string) bool {
    92  	for _, str := range strs {
    93  		if regex.MatchString(str) {
    94  			return true
    95  		}
    96  	}
    97  	return false
    98  }
    99  
   100  // Checks whether the HTTP pattern specified in `resourceRef` matches any of the
   101  // patterns defined for that resource.
   102  func checkHTTPPatternMatchesResource(m *desc.MethodDescriptor, resourceRef resourceReference) []lint.Problem {
   103  	annotation := utils.FindResource(resourceRef.resourceRefName, m.GetFile())
   104  	if annotation == nil {
   105  		return []lint.Problem{}
   106  	}
   107  
   108  	pathRegex, err := compilePathTemplateRegex(resourceRef.pathTemplate)
   109  	if err != nil {
   110  		return []lint.Problem{}
   111  	}
   112  
   113  	if !anyMatch(pathRegex, annotation.GetPattern()) {
   114  		message := fmt.Sprintf("The HTTP pattern %q does not match any of the patterns for resource %q", resourceRef.pathTemplate, resourceRef.resourceRefName)
   115  		return []lint.Problem{{Message: message, Descriptor: m, Location: locations.MethodHTTPRule(m)}}
   116  	}
   117  
   118  	return []lint.Problem{}
   119  }
   120  
   121  var httpTemplatePattern = &lint.MethodRule{
   122  	Name: lint.NewRuleName(127, "http-template-pattern"),
   123  	OnlyIf: func(m *desc.MethodDescriptor) bool {
   124  		return len(methodResourceReferences(m)) > 0
   125  	},
   126  	LintMethod: func(m *desc.MethodDescriptor) []lint.Problem {
   127  		problems := []lint.Problem{}
   128  
   129  		resourceRefs := methodResourceReferences(m)
   130  		for _, resourceRef := range resourceRefs {
   131  			problems = append(problems, checkHTTPPatternMatchesResource(m, resourceRef)...)
   132  		}
   133  
   134  		return problems
   135  	},
   136  }