joshbuddy-usher 0.4.1 → 0.4.2

Sign up to get free protection for your applications and to get access to all the features.
data/README.rdoc CHANGED
@@ -43,9 +43,25 @@ However, <tt>/path/test/one_more/path</tt> would not be matched.
43
43
 
44
44
  In the above examples, ['one', 'two', 'three'] and ['four', 'five'] respectively would be bound to the key :variable.
45
45
 
46
+ As well, variables can have a regex matcher.
47
+
48
+ <b>Example:</b>
49
+ <tt>/product/{:id,\d+}</tt> would match
50
+
51
+ * <tt>/product/123</tt>
52
+ * <tt>/product/4521</tt>
53
+
54
+ But not
55
+ * <tt>/product/AE-35</tt>
56
+
57
+ As well, the same logic applies for * variables as well, where only parts matchable by the supplied regex will
58
+ actually be bound to the variable
59
+
46
60
  ==== Static
47
61
 
48
62
  Static parts of literal character sequences. For instance, <tt>/path/something.html</tt> would match only the same path.
63
+ As well, static parts can have a regex pattern in them as well, such as <tt>/path/something.{html|xml}</tt> which would match only
64
+ <tt>/path/something.html</tt> and <tt>/path/something.xml</tt>
49
65
 
50
66
  ==== Optional sections
51
67
 
@@ -53,12 +69,14 @@ Sections of a route can be marked as optional by surrounding it with brackets. F
53
69
 
54
70
  ==== One and only one sections
55
71
 
56
- Sections of a route can be marked as "one and only one" by surrounding it with brackets and separating parts of the route with pipes. For instance, the path, <tt>/path/something(.xml|.html)</tt> would only match <tt>/path/something.xml</tt> and <tt>/path/something.html</tt>.
72
+ Sections of a route can be marked as "one and only one" by surrounding it with brackets and separating parts of the route with pipes.
73
+ For instance, the path, <tt>/path/something(.xml|.html)</tt> would only match <tt>/path/something.xml</tt> and
74
+ <tt>/path/something.html</tt>. Generally its more efficent to use one and only sections over using regex.
57
75
 
58
76
  === +options+
59
77
  * +transformers+ - Transforms a variable before it gets to the requirements. Takes either a +proc+ or a +symbol+. If its a +symbol+, calls the method on the incoming parameter. If its a +proc+, its called with the variable.
60
78
  * +requirements+ - After transformation, tests the condition using ===. If it returns false, it raises an <tt>Usher::ValidationException</tt>
61
- * +conditions+ - Accepts any of the following <tt>:protocol</tt>, <tt>:domain</tt>, <tt>:port</tt>, <tt>:query_string</tt>, <tt>:remote_ip</tt>, <tt>:user_agent</tt>, <tt>:referer</tt> and <tt>:method</tt>. This can be either a <tt>string</tt> or a regular expression.
79
+ * +conditions+ - Accepts any of the +request_methods+ specificied in the construction of Usher. This can be either a <tt>string</tt> or a regular expression.
62
80
  * Any other key is interpreted as a requirement for the variable of its name.
63
81
 
64
82
  == Rails
data/VERSION.yml CHANGED
@@ -1,4 +1,4 @@
1
1
  ---
2
2
  :major: 0
3
3
  :minor: 4
4
- :patch: 1
4
+ :patch: 2
@@ -0,0 +1,31 @@
1
+ $:.unshift File.dirname(__FILE__)
2
+
3
+ require 'rack_interface/route'
4
+
5
+ class Usher
6
+ module Interface
7
+ class EmailInterface
8
+
9
+ def initialize(&blk)
10
+ @routes = Usher.new(:delimiters => ['@', '-', '.'], :valid_regex => '[\+a-zA-Z0-9]+', :globs_capture_separators => true)
11
+ instance_eval(&blk) if blk
12
+ end
13
+
14
+ def for(path, &block)
15
+ @routes.add_route(path).to(block)
16
+ end
17
+
18
+ def reset!
19
+ @routes.reset!
20
+ end
21
+
22
+ def act(email)
23
+ response = @routes.recognize(email, email)
24
+ if response.path
25
+ response.path.route.params.first.call(response.params.inject({}){|h,(k,v)| h[k]=v.to_s; h })
26
+ end
27
+ end
28
+
29
+ end
30
+ end
31
+ end
@@ -5,6 +5,7 @@ class Usher
5
5
  autoload :Rails2Interface, 'interface/rails2_interface'
