qo 0.1.6 → 0.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +179 -17
- data/docs/.gitkeep +0 -0
- data/docs/_config.yml +1 -0
- data/lib/qo.rb +57 -8
- data/lib/qo/matcher.rb +10 -3
- data/lib/qo/version.rb +1 -1
- metadata +4 -2
    
        checksums.yaml
    CHANGED
    
    | @@ -1,7 +1,7 @@ | |
| 1 1 | 
             
            ---
         | 
| 2 2 | 
             
            SHA1:
         | 
| 3 | 
            -
              metadata.gz:  | 
| 4 | 
            -
              data.tar.gz:  | 
| 3 | 
            +
              metadata.gz: c98fa2c3b82ac4167eba17b183fe1cea46a0e36f
         | 
| 4 | 
            +
              data.tar.gz: ab2527c365781b54fac1290ac64e8a17acdcd07c
         | 
| 5 5 | 
             
            SHA512:
         | 
| 6 | 
            -
              metadata.gz:  | 
| 7 | 
            -
              data.tar.gz:  | 
| 6 | 
            +
              metadata.gz: f1e9b886e694b8c08f212209db40bc8ca1276b738d1d30c81040e0b651f264bcfc9c83a21d82d96f2ff418d05def3e5abae82a29394f184266fab424f5dad4a6
         | 
| 7 | 
            +
              data.tar.gz: ab226ea4d673ea462b2e41084d25c8a61b2c04e37e8aac9354745b1b17c4ae54b5efa6184ae28e0201fa453b525938f4d8f260912300a0f145cc0992607424e1
         | 
    
        data/README.md
    CHANGED
    
    | @@ -14,6 +14,38 @@ Fast forward a few months and I kind of wanted to make it real, so here it is. I | |
| 14 14 |  | 
| 15 15 | 
             
            ## Usage
         | 
| 16 16 |  | 
| 17 | 
            +
            ### Quick Start
         | 
| 18 | 
            +
             | 
| 19 | 
            +
            Qo is used for pattern matching in Ruby. All Qo matchers respond to `===` and `to_proc` meaning they can be used with `case` and Enumerable functions alike:
         | 
| 20 | 
            +
             | 
| 21 | 
            +
             | 
| 22 | 
            +
            ```ruby
         | 
| 23 | 
            +
            case ['Foo', 42]
         | 
| 24 | 
            +
            when Qo[:*, 42] then 'Truly the one answer'
         | 
| 25 | 
            +
            else nil
         | 
| 26 | 
            +
            end
         | 
| 27 | 
            +
             | 
| 28 | 
            +
            # Run a select like an AR query, getting the age attribute against a range
         | 
| 29 | 
            +
            people.select(&Qo[age: 18..30])
         | 
| 30 | 
            +
             | 
| 31 | 
            +
            # How about some "right-hand assignment" pattern matching
         | 
| 32 | 
            +
            name_longer_than_three      = -> person { person.name.size > 3 }
         | 
| 33 | 
            +
            people_with_truncated_names = people.map(&Qo.match_fn(
         | 
| 34 | 
            +
              Qo.m(name_longer_than_three) { |person| Person.new(person.name[0..2], person.age) },
         | 
| 35 | 
            +
              Qo.m(:*) # Identity function, catch-all
         | 
| 36 | 
            +
            ))
         | 
| 37 | 
            +
             | 
| 38 | 
            +
            # And standalone like a case:
         | 
| 39 | 
            +
            Qo.match(people.first,
         | 
| 40 | 
            +
              Qo.m(age: 10..19) { |person| "#{person.name} is a teen that's #{person.age} years old" },
         | 
| 41 | 
            +
              Qo.m(:*) { |person| "#{person.name} is #{person.age} years old" }
         | 
| 42 | 
            +
            )
         | 
| 43 | 
            +
            ```
         | 
| 44 | 
            +
             | 
| 45 | 
            +
            Get a lot more expressiveness in your queries and transformations. Read on for the full details.
         | 
| 46 | 
            +
             | 
| 47 | 
            +
            ### Qo'isms
         | 
| 48 | 
            +
             | 
| 17 49 | 
             
            Qo supports three main types of queries: `and`, `or`, and `not`.
         | 
