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

Provide friendlier access to request variants #18939

Merged
merged 2 commits into from Mar 27, 2015
Merged

Provide friendlier access to request variants #18939

merged 2 commits into from Mar 27, 2015

Conversation

georgeclaghorn
Copy link
Contributor

@georgeclaghorn georgeclaghorn commented Feb 14, 2015

Currently, request.variant is a dumb reader for the underlying array used to store the request's variants. If you set the variant like so:

request.variant = :phone

...then request.variant will return [:phone]. That can be cumbersome to deal with, since you have to check request.variant.first == :phone, request.variant == [:phone], request.variant.to_a.include?(:phone), etc.

This PR implements a friendlier interface:

request.variant = :phone
request.variant.phone?   # true
request.variant.tablet?  # false

request.variant = [:phone, :tablet]
request.variant.phone?    # true
request.variant.tablet?   # true
request.variant.desktop?  # false
request.variant.any?(:phone, :desktop)  # true
request.variant.any?(:desktop, :watch)  # false

Fixes #18933.

(variants & candidates).any?
end

def to_ary
Copy link
Contributor

@kaspth kaspth Feb 14, 2015

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is it this gets us?

Copy link
Contributor Author

@georgeclaghorn georgeclaghorn Feb 14, 2015

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This allows request.variant to be compared to other arrays, since Array comparison methods like #==, #&, and #| call to_ary on the other array:

request.variant = :phone
# => #<VariantInquirer @variants=[:phone]>

[:phone] == request.variant
# => true

Existing framework code relies on this behavior in a few places.

@kaspth kaspth added this to the 5.0.0 milestone Feb 15, 2015
@egilburg
Copy link
Contributor

egilburg commented Feb 16, 2015

@dhh would an ArrayInquirer also imply an ArrayWithIndifferentAccess? or do a runtime cast to_s or to_str?

e.g. in this example:

class SoftString
  def to_s
    'soft'
  end
end

class HardString
  def to_str
    'hard'
  end
end

ai = ActiveSupport::ArrayInquirer.new(['foo', :bar, SoftString.new, HardString.new])

What should each of these return? true, false, or cast exception?

ai.foo?
ai.bar?
ai.soft?
ai.hard?

And BTW this is not current behavior of StringInquirer:

ActiveSupport::StringInquirer.new(:foo)
# => TypeError: can't convert Symbol into String

It only works for Strings and objects which implement to_str, which Symbol doesn't. And this particular PR works on Symbols, so a similar implementation of ArrayInquirer won't work here - presumably, it would raise an exception during ArrayInquirer initialization, before even having a chance to call the method missing.

(perhaps for consistency, StringInquirer) should special-case handling symbols without raising, by casting them to_s? It could also handle any other object by to_s match but the latter could presumably be more disruptive for existing users.

@dhh
Copy link
Member

dhh commented Feb 16, 2015

I'm thinking it's a runtime comparison. So it works with both Indifferent and not. I also think that we should have a SymbolInquery. I've wanted that in the past.

@egilburg
Copy link
Contributor

egilburg commented Feb 16, 2015

@dhh do you prefer two separate inquirer classes, one just for strings and one just for symbols, rather than soften StringInquirer to accept symbols (and just cast them to_s on initialize)?

@dhh
Copy link
Member

dhh commented Feb 16, 2015

I was thinking about StringInquirer taking a symbol, but then it’s not a symbol any more, it’s a string. I’d rather just be explicit and have a SymbolInquirer that inherits from Symbol.

On Feb 16, 2015, at 11:18, Eugene Gilburg notifications@github.com wrote:

@dhh do you prefer two separate inquirer classes, one just for strings and one just for symbols, rather than soften StringInquirer to accept symbols (and just cast them to_s on initialize)?


Reply to this email directly or view it on GitHub.

@dhh
Copy link
Member

dhh commented Feb 16, 2015

Anyway, let's split those off into their own tickets. Don't want to swamp this ticket with those concerns. I'm very eager to get this in sooner rather than later.

@georgeclaghorn
Copy link
Contributor Author

georgeclaghorn commented Feb 22, 2015

Update: I've extracted an ArrayInquirer, but need to write tests. My schedule is a little crazy for the time being, but I will get this done.

@dhh
Copy link
Member

dhh commented Feb 22, 2015

Awesome, George. Not a mega rush. Thanks for helping with this!

On Feb 21, 2015, at 9:11 PM, George Claghorn notifications@github.com wrote:

Update: I've extracted an ArrayInquirer, but need to write tests. My schedule is a little crazy for the time being, but I will get this done.


Reply to this email directly or view it on GitHub #18939 (comment).

@@ -288,7 +288,7 @@ def method_missing(name, *args, &block)
end

def variant
if @variant.nil?
if @variant.empty?
@variants[:none] || @variants[:any]
elsif (@variants.keys & @variant).any?
@variant.each do |v|
Copy link
Contributor

@kaspth kaspth Feb 23, 2015

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we're already touching this code, maybe we can improve it. How about this? 😄

def variant
  if @variant.empty?
    @variants[:none] || @variants[:any]
  else
    @variants[ @variant.find { |v| @variants.key?(v) } || :any ]
  end
end

Copy link
Contributor Author

@georgeclaghorn georgeclaghorn Mar 12, 2015

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Took it a step further:

def variant
  if @variant.empty?
    @variants[:none] || @variants[:any]
  else
    @variants[variant_key]
  end
end

private
  def variant_key
    @variant.find { |variant| @variants.key?(variant) } || :any
  end