6
6
  autoload :MerbInterface, 'interface/merb_interface'
7
7
  autoload :RackInterface, 'interface/rack_interface'
8
+ autoload :EmailInterface, 'interface/email_interface'
8
9
 
9
10
  def self.for(type, &blk)
10
11
  case type
@@ -14,7 +15,10 @@ class Usher
14
15
  MerbInterface.new(&blk)
15
16
  when :rack
16
17
  RackInterface.new(&blk)
18
+ when :email
19
+ EmailInterface.new(&blk)
17
20
  end
21
+
18
22
  end
19
23
 
20
24
 
data/lib/usher/node.rb CHANGED
@@ -9,7 +9,7 @@ class Usher
9
9
  Response = Struct.new(:path, :params)
10
10
 
11
11
  attr_reader :lookup
12
- attr_accessor :terminates, :exclusive_type, :parent, :value, :request_methods
12
+ attr_accessor :terminates, :exclusive_type, :parent, :value, :request_methods, :globs_capture_separators
13
13
 
14
14
  def initialize(parent, value)
15
15
  @parent = parent
@@ -26,9 +26,10 @@ class Usher
26
26
  @depth ||= @parent && @parent.is_a?(Node) ? @parent.depth + 1 : 0
27
27
  end
28
28
 
29
- def self.root(route_set, request_methods)
29
+ def self.root(route_set, request_methods, globs_capture_separators)
30
30
  root = self.new(route_set, nil)
31
31
  root.request_methods = request_methods
32
+ root.globs_capture_separators = globs_capture_separators
32
33
  root
33
34
  end
34
35
 
@@ -69,6 +70,8 @@ class Usher
69
70
  current_node.lookup[nil] ||= Node.new(current_node, Route::RequestMethod.new(current_node.exclusive_type, nil))
70
71
  end
71
72
  else
73
+ key.globs_capture_separators = globs_capture_separators if key.is_a?(Route::Variable)
74
+
72
75
  if !key.is_a?(Route::Variable)
73
76
  current_node.upgrade_lookup if key.is_a?(Regexp)
74
77
  current_node.lookup[key] ||= Node.new(current_node, key)
@@ -116,7 +119,7 @@ class Usher
116
119
  path.unshift(next_part.parent.value) if next_part.parent.value.is_a?(Symbol)
117
120
  break
118
121
  else
119
- unless part.is_a?(Symbol)
122
+ unless part.is_a?(Symbol) && !next_part.value.globs_capture_separators
120
123
  part = next_part.value.transform!(part)
121
124
  next_part.value.valid!(part)
122
125
  params.last.last << part
@@ -152,7 +155,7 @@ class Usher
152
155
  path.unshift(next_part.parent.value) if next_part.parent.value.is_a?(Symbol)
153
156
  next_part.find(request, path, params)
154
157
  else
155
- unless part.is_a?(Symbol)
158
+ unless part.is_a?(Symbol) && !next_part.value.globs_capture_separators
156
159
  part = next_part.value.transform!(part)
157
160
  next_part.value.valid!(part)
158
161
  params.last.last << part
@@ -2,14 +2,15 @@ class Usher
2
2
  class Route
3
3
  class Variable
4
4
  attr_reader :type, :name, :validator, :transformer, :regex_matcher
5
- attr_accessor :look_ahead
5
+ attr_accessor :look_ahead, :globs_capture_separators
6
6
 
7
- def initialize(type, name, validator = nil, transformer = nil, regex_matcher = nil)
7
+ def initialize(type, name, validator = nil, transformer = nil, regex_matcher = nil, globs_capture_separators = false)
8
8
  @type = type
9
9
  @name = :"#{name}"
