go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/analysis/internal/aip/orderby_parser.go (about)

     1  // Copyright 2022 The LUCI Authors.
     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  //      http://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  // This file provides a parser for AIP-132 order by clauses, with advanced
    16  // field path support along the lines of AIP-161 (for map fields).
    17  // Only maps with string keys, not integer keys, are supported, however.
    18  //
    19  // Both field paths and the "desc" keyword are case-sensitive.
    20  //
    21  // order_by_list = order_by_clause {[spaces] "," order_by_clause} [spaces]
    22  // order_by_clause = field_path order
    23  // field_path = [spaces] segment {"." segment}
    24  // order = [spaces "desc"]
    25  // segment = string | quoted_string;
    26  // integer = ["-"] digit {digit};
    27  // string = (letter | "_") {letter | "_" | digit}
    28  // quoted_string = "`" { utf8-no-backtick | "`" "`" } "`"
    29  // spaces = " " { " " }
    30  //
    31  // No validation is performed to test that the field paths are valid for
    32  // a particular protocol buffer message.
    33  
    34  package aip
    35  
    36  import (
    37  	"regexp"
    38  	"strings"
    39  
    40  	participle "github.com/alecthomas/participle/v2"
    41  	"github.com/alecthomas/participle/v2/lexer"
    42  
    43  	"go.chromium.org/luci/common/errors"
    44  )
    45  
    46  const stringLiteralExpr = `[a-zA-Z_][a-zA-Z_0-9]*`
    47  
    48  var stringLiteralRE = regexp.MustCompile(`^` + stringLiteralExpr + `$`)
    49  
    50  var (
    51  	orderByLexer = lexer.MustSimple([]lexer.SimpleRule{
    52  		{Name: "Spaces", Pattern: `[ ]+`},
    53  		{Name: "String", Pattern: `[a-zA-Z_][a-zA-Z_0-9]*`},
    54  		{Name: "QuotedString", Pattern: "`(``|[^`])*`"},
    55  		{Name: "Operators", Pattern: "[.,]"},
    56  	})
    57  
    58  	orderByParser = participle.MustBuild[orderByList](participle.Lexer(orderByLexer))
    59  )
    60  
    61  // OrderBy represents a part of an AIP-132 order_by clause.
    62  type OrderBy struct {
    63  	// The field path. This is the path of the field in the
    64  	// resource message that the AIP-132 List RPC is listing.
    65  	FieldPath FieldPath
    66  	// Whether the field should be sorted in descending order.
    67  	Descending bool
    68  }
    69  
    70  // FieldPath represents the path to a field in a message.
    71  //
    72  // For example, for the given message:
    73  //
    74  //	message MyThing {
    75  //	   message Bar {
    76  //	       string foobar = 2;
    77  //	   }
    78  //	   string foo = 1;
    79  //	   Bar bar = 2;
    80  //	   map<string, Bar> named_bars = 3;
    81  //	}
    82  //
    83  // Some valid paths would be: foo, bar.foobar and
    84  // named_bars.`bar-key`.foobar.
    85  type FieldPath struct {
    86  	// The field path as its segments.
    87  	segments []string
    88  
    89  	// The canoncial reprsentation of the field path.
    90  	canoncial string
    91  }
    92  
    93  // NewFieldPath initialises a new field path with the given segments.
    94  func NewFieldPath(segments ...string) FieldPath {
    95  	var builder strings.Builder
    96  	for _, segment := range segments {
    97  		if builder.Len() > 0 {
    98  			builder.WriteString(".")
    99  		}
   100  		if stringLiteralRE.MatchString(segment) {
   101  			builder.WriteString(segment)
   102  		} else {
   103  			builder.WriteString("`")
   104  			builder.WriteString(strings.ReplaceAll(segment, "`", "``"))
   105  			builder.WriteString("`")
   106  		}
   107  	}
   108  	return FieldPath{
   109  		segments:  segments,
   110  		canoncial: builder.String(),
   111  	}
   112  }
   113  
   114  // Equals returns iff two field paths refer to exactly the
   115  // same field.
   116  func (f FieldPath) Equals(other FieldPath) bool {
   117  	return f.canoncial == other.canoncial
   118  }
   119  
   120  // String returns a canoncial representation of the field path,
   121  // following AIP-132 / AIP-161 syntax.
   122  func (f FieldPath) String() string {
   123  	return f.canoncial
   124  }
   125  
   126  // ParseOrderBy parses an AIP-132 order_by list. The method validates the
   127  // syntax is correct and each identifier appears at most once, but
   128  // it does not validate the identifiers themselves are valid.
   129  func ParseOrderBy(text string) ([]OrderBy, error) {
   130  	// Empty order_by list.
   131  	if strings.Trim(text, " ") == "" {
   132  		return nil, nil
   133  	}
   134  
   135  	expr, err := orderByParser.ParseString("", text)
   136  	if err != nil {
   137  		return nil, errors.Annotate(err, "syntax error").Err()
   138  	}
   139  
   140  	var result []OrderBy
   141  	for _, clause := range expr.SortOrder {
   142  		result = append(result, OrderBy{
   143  			FieldPath:  NewFieldPath(clause.FieldPath.Path()...),
   144  			Descending: clause.Order.Desc,
   145  		})
   146  	}
   147  
   148  	uniqueFieldPaths := make(map[string]struct{})
   149  	for _, orderBy := range result {
   150  		if _, ok := uniqueFieldPaths[orderBy.FieldPath.String()]; ok {
   151  			return nil, errors.Reason("field appears multiple times: %q", orderBy.FieldPath).Err()
   152  		}
   153  		uniqueFieldPaths[orderBy.FieldPath.String()] = struct{}{}
   154  	}
   155  
   156  	return result, nil
   157  }
   158  
   159  type orderByList struct {
   160  	SortOrder []*orderByClause `parser:"@@ ( Spaces? ',' @@ )* Spaces?"`
   161  }
   162  
   163  type orderByClause struct {
   164  	FieldPath *fieldPath `parser:"@@"`
   165  	Order     *order     `parser:"@@"`
   166  }
   167  
   168  type order struct {
   169  	Desc bool `parser:"@( Spaces 'desc' )?"`
   170  }
   171  
   172  type fieldPath struct {
   173  	Segments []*segment `parser:"Spaces? @@ ( '.' @@ )*"`
   174  }
   175  
   176  // Path returns the field path as a list of path segments.
   177  func (f *fieldPath) Path() []string {
   178  	result := make([]string, 0, len(f.Segments))
   179  	for _, segment := range f.Segments {
   180  		result = append(result, segment.Value())
   181  	}
   182  	return result
   183  }
   184  
   185  type segment struct {
   186  	StringValue  *string `parser:"@String"`
   187  	QuotedString *string `parser:"| @QuotedString"`
   188  }
   189  
   190  func (s *segment) Value() string {
   191  	if s.QuotedString != nil {
   192  		// Remove the outer backticks and replace all occurances
   193  		// of double backticks with single backticks.
   194  		unquotedString := (*s.QuotedString)[1 : len(*s.QuotedString)-1]
   195  		return strings.ReplaceAll(unquotedString, "``", "`")
   196  	}
   197  	if s.StringValue != nil {
   198  		return *s.StringValue
   199  	}
   200  	// Should never happen if parsing succeeds.
   201  	panic("invalid syntax")
   202  }