1 // Written in the D programming language. 2 /** 3 Haystack filter. 4 5 Copyright: Copyright (c) 2017, Radu Racariu <radu.racariu@gmail.com> 6 License: $(LINK2 www.boost.org/LICENSE_1_0.txt, Boost License 1.0) 7 Authors: Radu Racariu 8 **/ 9 module haystack.filter; 10 11 import std.algorithm : move; 12 import std.functional : equalTo, lessThan, greaterThan; 13 14 import haystack.tag; 15 import haystack.util.misc : isCharInputRange, Own; 16 import haystack.zinc.lexer; 17 18 /// Filter parsing exception 19 class FilterException : Exception 20 { 21 immutable this(string msg) 22 { 23 super(msg); 24 } 25 } 26 27 /// An empty resolver 28 alias EmptyResolver = Path.emptyResolver!Dict; 29 30 /// The default string based haystack filter 31 alias HaystackFilter = Filter!string; 32 33 /** 34 Haystack filter 35 */ 36 struct Filter(Range) 37 if (isCharInputRange!Range) 38 { 39 alias Lexer = ZincLexer!Range; 40 41 this(Range r) 42 { 43 auto lexer = Lexer(r); 44 if (lexer.empty) 45 throw InvalidFilterException; 46 or = parseOr(lexer); 47 } 48 49 this(ref return scope typeof(this) other) 50 { 51 this.or = Or(other.or); 52 } 53 54 @disable this(); 55 56 bool eval(Obj, Resolver)(Obj obj, Resolver resolver) const 57 { 58 return or.eval(obj, resolver); 59 } 60 61 size_t toHash() const nothrow 62 { 63 return or.toHash(); 64 } 65 66 bool opEquals()(auto ref const Filter other) const 67 { 68 return or == other.or; 69 } 70 71 private: 72 Or or; // start node 73 74 // parse `or` expression 75 Or parseOr(ref Lexer lexer, bool group = false) 76 { 77 auto a = parseAnd(lexer); 78 for (; !lexer.empty; lexer.popFront()) 79 { 80 if (lexer.front.isSpace) 81 continue; 82 if (lexer.front.isOf(TokenType.id, "or".tag)) 83 { 84 lexer.popFront(); 85 auto b = parseOr(lexer, group); 86 return Or(move(a), move(b)); 87 } 88 else if (lexer.front.isEmpty && !group) 89 { 90 throw InvalidFilterException; 91 } 92 else 93 { 94 break; 95 } 96 } 97 return Or(move(a)); 98 } 99 100 // parse `and` expression 101 And parseAnd(ref Lexer lexer) 102 { 103 auto a = parseTerm(lexer); 104 for (; !lexer.empty; lexer.popFront()) 105 { 106 if (lexer.front.isSpace) 107 continue; 108 if (lexer.front.isOf(TokenType.id, "and".tag)) 109 { 110 lexer.popFront(); 111 auto b = parseAnd(lexer); 112 return And(move(a), move(b)); 113 } 114 else 115 { 116 break; 117 } 118 } 119 return And(move(a)); 120 } 121 122 // parse a term 123 Term parseTerm(ref Lexer lexer) 124 { 125 import std.traits : EnumMembers; 126 127 enum State { parens, has, missing, cmp } 128 State state; 129 Path crtPath = void; 130 131 for (; !lexer.empty; lexer.popFront()) 132 { 133 parseStart: 134 if (lexer.front.isSpace) 135 continue; 136 switch (state) 137 { 138 case State.parens: 139 if (!lexer.front.hasChr('(')) 140 { 141 state = State.has; 142 goto parseStart; 143 } 144 else 145 { 146 lexer.popFront(); 147 auto term = Term(parseOr(lexer, true)); 148 if (!lexer.front.hasChr(')')) 149 throw InvalidFilterException; 150 lexer.popFront(); 151 return move(term); 152 } 153 154 case State.has: 155 if (!lexer.front.isId) 156 { 157 throw InvalidFilterException; 158 } 159 else 160 { 161 auto name = lexer.front.value!Str; 162 if (name == "not") 163 { 164 state = State.missing; 165 continue; 166 } 167 crtPath = parsePath(lexer); 168 state = State.cmp; 169 goto parseStart; 170 } 171 172 case State.missing: 173 auto path = parsePath(lexer); 174 return Term(Missing(path)); 175 176 case State.cmp: 177 auto chr = lexer.front.isEmpty ? lexer.front.curChar : dchar.init; 178 bool hasEq; 179 if (chr == '<' || chr == '>' || chr == '=' || chr == '!') 180 { 181 if (lexer.empty) 182 throw InvalidFilterException; 183 lexer.popFront(); 184 if (lexer.front.hasChr('=')) 185 { 186 hasEq = true; 187 lexer.popFront(); 188 } 189 if (lexer.empty || (chr == '=' && !hasEq)) 190 throw InvalidFilterException; 191 192 for (; !lexer.empty; lexer.popFront()) 193 { 194 if (lexer.front.isSpace) 195 continue; 196 if (lexer.front.isScalar || lexer.front.type == TokenType.id) 197 { 198 Tag tag = cast(Tag) lexer.front.tag; 199 if (tag.hasValue!Num) 200 { 201 auto num = tag.get!Num; 202 if (num.isNaN || num.isINF) 203 throw InvalidFilterException; 204 } 205 if (lexer.front.type == TokenType.id) // parse true - false 206 { 207 auto id = tag.get!Str; 208 if (id == "true") 209 tag = true.tag; 210 else if (id == "false") 211 tag = false.tag; 212 else 213 throw InvalidFilterException; 214 } 215 lexer.popFront(); 216 217 string op; 218 foreach (opEnum; EnumMembers!(Cmp.Op)) 219 { 220 if (!hasEq) 221 { 222 if (opEnum[0] == chr) 223 { 224 op = opEnum; 225 break; 226 } 227 } 228 else 229 { 230 if (opEnum[0] == chr && opEnum[$ - 1] == '=') 231 { 232 op = opEnum; 233 break; 234 } 235 } 236 } 237 return Term(Cmp(crtPath, op, tag)); 238 } 239 else // invalid term 240 break; 241 } 242 throw InvalidFilterException; 243 } 244 else 245 { 246 return Term(Has(crtPath)); 247 } 248 249 default: 250 throw InvalidFilterException; 251 } 252 } 253 return Term.makeEmpty(); 254 } 255 256 // parse a Path 257 Path parsePath(ref Lexer lexer) 258 { 259 import std.array : appender; 260 261 auto buf = appender!(string[])(); 262 enum State { id, sep } 263 State state; 264 265 loop: 266 for (; !lexer.empty; lexer.popFront()) 267 { 268 switch (state) 269 { 270 case State.id: 271 if (!lexer.front.isId) 272 throw InvalidFilterException; 273 274 auto name = lexer.front.value!Str; 275 buf.put(name.val); 276 state = State.sep; 277 break; 278 279 case State.sep: 280 if (!lexer.front.hasChr('-')) 281 break loop; 282 if (lexer.empty) 283 throw InvalidFilterException; 284 lexer.popFront(); 285 if (lexer.front.hasChr('>') && !lexer.empty) 286 state = State.id; 287 else 288 throw InvalidFilterException; 289 break; 290 291 default: 292 throw InvalidFilterException; 293 } 294 } 295 return Path(buf.data); 296 } 297 298 static InvalidFilterException = new immutable FilterException("Invalid filter input."); 299 } 300 301 unittest 302 { 303 alias StrFilter = Filter!(string); 304 305 auto filter = StrFilter("id or bar"); 306 assert(filter.eval(["id": marker], &EmptyResolver)); 307 assert(filter.eval(["bar": marker], &EmptyResolver)); 308 309 filter = StrFilter("not bar"); 310 assert(filter.eval(["id": marker], &EmptyResolver)); 311 assert(!filter.eval(["bar": marker], &EmptyResolver)); 312 313 filter = StrFilter("test == true"); 314 assert(filter.eval(["test": true.tag], &EmptyResolver)); 315 316 import std.exception : assertThrown; 317 assertThrown(StrFilter("test = ").eval(["foo": marker], &EmptyResolver)); 318 assertThrown(StrFilter("test('call')").eval(["null": marker], &EmptyResolver)); 319 320 321 filter = StrFilter("age == 6"); 322 assert(filter.eval(["age": 6.tag], &EmptyResolver)); 323 assert(!filter.eval(["bar": marker], &EmptyResolver)); 324 325 filter = StrFilter("age == 6 and foo"); 326 assert(filter.eval(["age": 6.tag, "foo": marker], &EmptyResolver)); 327 328 filter = StrFilter("(age and foo)"); 329 assert(filter.eval(["age": 6.tag, "foo": marker], &EmptyResolver)); 330 331 filter = StrFilter(`name == "foo bar"`); 332 assert(filter.eval(["name": "foo bar".tag], &EmptyResolver)); 333 334 filter = StrFilter(`name >= "foo bar"`); 335 assert(filter.eval(["name": "foo bar".tag], &EmptyResolver)); 336 337 filter = StrFilter(`a and b or foo`); 338 assert(filter.eval(["foo": marker], &EmptyResolver)); 339 340 filter = StrFilter(`a or b or foo`); 341 assert(filter.eval(["foo": marker], &EmptyResolver)); 342 343 filter = StrFilter(`a and b and c`); 344 assert(filter.eval(["a": marker, "b": marker, "c": marker], &EmptyResolver)); 345 346 filter = StrFilter(`a and b and c or d`); 347 assert(filter.eval(["d": marker], &EmptyResolver)); 348 349 filter = StrFilter(`(a or b) and c`); 350 assert(filter.eval(["b": marker, "c": marker], &EmptyResolver)); 351 352 assert(StrFilter(`a or b`) == StrFilter(`a or b`)); 353 354 filter = StrFilter("age == 6 and foo"); 355 StrFilter* filterCopy = new StrFilter(filter); 356 assert(filter.eval(["foo": marker, "age": 6.tag], &EmptyResolver)); 357 assert(filterCopy.eval(["foo": marker, "age": 6.tag], &EmptyResolver)); 358 359 assert(filterCopy.toHash() == filter.toHash()); 360 assert(*filterCopy == filter); 361 362 static struct PtrWrap 363 { 364 StrFilter* p; 365 366 size_t toHash() const nothrow 367 { 368 return p.toHash(); 369 } 370 371 bool opEquals()(auto ref const scope PtrWrap other) const scope 372 in (p !is null && other.p !is null) 373 { 374 return *p == *other.p; 375 } 376 } 377 378 string[PtrWrap] map; 379 map[PtrWrap(filterCopy)] = "a"; 380 assert(PtrWrap(&filter) in map); 381 } 382 383 /** 384 Or condition 385 */ 386 struct Or 387 { 388 And a; 389 Own!Or b; 390 391 this(And a) 392 { 393 this.a = move(a); 394 } 395 396 this(And a, Or b) 397 { 398 this.a = move(a); 399 this.b = move(b); 400 } 401 402 this(ref return scope Or other) 403 { 404 this.a = And(other.a); 405 if (other.b.isNull) 406 return; 407 this.b = Or(*other.b); 408 } 409 410 @disable this(this); 411 412 bool eval(Obj, Resolver)(Obj obj, Resolver resolver) const 413 { 414 assert(a.isValid, "Invalid 'or' expression."); 415 if (!b.isNull) 416 return a.eval(obj, resolver) || b.eval(obj, resolver); 417 else 418 return a.eval(obj, resolver); 419 } 420 421 size_t toHash() const nothrow @trusted 422 { 423 enum prime = 31; 424 size_t hash = prime * a.toHash(); 425 return prime * hash + (b.isNull ? 0 : b.toHash()); 426 } 427 428 bool opEquals()(auto ref const Or other) const nothrow 429 { 430 return a == other.a && b == other.b; 431 } 432 } 433 434 /** 435 And condition 436 */ 437 struct And 438 { 439 Term a; 440 Own!And b; 441 442 this(Term a) 443 { 444 this.a = move(a); 445 } 446 447 this(Term a, And b) 448 { 449 this.a = move(a); 450 this.b = move(b); 451 } 452 453 this(ref return scope And other) 454 { 455 this.a = Term(other.a); 456 if (other.b.isNull) 457 return; 458 this.b = And(*other.b); 459 } 460 461 @disable this(this); 462 463 @property bool isValid() const 464 { 465 return a.isValid; 466 } 467 468 bool eval(Obj, Resolver)(Obj obj, Resolver resolver) const 469 { 470 assert(a.isValid, "Invalid 'and' expression."); 471 if (!b.isNull && b.isValid) 472 return a.eval(obj, resolver) && b.eval(obj, resolver); 473 else 474 return a.eval(obj, resolver); 475 } 476 477 @safe size_t toHash() const nothrow 478 { 479 enum prime = 31; 480 size_t hash = prime * a.toHash(); 481 return prime * hash + (b.isNull ? 0 : b.toHash()); 482 } 483 484 bool opEquals()(auto ref const And other) const nothrow 485 { 486 return a == other.a && b == other.b; 487 } 488 } 489 490 /** 491 A filter term 492 */ 493 struct Term 494 { 495 enum Type 496 { 497 or, 498 has, 499 missing, 500 cmp, 501 empty 502 } 503 504 this(Or or) 505 { 506 type = Type.or; 507 val.or = or.move(); 508 } 509 510 this(Has has) 511 { 512 type = Type.has; 513 val.has = has; 514 } 515 516 this(Missing missing) 517 { 518 type = Type.missing; 519 val.missing = missing; 520 } 521 522 this(Cmp cmp) 523 { 524 type = Type.cmp; 525 val.cmp = cmp; 526 } 527 528 this(ref return scope Term other) 529 { 530 this.type = other.type; 531 final switch (type) 532 { 533 case Type.or: 534 this.val.or = Or(*other.val.or); 535 break; 536 case Type.has: 537 this.val.has = Has(other.val.has); 538 break; 539 case Type.missing: 540 this.val.missing = Missing(other.val.missing); 541 break; 542 case Type.cmp: 543 this.val.cmp = Cmp(other.val.cmp); 544 break; 545 case Type.empty: 546 break; 547 } 548 } 549 550 static Term makeEmpty() 551 { 552 return Term(Type.empty); 553 } 554 555 bool eval(Obj, Resolver)(Obj obj, Resolver resolver) const 556 { 557 final switch (type) 558 { 559 case Type.or: 560 return val.or.eval(obj, resolver); 561 case Type.has: 562 return val.has.eval(obj, resolver); 563 case Type.missing: 564 return val.missing.eval(obj, resolver); 565 case Type.cmp: 566 return val.cmp.eval(obj, resolver); 567 case Type.empty: 568 return false; 569 } 570 } 571 572 @trusted size_t toHash() const nothrow 573 { 574 final switch (type) 575 { 576 case Type.or: 577 return val.or.toHash(); 578 case Type.has: 579 return val.has.toHash(); 580 case Type.missing: 581 return val.missing.toHash(); 582 case Type.cmp: 583 return val.cmp.toHash(); 584 case Type.empty: 585 return 31; 586 } 587 } 588 589 bool opEquals()(auto ref const Term other) const nothrow 590 { 591 if (other.type != type) 592 return false; 593 594 final switch (type) 595 { 596 case Type.or: 597 return val.or == other.val.or; 598 case Type.has: 599 return val.has == other.val.has; 600 case Type.missing: 601 return val.missing == other.val.missing; 602 case Type.cmp: 603 return val.cmp == other.val.cmp; 604 case Type.empty: 605 return true; 606 } 607 } 608 609 @property bool isValid() const 610 { 611 return type != Type.empty; 612 } 613 614 ~this() 615 { 616 final switch (type) 617 { 618 case Type.or: 619 val.or.destroy(); break; 620 case Type.has: 621 val.has.destroy(); break; 622 case Type.missing: 623 val.missing.destroy(); break; 624 case Type.cmp: 625 val.cmp.destroy(); break; 626 case Type.empty: 627 break; 628 } 629 } 630 631 private: 632 633 @disable this(); 634 @disable this(this); 635 636 this(Type type) 637 { 638 this.type = type; 639 } 640 641 union Val 642 { 643 Own!Or or = void; 644 Has has = void; 645 Missing missing = void; 646 Cmp cmp = void; 647 } 648 649 Type type = Type.empty; 650 Val val = void; 651 } 652 653 /** 654 A filter path. 655 Can be a simple path that resolves to a dict, 656 or a chained path that resolves across multiple dicts 657 */ 658 struct Path 659 { 660 this (string name) 661 { 662 this._segments = [name]; 663 } 664 665 this(string[] segments) 666 { 667 this._segments = segments; 668 } 669 @disable this(); 670 671 Tag resolve(Obj, Resolver)(Obj obj, Resolver resolver) const 672 { 673 import std.traits : Unqual, isAssociativeArray, KeyType, ValueType; 674 675 static if (isAssociativeArray!Obj) 676 alias Type = Unqual!(ValueType!Obj)[Unqual!(KeyType!Obj)]; 677 else 678 alias Type = Unqual!Obj; 679 680 if (segments.length == 0 || segments[0].length == 0) 681 return Tag.init; 682 683 static if (is(Type : Dict)) 684 { 685 if (segments.length == 1) 686 return dictResolver(obj); 687 else if (resolver !is null) 688 return resolver(obj, this); 689 else 690 return Tag.init; 691 } 692 else 693 { 694 return resolver(obj, this); 695 } 696 } 697 698 @property string root() const 699 { 700 return _segments[0]; 701 } 702 703 @property ref const(string[]) segments() const 704 { 705 return _segments; 706 } 707 708 static Tag emptyResolver(Obj)(Obj, ref const(Path)) 709 { 710 return Tag.init; 711 } 712 713 size_t toHash() const nothrow 714 { 715 enum prime = 31; 716 size_t hash = prime; 717 foreach (seg; _segments) 718 foreach (c; seg) 719 hash = (hash * prime) + c; 720 return hash; 721 } 722 723 bool opEquals()(auto ref const Path other) const nothrow 724 { 725 return _segments == other._segments; 726 } 727 728 private: 729 730 Tag dictResolver(const(Dict) dict) const 731 in (segments.length) 732 { 733 return dict.get(root, Tag.init); 734 } 735 736 string[] _segments; 737 } 738 unittest 739 { 740 auto path = Path("test"); 741 auto dictResolver = &Path.emptyResolver!Dict; 742 assert(path.resolve(["test": marker], dictResolver) == marker); 743 auto range = ["test": marker].byKeyValue(); 744 Tag rangeResolver (typeof(range) obj, ref const(Path) path) 745 { 746 import std.algorithm : find; 747 return obj.find!(kv => kv.key == path.root).front.value; 748 } 749 assert(path.resolve(range, &rangeResolver) == marker); 750 751 Dict equip = ["id":"equip".tag, "name": "foobar".tag]; 752 753 Tag resolver(ref const(Dict) dict, ref const(Path) path) 754 { 755 foreach (i, ref p; path.segments) 756 { 757 if (i == 0) 758 { 759 if (!dict.has(p) || equip["id"] != dict[p]) 760 break; 761 } 762 if (i == 1) 763 { 764 return equip.get(p, Tag.init); 765 } 766 } 767 768 return Tag.init; 769 } 770 path = Path(["equipRef", "name"]); 771 assert(path.resolve(["equipRef": "equip".tag], &resolver) == "foobar".tag); 772 773 } 774 775 /////////////////////////////////////////////////////////////// 776 // Basic predicates 777 /////////////////////////////////////////////////////////////// 778 779 /** 780 Dict has the path 781 */ 782 struct Has 783 { 784 this(string path) 785 { 786 this.path = Path(path); 787 } 788 789 this(Path path) 790 { 791 this.path = path; 792 } 793 794 this(ref return scope Has other) 795 { 796 this.path = other.path; 797 } 798 @disable this(); 799 800 bool eval(Obj, Resolver)(Obj obj, Resolver resolver) const 801 { 802 return path.resolve(obj, resolver).hasValue; 803 } 804 805 @property Dict tags() 806 { 807 return [path.root: marker]; 808 } 809 810 size_t toHash() const nothrow 811 { 812 return 31 * path.toHash(); 813 } 814 815 bool opEquals()(auto ref const Has other) const nothrow 816 { 817 return path == other.path; 818 } 819 820 private: 821 Path path; 822 } 823 unittest 824 { 825 auto has = Has("foo"); 826 assert(has.eval(["foo":marker], &EmptyResolver)); 827 assert(has.tags == ["foo":marker]); 828 } 829 830 /** 831 Dict missing the path 832 */ 833 struct Missing 834 { 835 this(string path) 836 { 837 this.has = Path(path); 838 } 839 840 this(Path path) 841 { 842 this.has = path; 843 } 844 845 this(ref return scope Missing other) 846 { 847 this.has = Has(other.has); 848 } 849 @disable this(); 850 851 bool eval(Obj, Resolver)(Obj obj, Resolver resolver) const 852 { 853 return !has.eval(obj, resolver); 854 } 855 856 alias has this; 857 Has has; 858 } 859 unittest 860 { 861 auto missing = Missing("foo"); 862 assert(missing.eval(["bar": marker], &EmptyResolver)); 863 assert(missing.tags == ["foo": marker]); 864 } 865 866 /** 867 Dict has the path that satisfies the predicate 868 */ 869 struct Cmp 870 { 871 enum Op : string 872 { 873 eq = "==", 874 notEq = "!=", 875 less = "<", 876 lessOrEq = "<=", 877 greater = ">", 878 greaterOrEq = ">=" 879 } 880 881 this(string path, string op, Tag constant) 882 { 883 this(Path(path), cast(Op) op, constant); 884 } 885 886 this(Path path, string op, Tag constant) 887 { 888 this(path, cast(Op) op, constant); 889 } 890 891 this(Path path, Op op, Tag constant) 892 { 893 this.path = path; 894 this.op = op; 895 this.constant = constant; 896 } 897 898 this(ref return scope Cmp other) 899 { 900 this.path = other.path; 901 this.op = other.op; 902 this.constant = other.constant; 903 } 904 @disable this(); 905 906 bool eval(Obj, Resolver)(Obj obj, Resolver resolver) const 907 { 908 auto val = path.resolve(obj, resolver); 909 return predicate(val); 910 } 911 912 @property Dict tags() 913 { 914 foreach (Type; Tag.AllowedTypes) 915 { 916 if (constant.hasValue!Type) 917 return [path.segments[$ - 1]: Tag(Type.init)]; 918 } 919 return [path.segments[$]: marker]; 920 } 921 922 size_t toHash() const nothrow 923 { 924 enum prime = 31; 925 size_t hash = prime * path.toHash(); 926 foreach (c; op) 927 hash = (hash * prime) + c; 928 return prime * hash + constant.toHash(); 929 } 930 931 bool opEquals()(auto ref const Cmp other) const nothrow 932 { 933 if (op != other.op || path != other.path) 934 return false; 935 try 936 return constant == other.constant; 937 catch (Exception e) 938 return false; 939 } 940 941 private: 942 943 bool predicate(ref const(Tag) cmp) const 944 { 945 if (!cmp.hasValue) 946 return false; 947 948 final switch(op) 949 { 950 case Op.eq: 951 return equalTo(cmp, constant); 952 case Op.notEq: 953 return !equalTo(cmp, constant); 954 case Op.less: 955 return lessThan(cmp, constant); 956 case Op.lessOrEq: 957 return lessThan(cmp, constant) || equalTo(cmp, constant); 958 case Op.greater: 959 return greaterThan(cmp, constant); 960 case Op.greaterOrEq: 961 return greaterThan(cmp, constant) || equalTo(cmp, constant); 962 } 963 } 964 965 Path path; 966 Op op; 967 Tag constant; 968 } 969 unittest 970 { 971 auto cmp = Cmp("val", "==", true.tag); 972 assert(cmp.eval(["val": true.tag], &EmptyResolver)); 973 assert(cmp.tags == ["val": Bool.init.tag]); 974 975 cmp = Cmp("val", "!=", true.tag); 976 assert(cmp.eval(["val": false.tag], &EmptyResolver)); 977 978 cmp = Cmp("val", ">", false.tag); 979 assert(cmp.eval(["val": true.tag], &EmptyResolver)); 980 981 cmp = Cmp("val", ">", false.tag); 982 assert(cmp.eval(["val": true.tag], &EmptyResolver)); 983 984 cmp = Cmp("val", "==", 1.tag); 985 assert(cmp.eval(["val": 1.tag], &EmptyResolver)); 986 987 cmp = Cmp("val", "!=", 1.tag); 988 assert(cmp.eval(["val": 0.tag], &EmptyResolver)); 989 990 cmp = Cmp("val", ">", 100.tag); 991 assert(cmp.eval(["val": 999.tag], &EmptyResolver)); 992 993 cmp = Cmp("val", "<=", 100.tag); 994 assert(cmp.eval(["val": 100.tag], &EmptyResolver)); 995 996 cmp = Cmp("val", "<", 100.tag); 997 assert(cmp.eval(["val": 99.tag], &EmptyResolver)); 998 999 cmp = Cmp("val", ">=", 99.tag); 1000 assert(cmp.eval(["val": 99.tag], &EmptyResolver)); 1001 1002 cmp = Cmp("val", ">=", "foo".tag); 1003 assert(cmp.eval(["val": "foo".tag], &EmptyResolver)); 1004 1005 cmp = Cmp("val", ">", "fo".tag); 1006 assert(cmp.eval(["val": "foo".tag], &EmptyResolver)); 1007 }