10
10
  @validator = validator
11
11
  @transformer = transformer
12
12
  @regex_matcher = regex_matcher
13
+ @globs_capture_separators = globs_capture_separators
13
14
  end
14
15
 
15
16
  def to_s
@@ -3,13 +3,12 @@ require 'strscan'
3
3
  class Usher
4
4
  class Splitter
5
5
 
6
- def self.for_delimiters(delimiters)
6
+ def self.for_delimiters(delimiters, valid_regex)
7
7
  delimiters_regex = delimiters.collect{|d| Regexp.quote(d)} * '|'
8
-
9
8
  SplitterInstance.new(
10
9
  delimiters,
11
- Regexp.new('((:|\*)?[0-9A-Za-z\$\-_\+!\*\',]+|' + delimiters_regex + '|\(|\)|\||\{)'),
12
- Regexp.new(delimiters_regex + '|[0-9A-Za-z\$\-_\+!\*\',]+')
10
+ Regexp.new('((:|\*)?' + valid_regex + '|' + delimiters_regex + '|\(|\)|\||\{)'),
11
+ Regexp.new(delimiters_regex + '|' + valid_regex)
13
12
  )
14
13
  end
15
14
 
@@ -51,7 +50,7 @@ class Usher
51
50
  type = part.slice!(0).chr.to_sym
52
51
  current_group << Usher::Route::Variable.new(type, part, requirements && requirements[part.to_sym], transformers && transformers[part.to_sym])
