Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Checking a Hash for the presence of a key #21

Open
bfoz opened this issue Sep 4, 2018 · 9 comments
Open

Checking a Hash for the presence of a key #21

bfoz opened this issue Sep 4, 2018 · 9 comments

Comments

@bfoz
Copy link

bfoz commented Sep 4, 2018

It's currently possible to check that a Hash contains a given key and that the key's value matches a desired value. However, checking for the presence of a key without checking the key's value (ie. a matcher that's equivalent to Hash#key?) isn't possible.

My first thought was to try Qo[key? => :foo], but that fails with an ArgumentError because key? needs an argument.

After reading the source I thought that Qo[ [:key?, :foo] => true ] would work, but it didn't, presumably because the matcher argument to public_send isn't being splatted. Unfortunately, even if the calls to public_send were "fixed", this would be a tough solution to come up with without reading the code very carefully, and of course it would be an implementation-dependent hack.

I humbly suggest extending the API to allow for something like Qo.key?[:foo] for handling this use-case. Another option would be to have HashMatcher special-case the key? key.

@baweaver
Copy link
Owner

baweaver commented Sep 5, 2018

It's possible :)

Qo[foo: Any]

See the Any type in 0.5.0+: https://github.com/baweaver/qo#quick-start

https://github.com/baweaver/any

Before this you would have used a "wildcard":

Qo[foo: :*]

Does that satisfy the need?

@baweaver
Copy link
Owner

baweaver commented Sep 5, 2018

Also noted as of 0.5.0 it requires a "key" (method for objects) to be present on the other side, so using Any assures that that key exists and has a value.

Any itself is rather simple, it returns true for everything it's ==='d against:

require "any/version"

class Any
  class << self
    def ==(other) true end

    def ===(other) true end

    def to_proc; proc { true } end
  end
end

The only difference between this and the actual code is I stripped out the comments and spacing: https://github.com/baweaver/any/blob/master/lib/any.rb

@bfoz
Copy link
Author

bfoz commented Sep 5, 2018

That's not great, but I guess it works well enough. Thanks.

Does the same trick work for Hash#value?? ie. Qo[Any => 42]

FWIW, for the rabbit hole that led me to creating this issue I'm trying to check that a Hash has keys and values that match certain constraints without checking any particular key/value pair. For example, check that all keys are Symbols and that all values are Integers. Qo[Symbol => Integer] doesn't seem to work.

@baweaver
Copy link
Owner

baweaver commented Sep 5, 2018 via email

@baweaver
Copy link
Owner

baweaver commented Sep 5, 2018

Example:

contract = Qo.contract.all(Symbol => Integer)
contract.call(a: 1, b: 2) # => true
contract.call('a' => 1, b: 2) # => false

There would have to be an additional stipulation of all, so as to not violate the current API contract. I'd need to think this one through.

@baweaver
Copy link
Owner

baweaver commented Sep 5, 2018

(though now I almost wonder if this is another gem entirely as Qo itself doesn't behave like this)

@baweaver
Copy link
Owner

baweaver commented Sep 5, 2018

Musing on it a bit, it seems like the only way to get it to work is to make a distinction between 1-item and true multi-arity lists.

The problem is that'd slow the entire thing down a few clicks and introduce a bit more implied magic into the code.

@bfoz
Copy link
Author

bfoz commented Sep 6, 2018

I managed to get it to work relatively easily, with the caveat that I created a stripped down Matcher class to experiment on. I'm not sure how this would translate back to the real code, but maybe it'll help anyway.

For starters I created an all? method, and a test case:

# Compare the given pattern to all of the elements of a container and return true if they all match
def self.all?(*args)
    _m = self.new(*args)
    -> other { other.all?(_m) }
end

# RSpec
...
expect(Matcher.all?(Symbol => Integer)).to be === {foo:42, bar:24}
...

The arguments to all? are recognized as a hash matcher in the normal fashion. The trick is that other.all? magically calls Matcher#=== passing each element of other to it in turn, which in this case is key-value pairs from the Hash being matched. So, === then needs a special-case to check for being passed a length-2 array.

def ===(other)
    ...
    if other.is_a?(Array) and (2 == other.length)
        (k === other.first) and (v === other.last)
    end
    ...
end

And that's enough to pass the test. Admittedly it could use more testing, and the special-case logic certainly isn't ideal. In fact, it's probably terrible. So far it's working well enough for what I need. ¯\(ツ)

@baweaver baweaver mentioned this issue Feb 11, 2019
@baweaver
Copy link
Owner

Looking back on this issue, I believe it would add too much magic to the API. It would likely be better suited as an extension implementing some of the same interfaces.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants