Skip to content

Commit

Permalink
reorg and rework of matchers. Now we've extracted the creation of mat…
Browse files Browse the repository at this point in the history
…chers to another module for more segmentation and easier creation of customer matchers
  • Loading branch information
Mark Percival committed Feb 21, 2010
1 parent 12fddf0 commit 2056e31
Show file tree
Hide file tree
Showing 8 changed files with 98 additions and 64 deletions.
16 changes: 10 additions & 6 deletions README.markdown
Expand Up @@ -10,11 +10,13 @@ A tool to help format your sites mobile pages.

_initializers/divining\_rod.rb_

DiviningRod::Matchers.define do |map|
# map.ua /user_agent_regex/, :format => :blackberry, :tags => [:your_tag]
DiviningRod::Mapping.define do |map|
# map.ua /user_agent_regex/, :format => :wml, :tags => [:your_tag]
map.ua /Apple.*Mobile.*Safari/, :format => :webkit, :tags => [:apple, :youtube_capable] do |iphone|
iphone.ua /iPhone/, :tags => :iphone
iphone.ua /iPad/, :tags => :ipad
iphone.ua /iPad/, :tags => :ipad do |ipad|
ipad.ua /Unicorns/, :tags => [:omg_unicorns, :magic], :format => :happiness
end
iphone.ua /iPod/, :tags => :ipod
map.ua /Android/, :format => :webkit, :tags => [:android, :youtube_capable, :google_gears]
map.subdomain /wap/, :format => :wap, :tags => [:crappy_old_phone]
Expand Down Expand Up @@ -56,10 +58,12 @@ _app/views/mobile/show.webkit.html_

## Note on the development

This is still very much in beta, but we are using it in production. As such we plan
to do our best to keep the API the same.
Tags always merge, while all other hash keys get overridden. Tags also will always allow you to call them as
booleans. Ex @profile.iphone?

The user agent definitions will be updated here later this week.
If the :format key isn't available we default to request.format.

Any of keys can be used and queried arbitrarily.

## Todo

Expand Down
3 changes: 2 additions & 1 deletion lib/divining_rod.rb
Expand Up @@ -2,4 +2,5 @@ module DiviningRod; end

require 'divining_rod/profile'
require 'divining_rod/definition'
require 'divining_rod/definitions'
require 'divining_rod/mapping'
require 'divining_rod/matchers'
4 changes: 1 addition & 3 deletions lib/divining_rod/definition.rb
Expand Up @@ -16,9 +16,7 @@ def evaluate(request)
result = self
unless self.children.empty?
self.children.each do |child|
if child.evaluate(request)
child_result = child
end
child_result = child.evaluate(request)
break if child_result
end
end
Expand Down
56 changes: 34 additions & 22 deletions lib/divining_rod/definitions.rb → lib/divining_rod/mapping.rb
@@ -1,11 +1,11 @@
module DiviningRod
class Definitions
class Mapping
# Matchers create and keep track of the definitions.

class << self

attr_accessor :definitions

def define
yield(self)
end
Expand All @@ -17,34 +17,26 @@ def evaluate(request)
end
match
end

def ua(pattern, opts={}, &blk)
if @parent #merge the settings if they have a parent definition
tags = Array(opts[:tags]) | @parent.tags
opts = @parent.opts.merge(opts)
opts[:tags] = tags # tags are merged, not overridden
end
definition = Definition.new(opts) { |request|
if pattern.match(request.user_agent)
true
end
}

# The only really complicated part of this whole endeavor
# Matchers defines a definition method which is passed a regex pattern
# and options, and returns the defintion object.
# In this method we wrap the process to merge parent params and
# add the defintion as a child of another defintion.
#
# Ex. map.pattern :ua, /iPhone/, :tags => [:apple, :webkit], :format => :webkit
def pattern(type, pattern, opts = {})
opts = self.merge_parent(opts)
definition = Matchers.send(type.to_sym, pattern, opts)
add_definition(definition, @parent)
# And here's the recursive to let up define children of a definition
if block_given?
@parent = definition
yield(self)
end
@parent = nil #reset the scope
end