53
52
  when ?{
54
- pattern = ''
53
+ pattern = '^'
55
54
  count = 1
56
55
  variable = ss.scan(/[:\*]([^,]+),/)
57
56
  until count.zero?
@@ -64,7 +63,7 @@ class Usher
64
63
  end
65
64
  pattern << regex_part
66
65
  end
67
- pattern.slice!(pattern.size - 1)
66
+ pattern[pattern.size - 1] = ?$
68
67
  regex = Regexp.new(pattern)
69
68
  if variable
70
69
  variable_type = variable.slice!(0).chr.to_sym
data/lib/usher.rb CHANGED
@@ -31,7 +31,7 @@ class Usher
31
31
  # set.reset!
32
32
  # set.empty? => true
33
33
  def reset!
34
- @tree = Node.root(self, @request_methods)
34
+ @tree = Node.root(self, @request_methods, @globs_capture_separators)
35
35
  @named_routes = {}
36
36
  @routes = []
37
37
  @route_count = 0
@@ -39,16 +39,24 @@ class Usher
39
39
  end
40
40
  alias clear! reset!
41
41
 
42
- # Creates a route set, with optional Array of +delimiters+ and +request_methods+
42
+ # Creates a route set, with options
43
43
  #
44
- # The +delimiters+ must be one character. By default <tt>['/', '.']</tt> are used.
45
- # The +request_methods+ are methods that are called against the request object in order to
46
- # enforce the +conditions+ segment of the routes. For HTTP routes (and in fact the default), those
47
- # methods are <tt>[:protocol, :domain, :port, :query_string, :remote_ip, :user_agent, :referer, :method]</tt>.
44
+ # <tt>:globs_capture_separators</tt>: +true+ or +false+. (default +false+) Specifies whether glob matching will also include separators
45
+ # that are matched.
46
+ #
47
+ # <tt>:delimiters</tt>: Array of Strings. (default <tt>['/', '.']</tt>). Delimiters used in path separation. Array must be single character strings.
48
+ #
49
+ # <tt>:valid_regex</tt>: String. (default <tt>'[0-9A-Za-z\$\-_\+!\*\',]+'</tt>). String that can be interpolated into regex to match
50
+ # valid character sequences within path.
51
+ #
52
+ # <tt>:request_methods</tt>: Array of Symbols. (default <tt>[:protocol, :domain, :port, :query_string, :remote_ip, :user_agent, :referer, :method, :subdomains]</tt>)
53
+ # Array of methods called against the request object for the purposes of matching route requirements.
48
54
  def initialize(options = nil)
49
- @delimiters = options && options.delete(:options) || ['/', '.']
55
+ @globs_capture_separators = options && options.key?(:globs_capture_separators) ? options.delete(:globs_capture_separators) : false
56
+ @delimiters = options && options.delete(:delimiters) || ['/', '.']
57
+ @valid_regex = options && options.delete(:valid_regex) || '[0-9A-Za-z\$\-_\+!\*\',]+'
50
58
  @request_methods = options && options.delete(:request_methods) || [:protocol, :domain, :port, :query_string, :remote_ip, :user_agent, :referer, :method, :subdomains]
51
- @splitter = Splitter.for_delimiters(@delimiters)
59
+ @splitter = Splitter.for_delimiters(@delimiters, @valid_regex)
52
60
  reset!
53
61
  end
54
62
 
@@ -96,9 +104,25 @@ class Usher
96
104
  #
97
105
  # In the above examples, ['one', 'two', 'three'] and ['four', 'five'] respectively would be bound to the key :variable.
98
106
  #
107
+ # As well, variables can have a regex matcher.
108
+ #
109
+ # <b>Example:</b>
110
+ # <tt>/product/{:id,\d+}</tt> would match
111
+ #
112
+ # * <tt>/product/123</tt>
113
+ # * <tt>/product/4521</tt>
114
+ #
115
+ # But not
116
+ # * <tt>/product/AE-35</tt>
117
+ #
118
+ # As well, the same logic applies for * variables as well, where only parts matchable by the supplied regex will
119
+ # actually be bound to the variable
120
+ #
99
121
  # ==== Static
100
122
  #
101
123
  # Static parts of literal character sequences. For instance, <tt>/path/something.html</tt> would match only the same path.
124
+ # As well, static parts can have a regex pattern in them as well, such as <tt>/path/something.{html|xml}</tt> which would match only
125
+ # <tt>/path/something.html</tt> and <tt>/path/something.xml</tt>
102
126
  #
103
127
  # ==== Optional sections
104
128
  #
@@ -106,7 +130,9 @@ class Usher
106
130
  #
107
131
  # ==== One and only one sections
108
132
  #
109
- # Sections of a route can be marked as "one and only one" by surrounding it with brackets and separating parts of the route with pipes. For instance, the path, <tt>/path/something(.xml|.html)</tt> would only match <tt>/path/something.xml</tt> and <tt>/path/something.html</tt>.
133
+ # Sections of a route can be marked as "one and only one" by surrounding it with brackets and separating parts of the route with pipes.
134
+ # For instance, the path, <tt>/path/something(.xml|.html)</tt> would only match <tt>/path/something.xml</tt> and
135
+ # <tt>/path/something.html</tt>. Generally its more efficent to use one and only sections over using regex.
110
136
  #
111
137
  # === +options+
112
138
  # * +transformers+ - Transforms a variable before it gets to the requirements. Takes either a +proc+ or a +symbol+. If its a +symbol+, calls the method on the incoming parameter. If its a +proc+, its called with the variable.
@@ -0,0 +1,40 @@
1
+ require 'lib/usher'
2
+
3
+
4
+
5
+ def build_email_mock(email)
6
+ request = mock "Request"
7
+ request.should_receive(:email).any_number_of_times.and_return(email)
8
+ request
9
+ end
10
+
11
+ SampleController = Object.new
12
+
13
+ describe "Usher (for email) route recognition" do
14
+
15
+ before(:each) do
16
+ @route_set = Usher::Interface.for(:email)
17
+ end
18
+
19
+ it "should recognize a simple request" do
20
+ receiver = mock('receiver')
21
+ receiver.should_receive(:action).with({}).exactly(1)
22
+ @route_set.for('joshbuddy@gmail.com') { |params| receiver.action(params) }
23
+ @route_set.act('joshbuddy@gmail.com')
24
+ end
25
+
26
+ it "should recognize a wildcard domain" do
27
+ receiver = mock('receiver')
28
+ receiver.should_receive(:action).with({:domain => 'gmail.com'}).exactly(1)
29
+ @route_set.for('joshbuddy@*domain') { |params| receiver.action(params) }
30
+ @route_set.act('joshbuddy@gmail.com')
31
+ end
32
+
33
+ it "should recognize a complex email" do
34
+ receiver = mock('receiver')
35
+ receiver.should_receive(:action).with({:subject => 'sub+ect', :id => '123', :sid => '456', :tok => 'sdqwe123ae', :domain => 'mydomain.org'}).exactly(1)
36
+ @route_set.for(':subject.{:id,^\d+$}-{:sid,^\d+$}-{:tok,^\w+$}@*domain') { |params| receiver.action(params) }
37
+ @route_set.act('sub+ect.123-456-sdqwe123ae@mydomain.org')
38
+ end
39
+
40
+ end
@@ -4,38 +4,42 @@ describe "Usher route tokenizing" do
4
4
 
5
5
 
6
6
  it "should split / delimited routes" do
7
- Usher::Splitter.for_delimiters(['/', '.']).split('/test/this/split').should == [[:/, 'test', :/,'this', :/, 'split']]
7
+ Usher::Splitter.for_delimiters(['/', '.'], '[0-9A-Za-z\$\-_\+!\*\',]+').split('/test/this/split').should == [[:/, 'test', :/,'this', :/, 'split']]
8
8
  end
9
9
 
10
10
  it "should split / delimited routes with a regex in it" do
11
- Usher::Splitter.for_delimiters(['/', '.']).
12
- split('/test/{this}/split').should == [[:/, 'test', :/, /this/, :/, 'split']]
11
+ Usher::Splitter.for_delimiters(['/', '.'], '[0-9A-Za-z\$\-_\+!\*\',]+').
12
+ split('/test/{this}/split').should == [[:/, 'test', :/, /^this$/, :/, 'split']]
13
13
  end
14
14
 
15
15
  it "should split on ' ' delimited routes as well" do
16
- Usher::Splitter.for_delimiters([' ']).split('test this split').should == [['test', :' ', 'this', :' ', 'split']]
16
+ Usher::Splitter.for_delimiters([' '], '[0-9A-Za-z\$\-_\+!\*\',]+').split('test this split').should == [['test', :' ', 'this', :' ', 'split']]
17
+ end
18
+
19
+ it "should split on email delimiters as well" do
20
+ Usher::Splitter.for_delimiters(['@', '+', '-', '.'], '[a-zA-Z0-9]+').split('one+more.12345-09876-alphanum3ric5@domain.com').should == [["one", :+, "more", :".", "12345", :-, "09876", :-, "alphanum3ric5", :"@", "domain", :".", "com"]]
17
21
  end
18
22
 
19
23
  it "should split on ' ' delimited routes for more complex routes as well" do
20
- Usher::Splitter.for_delimiters([' ']).split('(test|this) split').should == [['test', :' ', 'split'], ['this', :' ', 'split']]
24
+ Usher::Splitter.for_delimiters([' '], '[0-9A-Za-z\$\-_\+!\*\',]+').split('(test|this) split').should == [['test', :' ', 'split'], ['this', :' ', 'split']]
21
25
  end
22
26
 
23
27
  it "should group optional parts with brackets" do
24
- Usher::Splitter.for_delimiters(['/', '.']).split('/test/this(/split)').should == [
28
+ Usher::Splitter.for_delimiters(['/', '.'], '[0-9A-Za-z\$\-_\+!\*\',]+').split('/test/this(/split)').should == [
25
29
  [:/, 'test', :/, 'this'],
26
30
  [:/, 'test', :/, 'this', :/, 'split']
27
31
  ]
28
32
  end
29
33
 
30
34
  it "should group exclusive optional parts with brackets and pipes" do
31
- Usher::Splitter.for_delimiters(['/', '.']).split('/test/this(/split|/split2)').should == [
35
+ Usher::Splitter.for_delimiters(['/', '.'], '[0-9A-Za-z\$\-_\+!\*\',]+').split('/test/this(/split|/split2)').should == [
32
36
  [:/, 'test', :/, 'this',:/, 'split'],
33
37
  [:/, 'test', :/, 'this',:/, 'split2']
34
38
  ]
35
39
  end
36
40
 
37
41
  it "should group exclusive optional-optional parts with brackets and pipes" do
38
- Usher::Splitter.for_delimiters(['/', '.']).split('/test/this((/split|/split2))').should == [
42
+ Usher::Splitter.for_delimiters(['/', '.'], '[0-9A-Za-z\$\-_\+!\*\',]+').split('/test/this((/split|/split2))').should == [
39
43
  [:/, 'test',:/, 'this'],
40
44
  [:/, 'test',:/, 'this', :/, 'split'],
41
45
  [:/, 'test',:/, 'this', :/, 'split2']
@@ -43,7 +47,7 @@ describe "Usher route tokenizing" do
43
47
  end
44
48
 
45
49
  it "should group optional parts with brackets (for non overlapping groups)" do
46
- Usher::Splitter.for_delimiters(['/', '.']).split('/test/this(/split)(/split2)') == [
50
+ Usher::Splitter.for_delimiters(['/', '.'], '[0-9A-Za-z\$\-_\+!\*\',]+').split('/test/this(/split)(/split2)') == [
47
51
  [:/, "test", :/, "this"],
48
52
  [:/, "test", :/, "this", :/, "split"],
49
53
  [:/, "test", :/, "this", :/, "split2"],
@@ -52,7 +56,7 @@ describe "Usher route tokenizing" do
52
56
  end
53
57
 
54
58
  it "should group nested-optional parts with brackets" do
55
- Usher::Splitter.for_delimiters(['/', '.']).split('/test/this(/split(.:format))') == [
59
+ Usher::Splitter.for_delimiters(['/', '.'], '[0-9A-Za-z\$\-_\+!\*\',]+').split('/test/this(/split(.:format))') == [
56
60
  [:/, "test", :/, "this"],
57
61
  [:/, "test", :/, "this", :/, "split"],
58
62
  [:/, "test", :/, "this", :/, "split", :'.', Usher::Route::Variable.new(:':', :format)]
@@ -60,12 +64,12 @@ describe "Usher route tokenizing" do
60
64
  end
61
65
 
62
66
  it "should to_s all different variable types" do
63
- Usher::Splitter.for_delimiters(['/', '.']).split('/:split/*splitter').first.collect{|v| v.to_s} ==
67
+ Usher::Splitter.for_delimiters(['/', '.'], '[0-9A-Za-z\$\-_\+!\*\',]+').split('/:split/*splitter').first.collect{|v| v.to_s} ==
64
68
  [ ':split', '*splitter' ]
65
69
  end
66
70
 
67
71
  it "should == variable types" do
68
- parts = Usher::Splitter.for_delimiters(['/', '.']).split('/:split/:split').first
72
+ parts = Usher::Splitter.for_delimiters(['/', '.'], '[0-9A-Za-z\$\-_\+!\*\',]+').split('/:split/:split').first
69
73
  parts[1].should == parts[3]
70
74
  end
71
75
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: joshbuddy-usher
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.1
4
+ version: 0.4.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Joshua Hull
@@ -9,7 +9,7 @@ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
11
 
12
- date: 2009-05-09 00:00:00 -07:00
12
+ date: 2009-05-11 00:00:00 -07:00
13
13
  default_executable:
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
@@ -40,6 +40,7 @@ files:
40
40
  - lib/usher/exceptions.rb
41
41
  - lib/usher/grapher.rb
42
42
  - lib/usher/interface
43
+ - lib/usher/interface/email_interface.rb
43
44
  - lib/usher/interface/merb_interface.rb
44
45
  - lib/usher/interface/rack_interface
45
46
  - lib/usher/interface/rack_interface/mapper.rb
@@ -58,6 +59,8 @@ files:
58
59
  - lib/usher/splitter.rb
59
60
  - lib/usher.rb
60
61
  - spec/private
62
+ - spec/private/email
63
+ - spec/private/email/recognize_spec.rb
61
64
  - spec/private/generate_spec.rb
62
65
  - spec/private/grapher_spec.rb
63
66
  - spec/private/path_spec.rb