Copy link
Contributor

@kaspth kaspth Mar 12, 2015

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great, 👍

@sdhull
Copy link
Contributor

sdhull commented Mar 11, 2015

Yes please merge this 👍

My initial inclination when working with request.variant was to inquire like that request.variant.iphone?

Failing that, I was surprised/dismayed by the fact request.variant.include? :iphone cannot be done unless you explicitly set the variant every time. It seems to me that if you don't set any variants, then request.variant.should == []. Actually, I'm not sure this PR actually accomplishes this, however being able to do request.variant.iphone? would obviate my need for request.variant defaulting to []. (Still strikes me that this would be proper for Principle Of Least Surprise.)

Moreover, since variant is not a single value (but rather a list of values), shouldn't it be request.variants?

@georgeclaghorn
Copy link
Contributor Author

georgeclaghorn commented Mar 11, 2015

@sdhull: Agree about pluralizing variant, or at least providing a plural alias. Would like to investigate that in a separate PR.

@sdhull
Copy link
Contributor

sdhull commented Mar 11, 2015

If left unset, will request.variant == [] be true?

Edit: nm, I see it will be default to ActiveSupport::ArrayInquirer.new

@georgeclaghorn
Copy link
Contributor Author

georgeclaghorn commented Mar 12, 2015

Updated to use an extracted ArrayInquirer.

@dhh @kaspth


def method_missing(name, *args)
if name[-1] == '?'
any?(name[0..-2]) || any?(name[0..-2].to_sym)
Copy link
Contributor

@egilburg egilburg Mar 12, 2015

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for consistency between string/symbol, this check should be at the any? level:

def any?(*candidates, &block)
  if candidates.none?
    super
  else
    candidates.detect do |candidate|
      include?(candidate) || include?(candidate.to_sym)
    end || false
  end
end 

private

def method_missing(name, *args)
  if name[-1] == '?'
    any?(name[0..2])
  end
end

Copy link
Contributor

@kaspth kaspth Mar 12, 2015

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your suggested code changes are a little off. We don't need to check for none? or explicitly return false. But I agree with moving the check 👍

Copy link
Contributor Author

@georgeclaghorn georgeclaghorn Mar 14, 2015

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed.

@kaspth
Copy link
Contributor

kaspth commented Mar 14, 2015

Looks great to me, 👍

assert_not @array_inquirer.any? { |v| v == :desktop }
end

def test_respond_to
Copy link
Contributor

@egilburg egilburg Mar 14, 2015

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

assert the shortcut method since you added it:

def test_inquiry_method
  assert_equal @array_inquirer, [:mobile, :tablet].inquiry
end

Wrapping an array in an `ArrayInquirer` gives a friendlier way to check its
string-like contents. For example, `request.variant` returns an `ArrayInquirer`
object. To check a request's variants, you can call:

    request.variant.phone?
    request.variant.any?(:phone, :tablet)

...instead of:

    request.variant.include?(:phone)
    request.variant.any? { |v| v.in?([:phone, :tablet]) }

`Array#inquiry` is a shortcut for wrapping the receiving array in an
`ArrayInquirer`:

    pets = [:cat, :dog]
    pets.cat?    # => true
    pets.ferret? # => false
    pets.any?(:cat, :ferret} # => true
@rafaelfranca rafaelfranca merged commit c64b99e into rails:master Mar 27, 2015
rafaelfranca added a commit that referenced this issue Mar 27, 2015
Provide friendlier access to request variants
@rafaelfranca
Copy link
Member

rafaelfranca commented Mar 27, 2015

Thank you so much. The code is great, but I'd not include the Array#inquiry we are promoting too much a feature that I don't think it will be used in most part of the applications. Lets keep just the ArrayInquirer constructor.

@dhh
Copy link
Member

dhh commented Mar 28, 2015

Great work, George! 🤘

@dhh
Copy link
Member

dhh commented Mar 28, 2015

I think the ArrayInquirer makes as much sense as the StringInquirer. I'm certainly going to use it. I think it's fair to highlight that it's there.

@rafaelfranca
Copy link
Member

rafaelfranca commented Mar 30, 2015

I mean, it will still be highlighted in the CHANGELOG and documentation but I don't think it needs a Array#inquiry method. Even that we have a lot of usage of this feature it will be usually well encapsulated, so we can call ActiveSupport::ArrayInquirer.new without losing the legibility.

@georgeclaghorn georgeclaghorn deleted the variant-inquiry branch Mar 30, 2015
@dhh
Copy link
Member

dhh commented Mar 30, 2015

I like the #inquiry form, as it makes it possible to use this inline on a passed parameter without extra setup. And it then maps directly to String#inquiry as well.

On Mar 30, 2015, at 3:56 AM, Rafael Mendonça França notifications@github.com wrote:

I mean, it will still be highlighted in the CHANGELOG and documentation but I don't think it needs a Array#inquiry method. Even that we have a lot of usage of this feature it will be usually well encapsulated, so we can call ActiveSupport::ArrayInquirer.new without losing the legibility.


Reply to this email directly or view it on GitHub.

@rafaelfranca
Copy link
Member

rafaelfranca commented Mar 30, 2015

👍, so I'll add back

@rafaelfranca
Copy link
Member

rafaelfranca commented Mar 30, 2015

Add back at b5c3502

@rafaelfranca rafaelfranca modified the milestones: 5.0.0 [temp], 5.0.0 Dec 30, 2015
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

10 participants