def subdomain(pattern, opts={})
add_definition Definition.new(opts) { |request|
if pattern.match(request.subdomains[0])
true
end
}
end

def default(opts = {})
add_definition Definition.new(opts) { |request| true }
end
Expand All @@ -61,6 +53,26 @@ def add_definition(definition, parent = nil)
def clear_definitions
@definitions = []
end

def method_missing(meth, *args, &blk)
# Lets us use map.ua instead of map.pattern :ua
if Matchers.respond_to?(meth.to_sym)
self.pattern(meth, args[0], args[1,], &blk)
else
super
end
end

protected

def merge_parent(opts)
if @parent #merge the settings if they have a parent definition
tags = Array(opts[:tags]) | @parent.tags
opts = @parent.opts.merge(opts)
opts[:tags] = tags # tags are merged, not overridden
end
opts
end

end

Expand Down
23 changes: 23 additions & 0 deletions lib/divining_rod/matchers.rb
@@ -0,0 +1,23 @@
module DiviningRod
class Matchers
class << self

def ua(pattern, opts = {})
Definition.new(opts) { |request|
if pattern.match(request.user_agent)
true
end
}
end

def subdomain(pattern, opts={})
Definition.new(opts) { |request|
if pattern.match(request.subdomains[0])
true
end
}
end

end
end
end
6 changes: 1 addition & 5 deletions lib/divining_rod/profile.rb
Expand Up @@ -5,7 +5,7 @@ class Profile

def initialize(request)
@request = request.clone #Lets not mess with the real one
@match = DiviningRod::Definitions.evaluate(request)
@match = DiviningRod::Mapping.evaluate(request)
end

def format
Expand Down Expand Up @@ -34,9 +34,5 @@ def method_missing(meth)
end
end

def definitions
DiviningRod::Matchers.definitions || []
end

end
end
42 changes: 21 additions & 21 deletions spec/basic_spec.rb
Expand Up @@ -4,8 +4,8 @@

before :each do
@request = mock("rails_request", :user_agent => 'My iPhone which is actually an iPad')
DiviningRod::Definitions.clear_definitions
DiviningRod::Definitions.define do |map|
DiviningRod::Mapping.clear_definitions
DiviningRod::Mapping.define do |map|
map.ua /iPhone/, :format => :webkit, :tags => [:iphone, :youtube, :geolocate] do |iphone|
iphone.ua /iPad/, :tags => [:ipad]
end
Expand All @@ -32,8 +32,8 @@

before :each do
@request = mock("rails_request", :user_agent => 'My Foo Fone', :format => :html)
DiviningRod::Definitions.clear_definitions
DiviningRod::Definitions.define do |map|
DiviningRod::Mapping.clear_definitions
DiviningRod::Mapping.define do |map|
map.ua /iPhone/, :format => :webkit, :tags => [:iphone, :youtube, :geolocate]
end
end
Expand All @@ -51,8 +51,8 @@

before :each do
@request = mock("rails_request", :user_agent => 'My Foo Fone')
DiviningRod::Definitions.clear_definitions
DiviningRod::Definitions.define do |map|
DiviningRod::Mapping.clear_definitions
DiviningRod::Mapping.define do |map|
map.ua /iPhone/, :format => :webkit, :tags => [:iphone, :youtube, :geolocate]
map.default :format => :html
end
Expand All @@ -70,8 +70,8 @@

before :each do
@request = mock("rails_request", :user_agent => 'Foo Fone', :format => :html)
DiviningRod::Definitions.clear_definitions
DiviningRod::Definitions.define do |map|
DiviningRod::Mapping.clear_definitions
DiviningRod::Mapping.define do |map|
map.ua /iPhone/, :format => :webkit, :tags => [:iphone, :youtube, :geolocate]
end
end
Expand All @@ -86,8 +86,8 @@