| 18 50 |  | 
| 19 51 | 
             
            Most examples are written in terms of `and` and its alias `[]`. `[]` is mostly used for portable syntax:
         | 
| @@ -62,6 +94,12 @@ Qo has a concept of a Wildcard, `:*`, which will match against any value | |
| 62 94 | 
             
            Qo[:*, :*] === ['Robert', 22] # true
         | 
| 63 95 | 
             
            ```
         | 
| 64 96 |  | 
| 97 | 
            +
            A single wildcard will match anything, and can frequently be used as an always true:
         | 
| 98 | 
            +
             | 
| 99 | 
            +
            ```ruby
         | 
| 100 | 
            +
            Qo[:*] === :literally_anything_here
         | 
| 101 | 
            +
            ```
         | 
| 102 | 
            +
             | 
| 65 103 | 
             
            ### 2 - Array Matching
         | 
| 66 104 |  | 
| 67 105 | 
             
            The first way a Qo matcher can be defined is by using `*varargs`:
         | 
| @@ -96,7 +134,7 @@ case ['Roberta', 22] | |
| 96 134 | 
             
            when Qo[:*, :*] then 'it matched'
         | 
| 97 135 | 
             
            else 'will not ever be reached'
         | 
| 98 136 | 
             
            end
         | 
| 99 | 
            -
            # => ' | 
| 137 | 
            +
            # => 'it matched'
         | 
| 100 138 |  | 
| 101 139 | 
             
            # Select
         | 
| 102 140 |  | 
| @@ -140,7 +178,7 @@ dirty_values = [nil, '', true] | |
| 140 178 | 
             
            # Standalone
         | 
| 141 179 |  | 
| 142 180 | 
             
            Qo[:nil?] === [nil]
         | 
| 143 | 
            -
            # => true
         | 
| 181 | 
            +
            # => true, though you could also just use Qo[nil]
         | 
| 144 182 |  | 
| 145 183 | 
             
            # Case statement
         | 
| 146 184 |  | 
| @@ -148,7 +186,7 @@ case ['Roberta', nil] | |
| 148 186 | 
             
            when Qo[:*, :nil?] then 'no age'
         | 
| 149 187 | 
             
            else 'not sure'
         | 
| 150 188 | 
             
            end
         | 
| 151 | 
            -
            # => ' | 
| 189 | 
            +
            # => 'no age'
         | 
| 152 190 |  | 
| 153 191 | 
             
            # Select
         | 
| 154 192 |  | 
| @@ -212,7 +250,7 @@ end | |
| 212 250 |  | 
| 213 251 | 
             
            # Reject
         | 
| 214 252 |  | 
| 215 | 
            -
            [nil, '', 10, 'string'].reject(&Qo.or( | 
| 253 | 
            +
            [nil, '', 10, 'string'].reject(&Qo.or(:nil?, :empty?))
         | 
| 216 254 | 
             
            # => [10, "string"]
         | 
| 217 255 | 
             
            ```
         | 
| 218 256 |  | 
| @@ -221,16 +259,58 @@ end | |
| 221 259 | 
             
            #### 3.1 - Hash matched against a Hash
         | 
| 222 260 |  | 
| 223 261 | 
             
            1. Does the key exist on the other hash?
         | 
| 224 | 
            -
            2.  | 
| 225 | 
            -
            3.  | 
| 226 | 
            -
            4. Does the target object's value  | 
| 227 | 
            -
            5.  | 
| 262 | 
            +
            2. Are the match value and match target hashes?
         | 
| 263 | 
            +
            3. Was a wildcard value provided?
         | 
| 264 | 
            +
            4. Does the target object's value case match against the match value?
         | 
| 265 | 
            +
            5. Does the target object's value predicate match against the match value?
         | 
| 266 | 
            +
            6. What about the String version of the match key? Abort if it can't coerce.
         | 
| 228 267 |  | 
| 229 268 | 
             
            ##### 3.1.1 - Key present
         | 
| 230 269 |  | 
| 231 270 | 
             
            Checks to see if the key is even present on the other object, false if not.
         | 
