github.com/web-platform-tests/wpt.fyi@v0.0.0-20240530210107-70cf978996f1/api/query/atoms.go (about) 1 // Copyright 2018 The WPT Dashboard Project. All rights reserved. 2 // Use of this source code is governed by a BSD-style license that can be 3 // found in the LICENSE file. 4 5 package query 6 7 // This file defines search atoms on the backend. All wpt.fyi search queries 8 // are broken down into a tree of search atoms, which is then traversed by the 9 // searchcache to find matching tests to include. 10 // 11 // All search atoms must define two methods: 12 // i. BindToRuns 13 // ii. UnmarshalJSON 14 // 15 // These are best understood in reverse, as that is also the order they are 16 // called in. UnmarshalJSON is used to convert from the original JSON search 17 // query into the tree of abstract search atoms. The atoms are referred to as 18 // abstract as they do not yet relate to any underlying data (i.e. any test 19 // runs). Many types of atom (such as AbstractExists) perform this 20 // unmarshalling recursively, which is how we end up with a tree. 21 // 22 // Once we have an abstract search tree, BindToRuns will convert it to a 23 // concrete search tree (that is, a tree of ConcreteQuery atoms). This gives 24 // the search atoms access to the specific runs that are being searched over, 25 // to pull any specific information needed. For example, this allows 26 // TestStatusEq to only produce results for test runs that match the specified 27 // product (and short-circuit entirely if no test runs match). 28 // 29 // Some abstract search atoms may produce more than one concrete search atom 30 // (e.g. AbstractExists, which produces a disjunction), whilst others may 31 // ignore the test runs entirely if they aren't relevant (e.g. 32 // TestNamePattern, which only cares about the test name and not the results). 33 // 34 // Note that this file does not perform the actual filtering of tests from the 35 // test runs to produce the search response; for that see the `filter` type in 36 // api/query/cache/index/filter.go 37 38 import ( 39 "encoding/json" 40 "errors" 41 "fmt" 42 "strings" 43 44 "github.com/sirupsen/logrus" 45 "github.com/web-platform-tests/wpt.fyi/shared" 46 ) 47 48 // AbstractQuery is an intermetidate representation of a test results query that 49 // has not been bound to specific shared.TestRun specs for processing. 50 type AbstractQuery interface { 51 BindToRuns(runs ...shared.TestRun) ConcreteQuery 52 } 53 54 // RunQuery is the internal representation of a query received from an HTTP 55 // client, including the IDs of the test runs to query, and the structured query 56 // to run. 57 type RunQuery struct { 58 RunIDs []int64 59 AbstractQuery 60 } 61 62 // True is a true-valued ConcreteQuery. 63 type True struct{} 64 65 // BindToRuns for True is a no-op; it is independent of test runs. 66 // nolint:ireturn // TODO: Fix ireturn lint error 67 func (t True) BindToRuns(_ ...shared.TestRun) ConcreteQuery { 68 return t 69 } 70 71 // False is a false-valued ConcreteQuery. 72 type False struct{} 73 74 // BindToRuns for False is a no-op; it is independent of test runs. 75 // nolint:ireturn // TODO: Fix ireturn lint error 76 func (f False) BindToRuns(_ ...shared.TestRun) ConcreteQuery { 77 return f 78 } 79 80 // TestNamePattern is a query atom that matches test names to a pattern string. 81 type TestNamePattern struct { 82 Pattern string 83 } 84 85 // BindToRuns for TestNamePattern is a no-op; it is independent of test runs. 86 // nolint:ireturn // TODO: Fix ireturn lint error 87 func (tnp TestNamePattern) BindToRuns(_ ...shared.TestRun) ConcreteQuery { 88 return tnp 89 } 90 91 // SubtestNamePattern is a query atom that matches subtest names to a pattern string. 92 type SubtestNamePattern struct { 93 Subtest string 94 } 95 96 // BindToRuns for SubtestNamePattern is a no-op; it is independent of test runs. 97 // nolint:ireturn // TODO: Fix ireturn lint error 98 func (tnp SubtestNamePattern) BindToRuns(_ ...shared.TestRun) ConcreteQuery { 99 return tnp 100 } 101 102 // TestPath is a query atom that matches exact test path prefixes. 103 // It is an inflexible equivalent of TestNamePattern. 104 type TestPath struct { 105 Path string 106 } 107 108 // BindToRuns for TestNamePattern is a no-op; it is independent of test runs. 109 // nolint:ireturn // TODO: Fix ireturn lint error 110 func (tp TestPath) BindToRuns(_ ...shared.TestRun) ConcreteQuery { 111 return tp 112 } 113 114 // AbstractExists represents an array of abstract queries, each of which must be 115 // satifisfied by some run. It represents the root of a structured query. 116 type AbstractExists struct { 117 Args []AbstractQuery 118 } 119 120 // BindToRuns binds each abstract query to an or-combo of that query against 121 // each specific/individual run. 122 // nolint:ireturn // TODO: Fix ireturn lint error 123 func (e AbstractExists) BindToRuns(runs ...shared.TestRun) ConcreteQuery { 124 queries := make([]ConcreteQuery, len(e.Args)) 125 // When the nested query is a single query, e.g. And/Or, bind that query directly. 126 if len(e.Args) == 1 { 127 return e.Args[0].BindToRuns(runs...) 128 } 129 130 for i, arg := range e.Args { 131 var query ConcreteQuery 132 // Exists queries are split; one run must satisfy the whole tree. 133 byRun := make([]ConcreteQuery, 0, len(runs)) 134 for _, run := range runs { 135 bound := arg.BindToRuns(run) 136 if _, ok := bound.(False); !ok { 137 byRun = append(byRun, bound) 138 } 139 } 140 query = Or{Args: byRun} 141 queries[i] = query 142 } 143 // And the overall node is true if all its exists queries are true. 144 return And{ 145 Args: queries, 146 } 147 } 148 149 // AbstractAll represents an array of abstract queries, each of which must be 150 // satifisfied by all runs. It represents the root of a structured query. 151 type AbstractAll struct { 152 Args []AbstractQuery 153 } 154 155 // BindToRuns binds each abstract query to an and-combo of that query against 156 // each specific/individual run. 157 // nolint:ireturn // TODO: Fix ireturn lint error 158 func (e AbstractAll) BindToRuns(runs ...shared.TestRun) ConcreteQuery { 159 queries := make([]ConcreteQuery, len(e.Args)) 160 for i, arg := range e.Args { 161 var query ConcreteQuery 162 byRun := make([]ConcreteQuery, 0, len(runs)) 163 for _, run := range runs { 164 bound := arg.BindToRuns(run) 165 if _, ok := bound.(True); !ok { // And with True is pointless. 166 byRun = append(byRun, bound) 167 } 168 } 169 query = And{Args: byRun} 170 queries[i] = query 171 } 172 // And the overall node is true if all its exists queries are true. 173 return And{ 174 Args: queries, 175 } 176 } 177 178 // AbstractNone represents an array of abstract queries, each of which must not be 179 // satifisfied by any run. It represents the root of a structured query. 180 type AbstractNone struct { 181 Args []AbstractQuery 182 } 183 184 // BindToRuns binds to a not-exists for the same query(s). 185 // nolint:ireturn // TODO: Fix ireturn lint error 186 func (e AbstractNone) BindToRuns(runs ...shared.TestRun) ConcreteQuery { 187 return Not{ 188 AbstractExists(e).BindToRuns(runs...), 189 } 190 } 191 192 // AbstractSequential represents the root of a sequential queries, where the first 193 // query must be satisfied by some run such that the next run, sequentially, also 194 // satisfies the next query, and so on. 195 type AbstractSequential struct { 196 Args []AbstractQuery 197 } 198 199 // BindToRuns binds each sequential query to an and-combo of those queries against 200 // specific sequential runs, for each combination of sequential runs. 201 // nolint:ireturn // TODO: Fix ireturn lint error 202 func (e AbstractSequential) BindToRuns(runs ...shared.TestRun) ConcreteQuery { 203 numSeqQueries := len(e.Args) 204 byRuns := []ConcreteQuery{} 205 for i := 0; i+numSeqQueries-1 < len(runs); i++ { 206 all := And{} // nolint:exhaustruct // TODO: Fix exhaustruct lint error. 207 for j, arg := range e.Args { 208 all.Args = append(all.Args, arg.BindToRuns(runs[i+j])) 209 } 210 byRuns = append(byRuns, all) 211 } 212 213 return Or{ 214 Args: byRuns, 215 } 216 } 217 218 // AbstractCount represents the root of a count query, where the exact number of 219 // runs that satisfy the query must match the expected count. 220 type AbstractCount struct { 221 Count int 222 Where AbstractQuery 223 } 224 225 // BindToRuns binds each count query to all of the runs, so that it can count the 226 // number of runs that match the criteria. 227 // nolint:ireturn // TODO: Fix ireturn lint error 228 func (c AbstractCount) BindToRuns(runs ...shared.TestRun) ConcreteQuery { 229 byRun := []ConcreteQuery{} 230 for _, run := range runs { 231 byRun = append(byRun, c.Where.BindToRuns(run)) 232 } 233 234 return Count{ 235 Count: c.Count, 236 Args: byRun, 237 } 238 } 239 240 // AbstractMoreThan is the root of a moreThan query, where the number of runs 241 // that satisfy the query must be more than the given count. 242 type AbstractMoreThan struct { 243 AbstractCount 244 } 245 246 // BindToRuns binds each count query to all of the runs, so that it can count the 247 // number of runs that match the criteria. 248 // nolint:ireturn // TODO: Fix ireturn lint error 249 func (m AbstractMoreThan) BindToRuns(runs ...shared.TestRun) ConcreteQuery { 250 c := m.AbstractCount.BindToRuns(runs...).(Count) 251 252 return MoreThan{c} 253 } 254 255 // AbstractLessThan is the root of a lessThan query, where the number of runs 256 // that satisfy the query must be less than the given count. 257 type AbstractLessThan struct { 258 AbstractCount 259 } 260 261 // BindToRuns binds each count query to all of the runs, so that it can count the 262 // number of runs that match the criteria. 263 // nolint:ireturn // TODO: Fix ireturn lint error 264 func (l AbstractLessThan) BindToRuns(runs ...shared.TestRun) ConcreteQuery { 265 c := l.AbstractCount.BindToRuns(runs...).(Count) 266 267 return LessThan{c} 268 } 269 270 // AbstractLink is represents the root of a link query, which matches Metadata 271 // URLs to a pattern string. 272 type AbstractLink struct { 273 Pattern string 274 metadataFetcher shared.MetadataFetcher 275 } 276 277 // BindToRuns for AbstractLink fetches metadata for either test-level issues or 278 // issues associated with the given runs. It does not filter the metadata by 279 // the pattern yet. 280 // nolint:ireturn // TODO: Fix ireturn lint error 281 func (l AbstractLink) BindToRuns(runs ...shared.TestRun) ConcreteQuery { 282 if l.metadataFetcher == nil { 283 l.metadataFetcher = searchcacheMetadataFetcher{} 284 } 285 includeTestLevel := true 286 metadata, _ := shared.GetMetadataResponse(runs, includeTestLevel, logrus.StandardLogger(), l.metadataFetcher) 287 metadataMap := shared.PrepareLinkFilter(metadata) 288 289 return Link{ 290 Pattern: l.Pattern, 291 Metadata: metadataMap, 292 } 293 } 294 295 // AbstractTriaged represents the root of a triaged query that matches 296 // tests where the test of a specific browser has been triaged through Metadata. 297 type AbstractTriaged struct { 298 Product *shared.ProductSpec 299 metadataFetcher shared.MetadataFetcher 300 } 301 302 // BindToRuns for AbstractTriaged binds each run matching the AbstractTriaged 303 // ProductSpec to a triaged object. 304 // nolint:ireturn // TODO: Fix ireturn lint error 305 func (t AbstractTriaged) BindToRuns(runs ...shared.TestRun) ConcreteQuery { 306 cq := make([]ConcreteQuery, 0) 307 308 if t.metadataFetcher == nil { 309 t.metadataFetcher = searchcacheMetadataFetcher{} 310 } 311 for _, run := range runs { 312 if t.Product == nil || t.Product.Matches(run) { 313 // We only want to fetch metadata for this specific run (or for no runs, if 314 // the search is for test-level issues). 315 includeTestLevel := false 316 metadataRuns := []shared.TestRun{run} 317 318 // Product being nil means that we want test-level issues. 319 if t.Product == nil { 320 includeTestLevel = true 321 metadataRuns = []shared.TestRun{} 322 } 323 metadata, _ := shared.GetMetadataResponse(metadataRuns, includeTestLevel, logrus.StandardLogger(), t.metadataFetcher) 324 metadataMap := shared.PrepareLinkFilter(metadata) 325 if len(metadataMap) > 0 { 326 cq = append(cq, Triaged{run.ID, metadataMap}) 327 } 328 } 329 } 330 331 if len(cq) == 0 { 332 return False{} 333 } 334 335 return Or{cq} 336 } 337 338 // AbstractTestLabel represents the root of a testlabel query, which matches test-level metadata 339 // labels to a searched label. 340 type AbstractTestLabel struct { 341 Label string 342 metadataFetcher shared.MetadataFetcher 343 } 344 345 // BindToRuns for AbstractTestLabel fetches test-level metadata; it is independent of test runs. 346 // nolint:ireturn // TODO: Fix ireturn lint error 347 func (t AbstractTestLabel) BindToRuns(_ ...shared.TestRun) ConcreteQuery { 348 if t.metadataFetcher == nil { 349 t.metadataFetcher = searchcacheMetadataFetcher{} 350 } 351 352 includeTestLevel := true 353 // Passing []shared.TestRun{} means that we want test-level issues. 354 metadata, _ := shared.GetMetadataResponse( 355 []shared.TestRun{}, 356 includeTestLevel, 357 logrus.StandardLogger(), 358 t.metadataFetcher, 359 ) 360 metadataMap := shared.PrepareTestLabelFilter(metadata) 361 362 return TestLabel{ 363 Label: t.Label, 364 Metadata: metadataMap, 365 } 366 } 367 368 // webFeaturesManifestFetcher describes the behavior to fetch Web Features data. 369 type webFeaturesManifestFetcher interface { 370 Fetch() (shared.WebFeaturesData, error) 371 } 372 373 // AbstractTestWebFeature represents the root of a web_feature query, which matches test-level 374 // metadata to a searched web feature. 375 type AbstractTestWebFeature struct { 376 TestWebFeatureAtom 377 manifestFetcher webFeaturesManifestFetcher 378 } 379 380 // BindToRuns for AbstractTestWebFeature fetches test-level metadata; it is independent of test runs. 381 // nolint:ireturn // TODO: Fix ireturn lint error 382 func (t AbstractTestWebFeature) BindToRuns(_ ...shared.TestRun) ConcreteQuery { 383 data, _ := t.manifestFetcher.Fetch() 384 385 return TestWebFeature{ 386 WebFeature: t.WebFeature, 387 WebFeaturesData: data, 388 } 389 } 390 391 // MetadataQuality represents the root of an "is" query, which asserts known 392 // metadata qualities to the results. 393 type MetadataQuality int 394 395 const ( 396 // MetadataQualityUnknown is a placeholder for unrecognized values. 397 MetadataQualityUnknown MetadataQuality = iota 398 // MetadataQualityDifferent represents an is:different atom. 399 // "different" ensures that one or more results differs from the other results. 400 MetadataQualityDifferent 401 // MetadataQualityTentative represents an is:tentative atom. 402 // "tentative" ensures that the results are from a tentative test. 403 MetadataQualityTentative 404 // MetadataQualityOptional represents an is:optional atom. 405 // "optional" ensures that the results are from an optional test. 406 MetadataQualityOptional 407 ) 408 409 // BindToRuns for MetadataQuality is a no-op; it is independent of test runs. 410 // nolint:ireturn // TODO: Fix ireturn lint error 411 func (q MetadataQuality) BindToRuns(_ ...shared.TestRun) ConcreteQuery { 412 return q 413 } 414 415 // TestStatusEq is a query atom that matches tests where the test status/result 416 // from at least one test run matches the given status value, optionally filtered 417 // to a specific browser name. 418 type TestStatusEq struct { 419 Product *shared.ProductSpec 420 Status shared.TestStatus 421 } 422 423 // TestStatusNeq is a query atom that matches tests where the test status/result 424 // from at least one test run does not match the given status value, optionally 425 // filtered to a specific browser name. 426 type TestStatusNeq struct { 427 Product *shared.ProductSpec 428 Status shared.TestStatus 429 } 430 431 // BindToRuns for TestStatusEq expands to a disjunction of RunTestStatusEq 432 // values. 433 // nolint:ireturn // TODO: Fix ireturn lint error 434 func (tse TestStatusEq) BindToRuns(runs ...shared.TestRun) ConcreteQuery { 435 ids := make([]int64, 0, len(runs)) 436 for _, run := range runs { 437 if tse.Product == nil || tse.Product.Matches(run) { 438 ids = append(ids, run.ID) 439 } 440 } 441 if len(ids) == 0 { 442 return False{} 443 } 444 if len(ids) == 1 { 445 return RunTestStatusEq{ids[0], tse.Status} 446 } 447 448 q := Or{make([]ConcreteQuery, len(ids))} 449 for i := range ids { 450 q.Args[i] = RunTestStatusEq{ids[i], tse.Status} 451 } 452 453 return q 454 } 455 456 // BindToRuns for TestStatusNeq expands to a disjunction of RunTestStatusNeq 457 // values. 458 // nolint:ireturn // TODO: Fix ireturn lint error 459 func (tsn TestStatusNeq) BindToRuns(runs ...shared.TestRun) ConcreteQuery { 460 ids := make([]int64, 0, len(runs)) 461 for _, run := range runs { 462 if tsn.Product == nil || tsn.Product.Matches(run) { 463 ids = append(ids, run.ID) 464 } 465 } 466 if len(ids) == 0 { 467 return False{} 468 } 469 if len(ids) == 1 { 470 return RunTestStatusNeq{ids[0], tsn.Status} 471 } 472 473 q := Or{make([]ConcreteQuery, len(ids))} 474 for i := range ids { 475 q.Args[i] = RunTestStatusNeq{ids[i], tsn.Status} 476 } 477 478 return q 479 } 480 481 // AbstractNot is the AbstractQuery for negation. 482 type AbstractNot struct { 483 Arg AbstractQuery 484 } 485 486 // BindToRuns for AbstractNot produces a Not with a bound argument. 487 // nolint:ireturn // TODO: Fix ireturn lint error 488 func (n AbstractNot) BindToRuns(runs ...shared.TestRun) ConcreteQuery { 489 return Not{n.Arg.BindToRuns(runs...)} 490 } 491 492 // AbstractOr is the AbstractQuery for disjunction. 493 type AbstractOr struct { 494 Args []AbstractQuery 495 } 496 497 // BindToRuns for AbstractOr produces an Or with bound arguments. 498 // nolint:ireturn // TODO: Fix ireturn lint error 499 func (o AbstractOr) BindToRuns(runs ...shared.TestRun) ConcreteQuery { 500 args := make([]ConcreteQuery, 0, len(o.Args)) 501 for i := range o.Args { 502 sub := o.Args[i].BindToRuns(runs...) 503 if t, ok := sub.(True); ok { 504 return t 505 } 506 if _, ok := sub.(False); ok { 507 continue 508 } 509 args = append(args, sub) 510 } 511 if len(args) == 0 { 512 return False{} 513 } 514 if len(args) == 1 { 515 return args[0] 516 } 517 518 return Or{ 519 Args: args, 520 } 521 } 522 523 // AbstractAnd is the AbstractQuery for conjunction. 524 type AbstractAnd struct { 525 Args []AbstractQuery 526 } 527 528 // BindToRuns for AbstractAnd produces an And with bound arguments. 529 // nolint:ireturn // TODO: Fix ireturn lint error 530 func (a AbstractAnd) BindToRuns(runs ...shared.TestRun) ConcreteQuery { 531 args := make([]ConcreteQuery, 0, len(a.Args)) 532 for i := range a.Args { 533 sub := a.Args[i].BindToRuns(runs...) 534 if _, ok := sub.(False); ok { 535 return False{} 536 } 537 if _, ok := sub.(True); ok { 538 continue 539 } 540 args = append(args, sub) 541 } 542 if len(args) == 0 { 543 return False{} 544 } 545 if len(args) == 1 { 546 return args[0] 547 } 548 549 return And{ 550 Args: args, 551 } 552 } 553 554 // UnmarshalJSON interprets the JSON representation of a RunQuery, instantiating 555 // (an) appropriate Query implementation(s) according to the JSON structure. 556 func (rq *RunQuery) UnmarshalJSON(b []byte) error { 557 var data struct { 558 RunIDs []int64 `json:"run_ids"` 559 Query json.RawMessage `json:"query"` 560 } 561 if err := json.Unmarshal(b, &data); err != nil { 562 return err 563 } 564 if len(data.RunIDs) == 0 { 565 return errors.New(`Missing run query property: "run_ids"`) 566 } 567 rq.RunIDs = data.RunIDs 568 569 if len(data.Query) > 0 { 570 q, err := unmarshalQ(data.Query) 571 if err != nil { 572 return err 573 } 574 rq.AbstractQuery = q 575 } else { 576 rq.AbstractQuery = True{} 577 } 578 579 return nil 580 } 581 582 // UnmarshalJSON for TestNamePattern attempts to interpret a query atom as 583 // {"pattern":<test name pattern string>}. 584 func (tnp *TestNamePattern) UnmarshalJSON(b []byte) error { 585 var data map[string]*json.RawMessage 586 if err := json.Unmarshal(b, &data); err != nil { 587 return err 588 } 589 patternMsg, ok := data["pattern"] 590 if !ok { 591 return errors.New(`Missing test name pattern property: "pattern"`) 592 } 593 var pattern string 594 if err := json.Unmarshal(*patternMsg, &pattern); err != nil { 595 return errors.New(`test name pattern property "pattern" is not a string`) 596 } 597 598 tnp.Pattern = pattern 599 600 return nil 601 } 602 603 // UnmarshalJSON for SubtestNamePattern attempts to interpret a query atom as 604 // {"subtest":<subtest name pattern string>}. 605 func (tnp *SubtestNamePattern) UnmarshalJSON(b []byte) error { 606 var data map[string]*json.RawMessage 607 if err := json.Unmarshal(b, &data); err != nil { 608 return err 609 } 610 subtestMsg, ok := data["subtest"] 611 if !ok { 612 return errors.New(`Missing subtest name pattern property: "subtest"`) 613 } 614 var subtest string 615 if err := json.Unmarshal(*subtestMsg, &subtest); err != nil { 616 return errors.New(`Subtest name property "subtest" is not a string`) 617 } 618 tnp.Subtest = subtest 619 620 return nil 621 } 622 623 // UnmarshalJSON for TestPath attempts to interpret a query atom as 624 // {"path":<test name pattern string>}. 625 func (tp *TestPath) UnmarshalJSON(b []byte) error { 626 var data map[string]*json.RawMessage 627 if err := json.Unmarshal(b, &data); err != nil { 628 629 return err 630 } 631 pathMsg, ok := data["path"] 632 if !ok { 633 return errors.New(`Missing test name path property: "path"`) 634 } 635 var path string 636 if err := json.Unmarshal(*pathMsg, &path); err != nil { 637 return errors.New(`Missing test name path property "path" is not a string`) 638 } 639 640 tp.Path = path 641 642 return nil 643 } 644 645 // UnmarshalJSON for TestStatusEq attempts to interpret a query atom as 646 // {"product": <browser name>, "status": <status string>}. 647 func (tse *TestStatusEq) UnmarshalJSON(b []byte) error { 648 var data struct { 649 BrowserName string `json:"browser_name"` // Legacy 650 Product string `json:"product"` 651 Status string `json:"status"` 652 } 653 if err := json.Unmarshal(b, &data); err != nil { 654 return err 655 } 656 if data.Product == "" && data.BrowserName != "" { 657 data.Product = data.BrowserName 658 } 659 if len(data.Status) == 0 { 660 return errors.New(`Missing test status constraint property: "status"`) 661 } 662 663 var product *shared.ProductSpec 664 if data.Product != "" { 665 p, err := shared.ParseProductSpec(data.Product) 666 if err != nil { 667 return err 668 } 669 product = &p 670 } 671 672 statusStr := strings.ToUpper(data.Status) 673 status := shared.TestStatusValueFromString(statusStr) 674 statusStr2 := status.String() 675 if statusStr != statusStr2 { 676 return fmt.Errorf(`Invalid test status: "%s"`, data.Status) 677 } 678 679 tse.Product = product 680 tse.Status = status 681 682 return nil 683 } 684 685 // UnmarshalJSON for TestStatusNeq attempts to interpret a query atom as 686 // {"product": <browser name>, "status": {"not": <status string>}}. 687 func (tsn *TestStatusNeq) UnmarshalJSON(b []byte) error { 688 var data struct { 689 BrowserName string `json:"browser_name"` // Legacy 690 Product string `json:"product"` 691 Status struct { 692 Not string `json:"not"` 693 } `json:"status"` 694 } 695 if err := json.Unmarshal(b, &data); err != nil { 696 return err 697 } 698 if data.Product == "" && data.BrowserName != "" { 699 data.Product = data.BrowserName 700 } 701 if len(data.Status.Not) == 0 { 702 return errors.New(`Missing test status constraint property: "status.not"`) 703 } 704 705 var product *shared.ProductSpec 706 if data.Product != "" { 707 p, err := shared.ParseProductSpec(data.Product) 708 if err != nil { 709 return err 710 } 711 product = &p 712 } 713 714 statusStr := strings.ToUpper(data.Status.Not) 715 status := shared.TestStatusValueFromString(statusStr) 716 statusStr2 := status.String() 717 if statusStr != statusStr2 { 718 return fmt.Errorf(`Invalid test status: "%s"`, data.Status) 719 } 720 721 tsn.Product = product 722 tsn.Status = status 723 724 return nil 725 } 726 727 // UnmarshalJSON for AbstractNot attempts to interpret a query atom as 728 // {"not": <abstract query>}. 729 func (n *AbstractNot) UnmarshalJSON(b []byte) error { 730 var data struct { 731 Not json.RawMessage `json:"not"` 732 } 733 if err := json.Unmarshal(b, &data); err != nil { 734 return err 735 } 736 if len(data.Not) == 0 { 737 return errors.New(`Missing negation property: "not"`) 738 } 739 740 q, err := unmarshalQ(data.Not) 741 n.Arg = q 742 743 return err 744 } 745 746 // UnmarshalJSON for AbstractOr attempts to interpret a query atom as 747 // {"or": [<abstract queries>]}. 748 func (o *AbstractOr) UnmarshalJSON(b []byte) error { 749 var data struct { 750 Or []json.RawMessage `json:"or"` 751 } 752 if err := json.Unmarshal(b, &data); err != nil { 753 return err 754 } 755 if len(data.Or) == 0 { 756 return errors.New(`Missing disjunction property: "or"`) 757 } 758 759 qs := make([]AbstractQuery, 0, len(data.Or)) 760 for _, msg := range data.Or { 761 q, err := unmarshalQ(msg) 762 if err != nil { 763 return err 764 } 765 qs = append(qs, q) 766 } 767 o.Args = qs 768 769 return nil 770 } 771 772 // UnmarshalJSON for AbstractAnd attempts to interpret a query atom as 773 // {"and": [<abstract queries>]}. 774 func (a *AbstractAnd) UnmarshalJSON(b []byte) error { 775 var data struct { 776 And []json.RawMessage `json:"and"` 777 } 778 if err := json.Unmarshal(b, &data); err != nil { 779 return err 780 } 781 if len(data.And) == 0 { 782 return errors.New(`Missing conjunction property: "and"`) 783 } 784 785 qs := make([]AbstractQuery, 0, len(data.And)) 786 for _, msg := range data.And { 787 q, err := unmarshalQ(msg) 788 if err != nil { 789 return err 790 } 791 qs = append(qs, q) 792 } 793 a.Args = qs 794 795 return nil 796 } 797 798 // UnmarshalJSON for AbstractExists attempts to interpret a query atom as 799 // {"exists": [<abstract queries>]}. 800 func (e *AbstractExists) UnmarshalJSON(b []byte) error { 801 var data struct { 802 Exists []json.RawMessage `json:"exists"` 803 } 804 if err := json.Unmarshal(b, &data); err != nil { 805 return err 806 } 807 if len(data.Exists) == 0 { 808 return errors.New(`Missing conjunction property: "exists"`) 809 } 810 811 qs := make([]AbstractQuery, 0, len(data.Exists)) 812 for _, msg := range data.Exists { 813 q, err := unmarshalQ(msg) 814 if err != nil { 815 return err 816 } 817 qs = append(qs, q) 818 } 819 e.Args = qs 820 821 return nil 822 } 823 824 // UnmarshalJSON for AbstractAll attempts to interpret a query atom as 825 // {"all": [<abstract query>]}. 826 func (e *AbstractAll) UnmarshalJSON(b []byte) error { 827 var data struct { 828 All []json.RawMessage `json:"all"` 829 } 830 if err := json.Unmarshal(b, &data); err != nil { 831 return err 832 } 833 if len(data.All) == 0 { 834 return errors.New(`Missing conjunction property: "all"`) 835 } 836 837 qs := make([]AbstractQuery, 0, len(data.All)) 838 for _, msg := range data.All { 839 q, err := unmarshalQ(msg) 840 if err != nil { 841 return err 842 } 843 qs = append(qs, q) 844 } 845 e.Args = qs 846 847 return nil 848 } 849 850 // UnmarshalJSON for AbstractNone attempts to interpret a query atom as 851 // {"none": [<abstract query>]}. 852 func (e *AbstractNone) UnmarshalJSON(b []byte) error { 853 var data struct { 854 None []json.RawMessage `json:"none"` 855 } 856 if err := json.Unmarshal(b, &data); err != nil { 857 return err 858 } 859 if len(data.None) == 0 { 860 return errors.New(`Missing conjunction property: "none"`) 861 } 862 863 qs := make([]AbstractQuery, 0, len(data.None)) 864 for _, msg := range data.None { 865 q, err := unmarshalQ(msg) 866 if err != nil { 867 return err 868 } 869 qs = append(qs, q) 870 } 871 e.Args = qs 872 873 return nil 874 } 875 876 // UnmarshalJSON for AbstractSequential attempts to interpret a query atom as 877 // {"exists": [<abstract queries>]}. 878 func (e *AbstractSequential) UnmarshalJSON(b []byte) error { 879 var data struct { 880 Sequential []json.RawMessage `json:"sequential"` 881 } 882 if err := json.Unmarshal(b, &data); err != nil { 883 return err 884 } 885 if len(data.Sequential) == 0 { 886 return errors.New(`Missing conjunction property: "sequential"`) 887 } 888 889 qs := make([]AbstractQuery, 0, len(data.Sequential)) 890 for _, msg := range data.Sequential { 891 q, err := unmarshalQ(msg) 892 if err != nil { 893 return err 894 } 895 qs = append(qs, q) 896 } 897 e.Args = qs 898 899 return nil 900 } 901 902 // UnmarshalJSON for AbstractCount attempts to interpret a query atom as 903 // {"count": int, "where": query}. 904 func (c *AbstractCount) UnmarshalJSON(b []byte) (err error) { 905 var data struct { 906 Count json.RawMessage `json:"count"` 907 Where json.RawMessage `json:"where"` 908 } 909 if err := json.Unmarshal(b, &data); err != nil { 910 return err 911 } 912 if len(data.Count) == 0 { 913 return errors.New(`Missing count property: "count"`) 914 } 915 if len(data.Where) == 0 { 916 return errors.New(`Missing count property: "where"`) 917 } 918 919 if err := json.Unmarshal(data.Count, &c.Count); err != nil { 920 return err 921 } 922 c.Where, err = unmarshalQ(data.Where) 923 if err != nil { 924 return err 925 } 926 927 return nil 928 } 929 930 // UnmarshalJSON for AbstractLessThan attempts to interpret a query atom as 931 // {"count": int, "where": query}. 932 func (l *AbstractLessThan) UnmarshalJSON(b []byte) error { 933 var data struct { 934 Count json.RawMessage `json:"lessThan"` 935 Where json.RawMessage `json:"where"` 936 } 937 if err := json.Unmarshal(b, &data); err != nil { 938 return err 939 } 940 if len(data.Count) == 0 { 941 return errors.New(`Missing lessThan property: "lessThan"`) 942 } 943 if len(data.Where) == 0 { 944 return errors.New(`Missing count property: "where"`) 945 } 946 947 err := json.Unmarshal(data.Count, &l.Count) 948 if err != nil { 949 return err 950 } 951 l.Where, err = unmarshalQ(data.Where) 952 if err != nil { 953 return err 954 } 955 956 return nil 957 } 958 959 // UnmarshalJSON for AbstractMoreThan attempts to interpret a query atom as 960 // {"count": int, "where": query}. 961 func (m *AbstractMoreThan) UnmarshalJSON(b []byte) (err error) { 962 var data struct { 963 Count json.RawMessage `json:"moreThan"` 964 Where json.RawMessage `json:"where"` 965 } 966 if err := json.Unmarshal(b, &data); err != nil { 967 return err 968 } 969 if len(data.Count) == 0 { 970 return errors.New(`Missing moreThan property: "moreThan"`) 971 } 972 if len(data.Where) == 0 { 973 return errors.New(`Missing count property: "where"`) 974 } 975 976 if err := json.Unmarshal(data.Count, &m.Count); err != nil { 977 return err 978 } 979 m.Where, err = unmarshalQ(data.Where) 980 if err != nil { 981 return err 982 } 983 984 return nil 985 } 986 987 // UnmarshalJSON for AbstractLink attempts to interpret a query atom as 988 // {"link":<metadata url pattern string>}. 989 func (l *AbstractLink) UnmarshalJSON(b []byte) error { 990 var data map[string]*json.RawMessage 991 if err := json.Unmarshal(b, &data); err != nil { 992 return err 993 } 994 patternMsg, ok := data["link"] 995 if !ok { 996 return errors.New(`Missing Link pattern property: "link"`) 997 } 998 var pattern string 999 if err := json.Unmarshal(*patternMsg, &pattern); err != nil { 1000 return errors.New(`Missing link pattern property "pattern" is not a string`) 1001 } 1002 1003 l.Pattern = pattern 1004 1005 return nil 1006 } 1007 1008 // UnmarshalJSON for AbstractTestLabel attempts to interpret a query atom as 1009 // {"label":<label string>}. 1010 func (t *AbstractTestLabel) UnmarshalJSON(b []byte) error { 1011 var data map[string]*json.RawMessage 1012 if err := json.Unmarshal(b, &data); err != nil { 1013 return err 1014 } 1015 labelMsg, ok := data["label"] 1016 if !ok { 1017 return errors.New(`Missing label pattern property: "label"`) 1018 } 1019 var label string 1020 if err := json.Unmarshal(*labelMsg, &label); err != nil { 1021 return errors.New(`Property "label" is not a string`) 1022 } 1023 1024 t.Label = label 1025 1026 return nil 1027 } 1028 1029 // TestWebFeatureAtom contains the parsed data from a "feature" query atom. 1030 type TestWebFeatureAtom struct { 1031 WebFeature string 1032 } 1033 1034 // UnmarshalJSON for TestWebFeatureAtom attempts to interpret a query atom as 1035 // {"feature":<web_feature_string>}. 1036 func (t *TestWebFeatureAtom) UnmarshalJSON(b []byte) error { 1037 var data map[string]*json.RawMessage 1038 if err := json.Unmarshal(b, &data); err != nil { 1039 return err 1040 } 1041 webFeatureMsg, ok := data["feature"] 1042 if !ok { 1043 return errors.New(`Missing web feature pattern property: "feature"`) 1044 } 1045 var webFeature string 1046 if err := json.Unmarshal(*webFeatureMsg, &webFeature); err != nil { 1047 return errors.New(`Property "feature" is not a string`) 1048 } 1049 1050 t.WebFeature = webFeature 1051 1052 return nil 1053 } 1054 1055 // UnmarshalJSON for AbstractTriaged attempts to interpret a query atom as 1056 // {"triaged":<browser name>}. 1057 func (t *AbstractTriaged) UnmarshalJSON(b []byte) error { 1058 var data map[string]*json.RawMessage 1059 if err := json.Unmarshal(b, &data); err != nil { 1060 return err 1061 } 1062 1063 browserNameMsg, ok := data["triaged"] 1064 if !ok { 1065 return errors.New(`Missing Triaged property: "triaged"`) 1066 } 1067 1068 var browserName string 1069 if err := json.Unmarshal(*browserNameMsg, &browserName); err != nil { 1070 return errors.New(`Triaged property "triaged" is not a string`) 1071 } 1072 1073 var product *shared.ProductSpec 1074 if browserName != "" { 1075 p, err := shared.ParseProductSpec(browserName) 1076 if err != nil { 1077 return err 1078 } 1079 product = &p 1080 } 1081 1082 t.Product = product 1083 1084 return nil 1085 } 1086 1087 // UnmarshalJSON for MetadataQuality attempts to interpret a query atom as 1088 // {"is":<metadata quality>}. 1089 func (q *MetadataQuality) UnmarshalJSON(b []byte) (err error) { 1090 var data map[string]*json.RawMessage 1091 if err := json.Unmarshal(b, &data); err != nil { 1092 return err 1093 } 1094 is, ok := data["is"] 1095 if !ok { 1096 return errors.New(`Missing "is" pattern property: "is"`) 1097 } 1098 var quality string 1099 if err := json.Unmarshal(*is, &quality); err != nil { 1100 return errors.New(`"is" property is not a string`) 1101 } 1102 1103 *q, err = MetadataQualityFromString(quality) 1104 1105 return err 1106 } 1107 1108 // MetadataQualityFromString returns the enum value for the given string. 1109 func MetadataQualityFromString(quality string) (MetadataQuality, error) { 1110 switch quality { 1111 case "different": 1112 return MetadataQualityDifferent, nil 1113 case "tentative": 1114 return MetadataQualityTentative, nil 1115 case "optional": 1116 return MetadataQualityOptional, nil 1117 } 1118 1119 return MetadataQualityUnknown, fmt.Errorf(`Unknown "is" quality "%s"`, quality) 1120 } 1121 1122 // nolint:ireturn // TODO: Fix ireturn lint error 1123 func unmarshalQ(b []byte) (AbstractQuery, error) { 1124 { 1125 var tnp TestNamePattern 1126 if err := json.Unmarshal(b, &tnp); err == nil { 1127 return tnp, nil 1128 } 1129 } 1130 { 1131 var stnp SubtestNamePattern 1132 if err := json.Unmarshal(b, &stnp); err == nil { 1133 return stnp, nil 1134 } 1135 } 1136 { 1137 var tp TestPath 1138 if err := json.Unmarshal(b, &tp); err == nil { 1139 return tp, nil 1140 } 1141 } 1142 { 1143 var tse TestStatusEq 1144 if err := json.Unmarshal(b, &tse); err == nil { 1145 return tse, nil 1146 } 1147 } 1148 { 1149 var tsn TestStatusNeq 1150 if err := json.Unmarshal(b, &tsn); err == nil { 1151 return tsn, nil 1152 } 1153 } 1154 { 1155 var n AbstractNot 1156 if err := json.Unmarshal(b, &n); err == nil { 1157 return n, nil 1158 } 1159 } 1160 { 1161 var o AbstractOr 1162 if err := json.Unmarshal(b, &o); err == nil { 1163 return o, nil 1164 } 1165 } 1166 { 1167 var a AbstractAnd 1168 if err := json.Unmarshal(b, &a); err == nil { 1169 return a, nil 1170 } 1171 } 1172 { 1173 var e AbstractExists 1174 if err := json.Unmarshal(b, &e); err == nil { 1175 return e, nil 1176 } 1177 } 1178 { 1179 var a AbstractAll 1180 if err := json.Unmarshal(b, &a); err == nil { 1181 return a, nil 1182 } 1183 } 1184 { 1185 var n AbstractNone 1186 if err := json.Unmarshal(b, &n); err == nil { 1187 return n, nil 1188 } 1189 } 1190 { 1191 var s AbstractSequential 1192 if err := json.Unmarshal(b, &s); err == nil { 1193 return s, nil 1194 } 1195 } 1196 { 1197 var c AbstractCount 1198 if err := json.Unmarshal(b, &c); err == nil { 1199 return c, nil 1200 } 1201 } 1202 { 1203 var c AbstractLessThan 1204 if err := json.Unmarshal(b, &c); err == nil { 1205 return c, nil 1206 } 1207 } 1208 { 1209 var c AbstractMoreThan 1210 if err := json.Unmarshal(b, &c); err == nil { 1211 return c, nil 1212 } 1213 } 1214 { 1215 var l AbstractLink 1216 if err := json.Unmarshal(b, &l); err == nil { 1217 return l, nil 1218 } 1219 } 1220 { 1221 var i MetadataQuality 1222 if err := json.Unmarshal(b, &i); err == nil { 1223 return i, nil 1224 } 1225 } 1226 { 1227 var t AbstractTriaged 1228 if err := json.Unmarshal(b, &t); err == nil { 1229 return t, nil 1230 } 1231 } 1232 { 1233 var t AbstractTestLabel 1234 if err := json.Unmarshal(b, &t); err == nil { 1235 return t, nil 1236 } 1237 } 1238 { 1239 var atom TestWebFeatureAtom 1240 if err := json.Unmarshal(b, &atom); err == nil { 1241 return AbstractTestWebFeature{ 1242 TestWebFeatureAtom: atom, 1243 manifestFetcher: searchcacheWebFeaturesManifestFetcher{}, 1244 }, nil 1245 } 1246 } 1247 const docsFilePath = "wpt.fyi/api/query/README.md" 1248 errorMsg := fmt.Sprintf("Failed to parse query fragment as any of the existing search atoms in %s", docsFilePath) 1249 1250 return nil, errors.New(errorMsg) 1251 }