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 }