| 232 271 |  | 
| 233 | 
            -
            ##### 3.1.2 -  | 
| 272 | 
            +
            ##### 3.1.2 - Match value and target are hashes
         | 
| 273 | 
            +
             | 
| 274 | 
            +
            If both the match value (`match_key: match_value`) and the match target are hashes, Qo will begin a recursive descent starting at the match key until it finds a matcher to try out:
         | 
| 275 | 
            +
             | 
| 276 | 
            +
            ```ruby
         | 
| 277 | 
            +
            Qo[a: {b: {c: 5..15}}] === {a: {b: {c: 10}}}
         | 
| 278 | 
            +
            # => true
         | 
| 279 | 
            +
             | 
| 280 | 
            +
            # Na, no fun. Deeper!
         | 
| 281 | 
            +
            Qo.and(a: {
         | 
| 282 | 
            +
              f: 5..15,
         | 
| 283 | 
            +
              b: {
         | 
| 284 | 
            +
                c: /foo/,
         | 
| 285 | 
            +
                d: 10..30
         | 
| 286 | 
            +
              }
         | 
| 287 | 
            +
            }).call(a: {
         | 
| 288 | 
            +
              f: 10,
         | 
| 289 | 
            +
              b: {
         | 
| 290 | 
            +
                c: 'foobar',
         | 
| 291 | 
            +
                d: 20
         | 
| 292 | 
            +
              }
         | 
| 293 | 
            +
            })
         | 
| 294 | 
            +
            # => true
         | 
| 295 | 
            +
             | 
| 296 | 
            +
            # It can get chaotic with `or` though. Anything anywhere in there matches and
         | 
| 297 | 
            +
            # it'll pass.
         | 
| 298 | 
            +
            Qo.or(a: {
         | 
| 299 | 
            +
              f: false,
         | 
| 300 | 
            +
              b: {
         | 
| 301 | 
            +
                c: /nope/,
         | 
| 302 | 
            +
                d: 10..30
         | 
| 303 | 
            +
              }
         | 
| 304 | 
            +
            }).call(a: {
         | 
| 305 | 
            +
              f: 10,
         | 
| 306 | 
            +
              b: {
         | 
| 307 | 
            +
                c: 'foobar',
         | 
| 308 | 
            +
                d: 20
         | 
| 309 | 
            +
              }
         | 
| 310 | 
            +
            })
         | 
| 311 | 
            +
            ```
         | 
| 312 | 
            +
             | 
| 313 | 
            +
            ##### 3.1.3 - Wildcard provided
         | 
| 234 314 |  | 
| 235 315 | 
             
            As with other wildcards, if the value matched against is a wildcard it'll always get through:
         | 
| 236 316 |  | 
| @@ -239,7 +319,7 @@ Qo[name: :*] === {name: 'Foo'} | |
| 239 319 | 
             
            # => true
         | 
| 240 320 | 
             
            ```
         | 
| 241 321 |  | 
| 242 | 
            -
            ##### 3.1. | 
| 322 | 
            +
            ##### 3.1.4 - Case match present
         | 
| 243 323 |  | 
| 244 324 | 
             
            If a case match is present for the key, it'll try and compare:
         | 
| 245 325 |  | 
| @@ -259,12 +339,12 @@ end | |
| 259 339 |  | 
| 260 340 | 
             
            # Select
         | 
| 261 341 |  | 
| 262 | 
            -
            people_hashes = people_arrays.map { | | 
| 342 | 
            +
            people_hashes = people_arrays.map { |n, a| {name: n, age: a} }
         | 
| 263 343 | 
             
            people_hashes.select(&Qo[age: 15..25])
         | 
| 264 344 | 
             
            # => [{:name=>"Robert", :age=>22}, {:name=>"Roberta", :age=>22}, {:name=>"Bar", :age=>18}]
         | 
| 265 345 | 
             
            ```
         | 
| 266 346 |  | 
| 267 | 
            -
            ##### 3.1. | 
| 347 | 
            +
            ##### 3.1.5 - Predicate match present
         | 
| 268 348 |  | 
| 269 349 | 
             
            Much like our array friend above, if a predicate style method is present see if it'll work
         | 
