joshbuddy-usher 0.4.1 → 0.4.2

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.
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