before :each do
@request = mock("rails_request", :user_agent => 'Foo Fone', :subdomains => ['wap'])
DiviningRod::Definitions.clear_definitions
DiviningRod::Definitions.define do |map|
DiviningRod::Mapping.clear_definitions
DiviningRod::Mapping.define do |map|
map.subdomain /wap/, :format => :wap, :tags => [:shitty]
end
end
Expand All @@ -104,8 +104,8 @@

before :each do
@request = mock("rails_request", :user_agent => nil, :subdomains => [])
DiviningRod::Definitions.clear_definitions
DiviningRod::Definitions.define do |map|
DiviningRod::Mapping.clear_definitions
DiviningRod::Mapping.define do |map|
map.ua /iPhone/, :format => :wap, :tags => [:shitty]
end
end
Expand All @@ -119,34 +119,34 @@
end


describe DiviningRod::Definitions do
describe DiviningRod::Mapping do

before :each do
@request = mock("rails_request", :user_agent => 'iPhone Foo')
DiviningRod::Definitions.clear_definitions
DiviningRod::Definitions.define do |map|
DiviningRod::Mapping.clear_definitions
DiviningRod::Mapping.define do |map|
map.ua /iPhone/, :format => :iphone, :tags => [:iphone, :youtube]
end
end

it "should recognize an iPhone" do
DiviningRod::Definitions.definitions.first.evaluate(@request).should be_true
DiviningRod::Definitions.definitions.first.format.should eql(:iphone)
DiviningRod::Mapping.definitions.first.evaluate(@request).should be_true
DiviningRod::Mapping.definitions.first.format.should eql(:iphone)
end

describe "defining a default definition" do

before :each do
@request = mock("rails_request", :user_agent => 'Foo Fone')
DiviningRod::Definitions.clear_definitions
DiviningRod::Definitions.define do |map|
DiviningRod::Mapping.clear_definitions
DiviningRod::Mapping.define do |map|
map.default :format => :unknown, :tags => [:html]
end
end

it "should use the default route if no other match is found" do
DiviningRod::Definitions.definitions.first.evaluate(@request).should be_true
DiviningRod::Definitions.definitions.first.format.should eql(:unknown)
DiviningRod::Mapping.definitions.first.evaluate(@request).should be_true
DiviningRod::Mapping.definitions.first.format.should eql(:unknown)
end

end
Expand Down
12 changes: 6 additions & 6 deletions spec/definitions_spec.rb → spec/mapping_spec.rb
@@ -1,11 +1,11 @@
require 'spec_helper'

describe DiviningRod::Definitions do
describe DiviningRod::Mapping do

before :each do
@request = mock("rails_request", :user_agent => 'My iPhone', :format => :html)
DiviningRod::Definitions.clear_definitions
DiviningRod::Definitions.define do |map|
DiviningRod::Mapping.clear_definitions
DiviningRod::Mapping.define do |map|
map.ua /iPhone/, :format => :webkit, :tags => [:iphone, :youtube, :geolocate] do |iphone|
iphone.ua /iPad/, :tags => [:ipad] do |ipad|
ipad.ua /Unicorns/, :tags => [:omg_unicorns]
Expand All @@ -17,20 +17,20 @@
end

it "should match a top level user agent" do
result = DiviningRod::Definitions.evaluate(@request)
result = DiviningRod::Mapping.evaluate(@request)
result.should_not be_nil
result.tags.should include(:iphone)
end

it "should match a child definition" do
ipad_request = mock("rails_request", :user_agent => 'My iPhone is really an iPad', :format => :html)
result = DiviningRod::Definitions.evaluate(ipad_request)
result = DiviningRod::Mapping.evaluate(ipad_request)
result.tags.should include(:ipad)
end

it "should match a sub child definition" do
ipad_request = mock("rails_request", :user_agent => 'New iPad - now with Unicorns', :format => :html)
result = DiviningRod::Definitions.evaluate(ipad_request)
result = DiviningRod::Mapping.evaluate(ipad_request)
result.tags.should include(:ipad)
result.tags.should include(:omg_unicorns)
end
Expand Down

0 comments on commit 2056e31

Please sign in to comment.