| 270 350 |  | 
| @@ -282,16 +362,16 @@ else 'nope' | |
| 282 362 | 
             
            end
         | 
| 283 363 | 
             
            # => "No age provided!"
         | 
| 284 364 |  | 
| 285 | 
            -
            #  | 
| 365 | 
            +
            # Reject
         | 
| 286 366 |  | 
| 287 367 | 
             
            people_hashes = people_arrays.map { |(n,a)| {name: n, age: a} } << {name: 'Ghost', age: nil}
         | 
| 288 | 
            -
            people_hashes. | 
| 368 | 
            +
            people_hashes.reject(&Qo[age: :nil?])
         | 
| 289 369 | 
             
            # => [{:name=>"Robert", :age=>22}, {:name=>"Roberta", :age=>22}, {:name=>"Bar", :age=>18}]
         | 
| 290 370 | 
             
            ```
         | 
| 291 371 |  | 
| 292 372 | 
             
            Careful though, if the key doesn't exist that won't match. I'll have to consider this one later.
         | 
| 293 373 |  | 
| 294 | 
            -
            ##### 3.1. | 
| 374 | 
            +
            ##### 3.1.6 - String variant present
         | 
| 295 375 |  | 
| 296 376 | 
             
            Coerces the key into a string if possible, and sees if that can provide a valid case match
         | 
| 297 377 |  | 
| @@ -308,7 +388,7 @@ If it doesn't know how to deal with it, false out. | |
| 308 388 |  | 
| 309 389 | 
             
            ##### 3.2.2 - Wildcard provided
         | 
| 310 390 |  | 
| 311 | 
            -
            Same as other wildcards
         | 
| 391 | 
            +
            Same as other wildcards, but if the object doesn't respond to the method you specify it'll have false'd out before it reaches here.
         | 
| 312 392 |  | 
| 313 393 | 
             
            ##### 3.2.3 - Case match present
         | 
| 314 394 |  | 
| @@ -402,22 +482,89 @@ people_objects.map(&Qo.match_fn( | |
| 402 482 |  | 
| 403 483 | 
             
            So we just truncated everyone's name that was longer than three characters.
         | 
| 404 484 |  | 
| 485 | 
            +
            ### 6 - Helper functions
         | 
| 486 | 
            +
             | 
| 487 | 
            +
            There are a few functions added for convenience, and it should be noted that because all Qo matchers respond to `===` that they can be used as helpers as well.
         | 
| 488 | 
            +
             | 
| 489 | 
            +
            #### 6.1 - Dig
         | 
| 490 | 
            +
             | 
| 491 | 
            +
            Dig is used to get in deep at a nested hash value. It takes a dot-path and a `===` respondant matcher:
         | 
| 492 | 
            +
             | 
| 493 | 
            +
            ```ruby
         | 
| 494 | 
            +
            Qo.dig('a.b.c', Qo.or(1..5, 15..25)) === {a: {b: {c: 1}}}
         | 
| 495 | 
            +
            # => true
         | 
| 496 | 
            +
             | 
| 497 | 
            +
            Qo.dig('a.b.c', Qo.or(1..5, 15..25)) === {a: {b: {c: 20}}}
         | 
| 498 | 
            +
            # => true
         | 
| 499 | 
            +
            ```
         | 
| 500 | 
            +
             | 
| 501 | 
            +
            To be fair that means anything that can respond to `===`, including classes and other such things.
         | 
| 502 | 
            +
             | 
| 503 | 
            +
            #### 6.2 - Count By
         | 
| 504 | 
            +
             | 
| 505 | 
            +
            This ends up coming up a lot, especially around querying, so let's get a way to count by!
         | 
| 506 | 
            +
             | 
| 507 | 
            +
            ```ruby
         | 
| 508 | 
            +
            Qo.count_by([1,2,3,2,2,2,1]
         | 
| 509 | 
            +
             | 
| 510 | 
            +
            # => {
         | 
| 511 | 
            +
            #   1 => 2,
         | 
| 512 | 
            +
            #   2 => 4,
         | 
| 513 | 
            +
            #   3 => 1
         | 
| 514 | 
            +
            # }
         | 
| 515 | 
            +
             | 
| 516 | 
            +
            Qo.count_by([1,2,3,2,2,2,1], &:even?)
         | 
| 517 | 
            +
             | 
| 518 | 
            +
            # => {
         | 
| 519 | 
            +
            #   false => 3,
         | 
| 520 | 
            +
            #   true  => 4
         | 
| 521 | 
            +
            # }
         | 
| 522 | 
            +
            ```
         | 
| 523 | 
            +
             | 
| 405 524 | 
             
            ### 5 - Hacky Fun Time
         | 
| 406 525 |  | 
| 407 526 | 
             
            These examples will grow over the next few weeks as I think of more fun things to do with Qo. PRs welcome if you find fun uses!
         | 
| 408 527 |  | 
| 409 528 | 
             
            #### 5.1 - JSON
         | 
| 410 529 |  | 
| 530 | 
            +
            > Note that Qo does not support deep querying of hashes (yet)
         | 
| 531 | 
            +
             | 
| 532 | 
            +
            ##### 5.1.1 - JSON Placeholder
         | 
| 533 | 
            +
             | 
| 411 534 | 
             
            Qo tries to be clever though, it assumes Symbol keys first and then String keys, so how about some JSON?:
         | 
| 412 535 |  | 
| 413 536 | 
             
            ```ruby
         | 
| 414 537 | 
             
            require 'json'
         | 
| 415 538 | 
             
            require 'net/http'
         | 
| 539 | 
            +
             | 
| 416 540 | 
             
            posts = JSON.parse(
         | 
| 417 | 
            -
              Net::HTTP.get(URI("https://jsonplaceholder.typicode.com/posts")),symbolize_names: true
         | 
| 541 | 
            +
              Net::HTTP.get(URI("https://jsonplaceholder.typicode.com/posts")), symbolize_names: true
         | 
| 542 | 
            +
            )
         | 
| 543 | 
            +
             | 
| 544 | 
            +
            users = JSON.parse(
         | 
| 545 | 
            +
              Net::HTTP.get(URI("https://jsonplaceholder.typicode.com/users")), symbolize_names: true
         | 
| 418 546 | 
             
            )
         | 
| 419 547 |  | 
| 548 | 
            +
            # Get all posts where the userId is 1.
         | 
| 420 549 | 
             
            posts.select(&Qo[userId: 1])
         | 
| 550 | 
            +
             | 
| 551 | 
            +
            # Get users named Nicholas or have two names and an address somewhere with a zipcode
         | 
| 552 | 
            +
            # that starts with 9 or 4.
         | 
| 553 | 
            +
            #
         | 
| 554 | 
            +
            # Qo matchers return a `===` respondant object, remember, so we can totally nest them.
         | 
| 555 | 
            +
            users.select(&Qo.and(
         | 
| 556 | 
            +
              name: Qo.or(/^Nicholas/, /^\w+ \w+$/),
         | 
| 557 | 
            +
              address: {
         | 
| 558 | 
            +
                zipcode: Qo.or(/^9/, /^4/)
         | 
| 559 | 
            +
              }
         | 
| 560 | 
            +
            ))
         | 
| 561 | 
            +
             | 
| 562 | 
            +
            # We could even use dig to get at some of the same information. This and the above will
         | 
| 563 | 
            +
            # return the same results even.
         | 
| 564 | 
            +
            users.select(&Qo.and(
         | 
| 565 | 
            +
              Qo.dig('address.zipcode', Qo.or(/^9/, /^4/)),
         | 
| 566 | 
            +
              name: Qo.or(/^Nicholas/, /^\w+ \w+$/)
         | 
| 567 | 
            +
            ))
         | 
| 421 568 | 
             
            ```
         | 
| 422 569 |  | 
| 423 570 | 
             
            Nifty!
         | 
| @@ -436,6 +583,21 @@ hosts.select(&Qo[IPAddr.new('192.168.1.1/8')]) | |
| 436 583 | 
             
            => [["192.168.1.1", "(Router)"], ["192.168.1.2", "(My Computer)"]]
         | 
| 437 584 | 
             
            ```
         | 
| 438 585 |  | 
| 586 | 
            +
            ##### 5.2.2 - `du`
         | 
| 587 | 
            +
             | 
| 588 | 
            +
            The nice thing about Unix style commands is that they use headers, which means CSV can get a hold of them for some good formatting. It's also smart enough to deal with space seperators that may vary in length:
         | 
| 589 | 
            +
             | 
| 590 | 
            +
            ```ruby
         | 
| 591 | 
            +
            rows = CSV.new(`df -h`, col_sep: " ", headers: true).read.map(&:to_h)
         | 
| 592 | 
            +
             | 
| 593 | 
            +
            rows.map(&Qo.match_fn(
         | 
| 594 | 
            +
              Qo.m(Avail: /Gi$/) { |row|
         | 
| 595 | 
            +
                "#{row['Filesystem']} mounted on #{row['Mounted']} [#{row['Avail']} / #{row['Size']}]"
         | 
| 596 | 
            +
              }
         | 
| 597 | 
            +
            )).compact
         | 
| 598 | 
            +
            # => ["/dev/***** mounted on / [186Gi / 466Gi]"]
         | 
| 599 | 
            +
            ```
         | 
| 600 | 
            +
             | 
| 439 601 | 
             
            ## Installation
         | 
| 440 602 |  | 
| 441 603 | 
             
            Add this line to your application's Gemfile:
         | 
    
        data/docs/.gitkeep
    ADDED
    
    | 
            File without changes
         | 
    
        data/docs/_config.yml
    ADDED
    
    | @@ -0,0 +1 @@ | |
| 1 | 
            +
            theme: jekyll-theme-cayman
         | 
    
        data/lib/qo.rb
    CHANGED
    
    | @@ -6,31 +6,61 @@ module Qo | |
| 6 6 | 
             
              WILDCARD_MATCH = :*
         | 
| 7 7 |  | 
| 8 8 | 
             
              class << self
         | 
| 9 | 
            -
             | 
| 9 | 
            +
             | 
| 10 | 
            +
                # Creates a Guard Block matcher.
         | 
| 11 | 
            +
                #
         | 
| 12 | 
            +
                # A guard block matcher is used to guard a function from running unless
         | 
| 13 | 
            +
                # the left-hand matcher passes. Once called with a value, it will either
         | 
| 14 | 
            +
                # return `[false, false]` or `[true, Any]`.
         | 
| 15 | 
            +
                #
         | 
| 16 | 
            +
                # This wrapping is done to preserve intended false or nil responses,
         | 
| 17 | 
            +
                # and is unwrapped with match below.
         | 
| 18 | 
            +
                #
         | 
| 19 | 
            +
                # @param *array_matchers    [Array] varargs matchers
         | 
| 20 | 
            +
                # @param **keyword_matchers [Hash]  kwargs matchers
         | 
| 21 | 
            +
                # @param &fn                [Proc]  Guarded function
         | 
| 22 | 
            +
                #
         | 
| 23 | 
            +
                # @return [Proc[Any]]
         | 
| 24 | 
            +
                #     Any -> Proc[Any]
         | 
| 25 | 
            +
                def matcher(*array_matchers, **keyword_matchers, &fn)
         | 
| 10 26 | 
             
                  Qo::GuardBlockMatcher.new(*array_matchers, **keyword_matchers, &fn)
         | 
| 11 27 | 
             
                end
         | 
| 12 28 |  | 
| 13 | 
            -
                 | 
| 14 | 
            -
             | 
| 29 | 
            +
                alias_method :m, :matcher
         | 
| 30 | 
            +
             | 
| 31 | 
            +
             | 
| 32 | 
            +
                # Takes a set of Guard Block matchers, runs each in sequence, then
         | 
| 33 | 
            +
                # unfolds the response from the first passing block.
         | 
| 34 | 
            +
                #
         | 
| 35 | 
            +
                # @param target       [Any]                      Target object to run against
         | 
| 36 | 
            +
                # @param *qo_matchers [Array[GuardBlockMatcher]] Collection of matchers to run
         | 
| 37 | 
            +
                #
         | 
| 38 | 
            +
                # @return [type] [description]
         | 
| 39 | 
            +
                def match(target, *qo_matchers)
         | 
| 40 | 
            +
                  all_are_guards = qo_matchers.all? { |q| q.is_a?(Qo::GuardBlockMatcher) }
         | 
| 15 41 | 
             
                  raise 'Must patch Qo GuardBlockMatchers!' unless all_are_guards
         | 
| 16 42 |  | 
| 17 43 | 
             
                  qo_matchers.reduce(nil) { |_, matcher|
         | 
| 18 | 
            -
                    did_match, match_result = matcher.call( | 
| 44 | 
            +
                    did_match, match_result = matcher.call(target)
         | 
| 19 45 | 
             
                    break match_result if did_match
         | 
| 20 46 | 
             
                  }
         | 
| 21 47 | 
             
                end
         | 
| 22 48 |  | 
| 49 | 
            +
                # Wraps match to allow it to be used in a points-free style like regular matchers.
         | 
| 50 | 
            +
                #
         | 
| 51 | 
            +
                # @param *qo_matchers [Array[GuardBlockMatcher]] Collection of matchers to run
         | 
| 52 | 
            +
                #
         | 
| 53 | 
            +
                # @return [Proc[Any]]
         | 
| 54 | 
            +
                #     Any -> Any
         | 
| 23 55 | 
             
                def match_fn(*qo_matchers)
         | 
| 24 | 
            -
                  ->  | 
| 56 | 
            +
                  -> target { match(target, *qo_matchers) }
         | 
| 25 57 | 
             
                end
         | 
| 26 58 |  | 
| 27 59 | 
             
                def and(*array_matchers, **keyword_matchers)
         | 
| 28 60 | 
             
                  Qo::Matcher.new('and', *array_matchers, **keyword_matchers)
         | 
| 29 61 | 
             
                end
         | 
| 30 62 |  | 
| 31 | 
            -
                 | 
| 32 | 
            -
                  Qo::Matcher.new('and', *array_matchers, **keyword_matchers)
         | 
| 33 | 
            -
                end
         | 
| 63 | 
            +
                alias_method :[], :and
         | 
| 34 64 |  | 
| 35 65 | 
             
                def or(*array_matchers, **keyword_matchers)
         | 
| 36 66 | 
             
                  Qo::Matcher.new('or', *array_matchers, **keyword_matchers)
         | 
| @@ -39,5 +69,24 @@ module Qo | |
| 39 69 | 
             
                def not(*array_matchers, **keyword_matchers)
         | 
| 40 70 | 
             
                  Qo::Matcher.new('not', *array_matchers, **keyword_matchers)
         | 
| 41 71 | 
             
                end
         | 
| 72 | 
            +
             | 
| 73 | 
            +
                # Utility functions. Consider placing these elsewhere.
         | 
| 74 | 
            +
             | 
| 75 | 
            +
                def dig(path_map, expected_value)
         | 
| 76 | 
            +
                  -> hash {
         | 
| 77 | 
            +
                    segments = path_map.split('.')
         | 
| 78 | 
            +
             | 
| 79 | 
            +
                    expected_value === hash.dig(*segments) ||
         | 
| 80 | 
            +
                    expected_value === hash.dig(*segments.map(&:to_sym))
         | 
| 81 | 
            +
                  }
         | 
| 82 | 
            +
                end
         | 
| 83 | 
            +
             | 
| 84 | 
            +
                def count_by(targets, &fn)
         | 
| 85 | 
            +
                  fn ||= -> v { v }
         | 
| 86 | 
            +
             | 
| 87 | 
            +
                  targets.each_with_object(Hash.new(0)) { |target, counts|
         | 
| 88 | 
            +
                    counts[fn[target]] += 1
         | 
| 89 | 
            +
                  }
         | 
| 90 | 
            +
                end
         | 
| 42 91 | 
             
              end
         | 
| 43 92 | 
             
            end
         | 
    
        data/lib/qo/matcher.rb
    CHANGED
    
    | @@ -116,7 +116,13 @@ module Qo | |
| 116 116 | 
             
                #     Any -> Any -> Bool # Matches against wildcard or a key and value. Coerces key to_s if no matches for JSON.
         | 
| 117 117 | 
             
                private def hash_against_hash_matcher(match_target)
         | 
| 118 118 | 
             
                  -> match_key, match_value {
         | 
| 119 | 
            -
                    match_target.key?(match_key) | 
| 119 | 
            +
                    return false unless match_target.key?(match_key)
         | 
| 120 | 
            +
             | 
| 121 | 
            +
                    # If both the match value and target are hashes, descend if the key exists
         | 
| 122 | 
            +
                    if match_value.is_a?(Hash) && match_target.is_a?(Hash)
         | 
| 123 | 
            +
                      return match_against_hash(match_value)[match_target[match_key]]
         | 
| 124 | 
            +
                    end
         | 
| 125 | 
            +
             | 
| 120 126 | 
             
                    wildcard_match(match_value) ||
         | 
| 121 127 | 
             
                    case_match(match_target[match_key], match_value)  ||
         | 
| 122 128 | 
             
                    method_matches?(match_target[match_key], match_value) || (
         | 
| @@ -137,7 +143,8 @@ module Qo | |
| 137 143 | 
             
                #     Any -> Any -> Bool # Matches against wildcard or match value versus the public send return of the target
         | 
| 138 144 | 
             
                private def hash_against_object_matcher(match_target)
         | 
| 139 145 | 
             
                  -> match_key, match_value {
         | 
| 140 | 
            -
                    match_target.respond_to?(match_key) | 
| 146 | 
            +
                    return false unless match_target.respond_to?(match_key)
         | 
| 147 | 
            +
             | 
| 141 148 | 
             
                    wildcard_match(match_value) ||
         | 
| 142 149 | 
             
                    case_match(method_send(match_target, match_key), match_value) ||
         | 
| 143 150 | 
             
                    method_matches?(method_send(match_target, match_key), match_value)
         | 
| @@ -191,7 +198,7 @@ module Qo | |
| 191 198 |  | 
| 192 199 | 
             
                # Wraps a case equality statement to make it a bit easier to read. The
         | 
| 193 200 | 
             
                # typical left bias of `===` can be confusing reading down a page, so
         | 
| 194 | 
            -
                # more of a clarity thing than anything.
         | 
| 201 | 
            +
                # more of a clarity thing than anything. Also makes for nicer stack traces.
         | 
| 195 202 | 
             
                #
         | 
| 196 203 | 
             
                # @param target [Any] Target to match against
         | 
| 197 204 | 
             
                # @param value [respond_to?(:===)]
         | 
    
        data/lib/qo/version.rb
    CHANGED
    
    
    
        metadata
    CHANGED
    
    | @@ -1,14 +1,14 @@ | |
| 1 1 | 
             
            --- !ruby/object:Gem::Specification
         | 
| 2 2 | 
             
            name: qo
         | 
| 3 3 | 
             
            version: !ruby/object:Gem::Version
         | 
| 4 | 
            -
              version: 0.1. | 
| 4 | 
            +
              version: 0.1.7
         | 
| 5 5 | 
             
            platform: ruby
         | 
| 6 6 | 
             
            authors:
         | 
| 7 7 | 
             
            - Brandon Weaver
         | 
| 8 8 | 
             
            autorequire: 
         | 
| 9 9 | 
             
            bindir: exe
         | 
| 10 10 | 
             
            cert_chain: []
         | 
| 11 | 
            -
            date: 2018-04- | 
| 11 | 
            +
            date: 2018-04-13 00:00:00.000000000 Z
         | 
| 12 12 | 
             
            dependencies:
         | 
| 13 13 | 
             
            - !ruby/object:Gem::Dependency
         | 
| 14 14 | 
             
              name: bundler
         | 
| @@ -99,6 +99,8 @@ files: | |
| 99 99 | 
             
            - Rakefile
         | 
| 100 100 | 
             
            - bin/console
         | 
| 101 101 | 
             
            - bin/setup
         | 
| 102 | 
            +
            - docs/.gitkeep
         | 
| 103 | 
            +
            - docs/_config.yml
         | 
| 102 104 | 
             
            - lib/qo.rb
         | 
| 103 105 | 
             
            - lib/qo/guard_block_matcher.rb
         | 
| 104 106 | 
             
            - lib/qo/matcher.rb
         |