joshbuddy-usher 0.3.0 → 0.3.2

Sign up to get free protection for your applications and to get access to all the features.
data/Rakefile CHANGED
@@ -9,7 +9,7 @@ begin
9
9
  s.homepage = "http://github.com/joshbuddy/usher"
10
10
  s.authors = ["Joshua Hull"]
11
11
  s.files = FileList["[A-Z]*", "{lib,spec,rails}/**/*"]
12
- s.add_dependency 'joshbuddy-fuzzy_hash'
12
+ s.add_dependency 'joshbuddy-fuzzy_hash', '>=0.0.2'
13
13
  end
14
14
  rescue LoadError
15
15
  puts "Jeweler not available. Install it with: sudo gem install technicalpickles-jeweler -s http://gems.github.com"
data/VERSION.yml CHANGED
@@ -1,4 +1,4 @@
1
1
  ---
2
- :major: 0
3
2
  :minor: 3
4
- :patch: 0
3
+ :patch: 2
4
+ :major: 0
data/lib/usher.rb CHANGED
@@ -30,7 +30,7 @@ class Usher
30
30
  # set.reset!
31
31
  # set.empty? => true
32
32
  def reset!
33
- @tree = Node.root(self)
33
+ @tree = Node.root(self, @request_methods)
34
34
  @named_routes = {}
35
35
  @routes = []
36
36
  @route_count = 0
@@ -38,9 +38,16 @@ class Usher
38
38
  end
39
39
  alias clear! reset!
40
40
 
41
- # Creates a route set
42
- def initialize(delimiter = '/')
43
- @splitter = Splitter.delimiter(delimiter)
41
+ # Creates a route set, with optional Array of +delimiters+ and +request_methods+
42
+ #
43
+ # The +delimiters+ must be one character. By default <tt>['/', '.']</tt> are used.
44
+ # The +request_methods+ are methods that are called against the request object in order to
45
+ # enforce the +conditions+ segment of the routes. For HTTP routes (and in fact the default), those
46
+ # methods are <tt>[:protocol, :domain, :port, :query_string, :remote_ip, :user_agent, :referer, :method]</tt>.
47
+ def initialize(delimiters = ['/', '.'], request_methods = [:protocol, :domain, :port, :query_string, :remote_ip, :user_agent, :referer, :method])
48
+ @delimiters = delimiters
49
+ @splitter = Splitter.for_delimiters(delimiters)
50
+ @request_methods = request_methods
44
51
  reset!
45
52
  end
46
53
 
@@ -103,7 +110,7 @@ class Usher
103
110
  # === +options+
104
111
  # * +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.
105
112
  # * +requirements+ - After transformation, tests the condition using ===. If it returns false, it raises an <tt>Usher::ValidationException</tt>
106
- # * +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.
113
+ # * +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.
107
114
  # * Any other key is interpreted as a requirement for the variable of its name.
108
115
  def add_route(path, options = {})
109
116
  transformers = options.delete(:transformers) || {}
@@ -116,7 +123,8 @@ class Usher
116
123
  end
117
124
  end
118
125
 
119
- route = Route.new(path, self, {:transformers => transformers, :conditions => conditions, :requirements => requirements}).to(options)
126
+ route = Route.new(path, self, {:transformers => transformers, :conditions => conditions, :requirements => requirements})
127
+ route.to(options) unless options.empty?
120
128
 
121
129
  @tree.add(route)
122
130
  @routes << route
@@ -131,8 +139,8 @@ class Usher
131
139
  # set = Usher.new
132
140
  # route = set.add_route('/test')
133
141
  # set.recognize(Request.new('/test')).path.route == route => true
134
- def recognize(request)
135
- @tree.find(request, @splitter.url_split(request.path))
142
+ def recognize(request, path = request.path)
143
+ @tree.find(request, @splitter.url_split(path))
136
144
  end
137
145
 
138
146
  # Recognizes a set of +parameters+ and gets the closest matching Usher::Route::Path or +nil+ if no route exists.
@@ -153,6 +161,7 @@ class Usher
153
161
  # set.generate_url(route.primary_path, {:controller => 'c', :action => 'a'}) == '/c/a' => true
154
162
  def generate_url(route, params = {}, options = {})
155
163
  check_variables = options.key?(:check_variables) ? options.delete(:check_variables) : false
164
+ delimiter = options.key?(:delimiter) ? options.delete(:delimiter) : @delimiters.first
156
165
 
157
166
  path = case route
158
167
  when Symbol
@@ -184,16 +193,13 @@ class Usher
184
193
  case p.type
185
194
  when :*
186
195
  param_list.first.each {|dp| p.valid!(dp.to_s) } if check_variables
187
- generated_path << '/' << param_list.shift.collect{|dp| dp.to_s} * '/'
188
- when :'.:'
189
- p.valid!(param_list.first.to_s) if check_variables
190
- (dp = param_list.shift) && generated_path << '.' << dp.to_s
196
+ generated_path << param_list.shift.collect{|dp| dp.to_s} * delimiter
191
197
  else
192
198
  p.valid!(param_list.first.to_s) if check_variables
193
- (dp = param_list.shift) && generated_path << '/' << dp.to_s
199
+ (dp = param_list.shift) && generated_path << dp.to_s
194
200
  end
195
201
  else
196
- generated_path << '/' << p.to_s
202
+ generated_path << p.to_s
197
203
  end
198
204
  end
199
205
  unless params_hash.empty?
@@ -23,7 +23,7 @@ class Usher
23
23
  def call(env)
24
24
  response = @routes.recognize(Request.new(env['REQUEST_URI'], env['REQUEST_METHOD']))
25
25
  env['usher.params'] = response.params.inject({}){|h,(k,v)| h[k]=v; h }
26
- response.path.route.params.call(env)
26
+ response.path.route.params.first.call(env)
27
27
  end
28
28
 
29
29
  end
@@ -34,18 +34,19 @@ class Usher
34
34
  add_route('/:controller', options.merge({:action => 'index'}))
35
35
  @controller_route_added = true
36
36
  end
37
-
38
- options[:action] = 'index' unless options[:action]
39
37
 
38
+ options[:action] = 'index' unless options[:action]
39
+
40
+ path[0, 0] = '/' unless path[0] == ?/
40
41
  route = @usher.add_route(path, options)
41
- raise "your route must include a controller" unless route.primary_path.dynamic_set.include?(:controller) || route.params.include?(:controller)
42
+ raise "your route must include a controller" unless route.primary_path.dynamic_set.include?(:controller) || route.params.first.include?(:controller)
42
43
  route
43
44
  end
44
45
 
45
46
  def recognize(request)
46
47
  node = @usher.recognize(request)
47
48
  params = node.params.inject({}){|h,(k,v)| h[k]=v; h }
48
- request.path_parameters = (node.params.empty? ? node.path.route.params : node.path.route.params.merge(params)).with_indifferent_access
49
+ request.path_parameters = (node.params.empty? ? node.path.route.params.first : node.path.route.params.first.merge(params)).with_indifferent_access
49
50
  "#{request.path_parameters[:controller].camelize}Controller".constantize
50
51
  rescue
51
52
  raise ActionController::RoutingError, "No route matches #{request.path.inspect} with #{request.inspect}"
@@ -70,14 +71,16 @@ class Usher
70
71
  merged_options = options
71
72
  merged_options[:controller] = recall[:controller] unless options.key?(:controller)
72
73
  unless options.key?(:action)
73
- options[:action] = nil
74
+ options[:action] = ''
74
75
  end
75
76
  route_for_options(merged_options)
76
77
  end
77
78
  case method
78
79
  when :generate
79
80
  merged_options ||= recall.merge(options)
80
- generate_url(route, merged_options)
81
+ url = generate_url(route, merged_options)
82
+ url.slice!(-1) if url[-1] == ?/
83
+ url
81
84
  else
82
85
  raise "method #{method} not recognized"
83
86
  end
data/lib/usher/node.rb CHANGED
@@ -6,11 +6,10 @@ class Usher
6
6
 
7
7
  class Node
8
8
 
9
- ConditionalTypes = [:protocol, :domain, :port, :query_string, :remote_ip, :user_agent, :referer, :method]
10
9
  Response = Struct.new(:path, :params)
11
10
 
12
11
  attr_reader :lookup
13
- attr_accessor :terminates, :exclusive_type, :parent, :value
12
+ attr_accessor :terminates, :exclusive_type, :parent, :value, :request_methods
14
13
 
15
14
  def initialize(parent, value)
16
15
  @parent = parent
@@ -24,8 +23,10 @@ class Usher
24
23
  @depth ||= @parent && @parent.is_a?(Node) ? @parent.depth + 1 : 0
25
24
  end
26
25
 
27
- def self.root(route_set)
28
- self.new(route_set, nil)
26
+ def self.root(route_set, request_methods)
27
+ root = self.new(route_set, nil)
28
+ root.request_methods = request_methods
29
+ root
29
30
  end
30
31
 
31
32
  def has_globber?
@@ -63,8 +64,8 @@ class Usher
63
64
  def add(route)
64
65
  route.paths.each do |path|
65
66
  parts = path.parts.dup
66
- ConditionalTypes.each do |type|
67
- parts.push(Route::RequestMethod.new(type, route.conditions[type])) if route.conditions[type]
67
+ request_methods.each do |type|
68
+ parts.push(Route::RequestMethod.new(type, route.conditions[type])) if route.conditions.key?(type)
68
69
  end
69
70
 
70
71
  current_node = self
@@ -123,9 +124,6 @@ class Usher
123
124
  params.last.last << part
124
125
  when :':'
125
126
  params << [next_part.value.name, part]
126
- when:'.:'
127
- part.slice!(0)
128
- params << [next_part.value.name, part]
129
127
  end
130
128
  end
131
129
  next_part.find(request, path, params)
data/lib/usher/route.rb CHANGED
@@ -16,6 +16,7 @@ class Usher
16
16
  @transformers = options.delete(:transformers)
17
17
  @paths = @router.splitter.split(@original_path, @requirements, @transformers).collect {|path| Path.new(self, path)}
18
18
  @primary_path = @paths.first
19
+ @params = []
19
20
  end
20
21
 
21
22
 
@@ -27,7 +28,7 @@ class Usher
27
28
  # route.to(:controller => 'testing', :action => 'index')
28
29
  # set.recognize(Request.new('/test')).first.params => {:controller => 'testing', :action => 'index'}
29
30
  def to(options)
30
- @params = options
31
+ @params << options
31
32
  self
32
33
  end
33
34
 
@@ -3,11 +3,13 @@ require 'strscan'
3
3
  class Usher
4
4
  class Splitter
5
5
 
6
- def self.delimiter(delimiter = '/')
6
+ def self.for_delimiters(delimiters)
7
+ delimiters_regex = delimiters.collect{|d| Regexp.quote(d)} * '|'
8
+
7
9
  SplitterInstance.new(
8
- delimiter,
9
- Regexp.new('((:|\*||\.:|\.)[0-9A-Za-z\$\-_\+!\*\',]+|' + Regexp.quote(delimiter) + '|\(|\)|\|)'),
10
- Regexp.new(Regexp.quote(delimiter) + '|\.?[0-9A-Za-z\$\-_\+!\*\',]+')
10
+ delimiters,
11
+ Regexp.new('((:|\*)?[0-9A-Za-z\$\-_\+!\*\',]+|' + delimiters_regex + '|\(|\)|\|)'),
12
+ Regexp.new(delimiters_regex + '|[0-9A-Za-z\$\-_\+!\*\',]+')
11
13
  )
12
14
  end
13
15
 
@@ -15,8 +17,9 @@ class Usher
15
17
 
16
18
  class SplitterInstance
17
19
 
18
- def initialize(delimiter, split_regex, url_split_regex)
19
- @delimiter = delimiter
20
+ def initialize(delimiters, split_regex, url_split_regex)
21
+ @delimiters = delimiters
22
+ @delimiter_chars = delimiters.collect{|d| d[0]}
20
23
  @split_regex = split_regex
21
24
  @url_split_regex = url_split_regex
22
25
  end
@@ -26,7 +29,12 @@ class Usher
26
29
  ss = StringScanner.new(path)
27
30
  while !ss.eos?
28
31
  if part = ss.scan(@url_split_regex)
29
- parts << part unless part == @delimiter
32
+ parts << case part[0]
33
+ when *@delimiter_chars
34
+ part.to_sym
35
+ else
36
+ part
37
+ end
30
38
  end
31
39
  end if path && !path.empty?
32
40
  parts
@@ -39,7 +47,7 @@ class Usher
39
47
  while !ss.eos?
40
48
  part = ss.scan(@split_regex)
41
49
  case part[0]
42
- when ?*, ?:, ?.
50
+ when ?*, ?:
43
51
  type = (part[1] == ?: ? part.slice!(0,2) : part.slice!(0).chr).to_sym
44
52
  current_group << Usher::Route::Variable.new(type, part, :validator => requirements[part.to_sym], :transformer => transformers[part.to_sym])
45
53
  when ?(
@@ -59,7 +67,8 @@ class Usher
59
67
  end
60
68
  current_group.parent << Group.new(:all, current_group.parent)
61
69
  current_group = current_group.parent.last
62
- when @delimiter[0]
70
+ when *@delimiter_chars
71
+ current_group << part.to_sym
63
72
  else
64
73
  current_group << part
65
74
  end
data/spec/path_spec.rb CHANGED
@@ -22,12 +22,12 @@ describe "Usher route adding" do
22
22
  it "should add every kind of optional route possible" do
23
23
  route_set.add_route('/a/b(/c)(/d(/e))')
24
24
  route_set.routes.first.paths.collect{|a| a.parts }.should == [
25
- ["a", "b"],
26
- ["a", "b", "c", "d"],
27
- ["a", "b", "d", "e"],
28
- ["a", "b", "c"],
29
- ["a", "b", "d"],
30
- ["a", "b", "c", "d", "e"]
25
+ [:/, "a", :/, "b"],
26
+ [:/, "a", :/, "b", :/, "c", :/, "d"],
27
+ [:/, "a", :/, "b", :/, "d", :/, "e"],
28
+ [:/, "a", :/, "b", :/, "c"],
29
+ [:/, "a", :/, "b", :/, "d"],
30
+ [:/, "a", :/, "b", :/, "c", :/, "d", :/, "e"]
31
31
  ]
32
32
 
33
33
  end
@@ -28,7 +28,7 @@ describe "Usher (for rack) route dispatching" do
28
28
 
29
29
  it "should dispatch a simple request" do
30
30
  env = {'REQUEST_URI' => '/sample', 'REQUEST_METHOD' => 'get', 'usher.params' => {}}
31
- route_set.add('/sample', :controller => 'sample', :action => 'action').to(build_app_mock(env.dup))
31
+ route_set.add('/sample').to(build_app_mock(env.dup))
32
32
  route_set.call(env)
33
33
  end
34
34
 
@@ -10,17 +10,17 @@ describe "Usher (for rails) URL generation" do
10
10
  end
11
11
 
12
12
  it "should fill in the controller from recall" do
13
- route_set.add_route(':controller/:action/:id')
13
+ route_set.add_route('/:controller/:action/:id')
14
14
  route_set.generate({:action => 'thingy'}, {:controller => 'sample', :action => 'index', :id => 123}, :generate).should == '/sample/thingy'
15
15
  end
16
16
 
17
17
  it "should skip the action if not provided" do
18
- route_set.add_route(':controller/:action/:id')
18
+ route_set.add_route('/:controller/:action/:id')
19
19
  route_set.generate({:controller => 'thingy'}, {:controller => 'sample', :action => 'index', :id => 123}, :generate).should == '/thingy'
20
20
  end
21
21
 
22
22
  it "should pick the correct param from optional parts" do
23
- route_set.add_route(':controller/:action(.:format)')
23
+ route_set.add_route('/:controller/:action(.:format)')
24
24
  route_set.generate({:action => 'thingy', :format => 'html'}, {:controller => 'sample', :action => 'index', :id => 123}, :generate).should == '/sample/thingy.html'
25
25
  route_set.generate({:action => 'thingy'}, {:controller => 'sample', :action => 'index', :id => 123}, :generate).should == '/sample/thingy'
26
26
  end
@@ -36,12 +36,12 @@ describe "Usher route recognition" do
36
36
  end
37
37
 
38
38
  it "should recognize a format-style literal" do
39
- target_route = route_set.add_route(':action.html', :controller => 'sample', :action => 'action')
39
+ target_route = route_set.add_route('/:action.html', :controller => 'sample', :action => 'action')
40
40
  route_set.recognize(build_request({:method => 'get', :path => '/sample.html', :domain => 'admin.host.com'})).should == Usher::Node::Response.new(target_route.paths.first, [[:action , 'sample']])
41
41
  end
42
42
 
43
43
  it "should recognize a format-style variable along side another variable" do
44
- target_route = route_set.add_route(':action.:format', :controller => 'sample', :action => 'action')
44
+ target_route = route_set.add_route('/:action.:format', :controller => 'sample', :action => 'action')
45
45
  route_set.recognize(build_request({:method => 'get', :path => '/sample.html', :domain => 'admin.host.com'})).should == Usher::Node::Response.new(target_route.paths.first, [[:action , 'sample'], [:format, 'html']])
46
46
  end
47
47
 
@@ -57,12 +57,21 @@ describe "Usher route recognition" do
57
57
  end
58
58
 
59
59
  it "should correctly fix that tree if conditionals are used later" do
60
- noop_route = route_set.add_route('noop', :controller => 'products', :action => 'noop')
60
+ noop_route = route_set.add_route('/noop', :controller => 'products', :action => 'noop')
61
61
  product_show_route = route_set.add_route('/products/show/:id', :id => /\d+/, :conditions => {:method => 'get'})
62
62
  noop_route.paths.include?(route_set.recognize(build_request({:method => 'get', :path => '/noop', :domain => 'admin.host.com'})).first).should == true
63
63
  product_show_route.paths.include?(route_set.recognize(build_request({:method => 'get', :path => '/products/show/123', :domain => 'admin.host.com'})).first).should == true
64
64
  end
65
65
 
66
+ it "should use conditionals that are boolean" do
67
+ # hijacking user_agent
68
+ insecure_product_show_route = route_set.add_route('/products/show/:id', :id => /\d+/, :conditions => {:user_agent => false, :method => 'get'})
69
+ secure_product_show_route = route_set.add_route('/products/show/:id', :id => /\d+/, :conditions => {:user_agent => true, :method => 'get'})
70
+
71
+ secure_product_show_route.should == route_set.recognize(build_request({:method => 'get', :path => '/products/show/123', :domain => 'admin.host.com', :user_agent => true})).path.route
72
+ insecure_product_show_route.should == route_set.recognize(build_request({:method => 'get', :path => '/products/show/123', :domain => 'admin.host.com', :user_agent => false})).path.route
73
+ end
74
+
66
75
  it "should use a transformer (proc) on incoming variables" do
67
76
  route_set.add_route('/:controller/:action/:id', :transformers => {:id => proc{|v| v.to_i}})
68
77
  route_set.recognize(build_request({:method => 'get', :path => '/products/show/123asd', :domain => 'admin.host.com'})).params.rassoc(123).first.should == :id
data/spec/split_spec.rb CHANGED
@@ -4,53 +4,53 @@ describe "Usher route tokenizing" do
4
4
 
5
5
 
6
6
  it "should split / delimited routes" do
7
- Usher::Splitter.delimiter('/').split('/test/this/split').should == [['test', 'this', 'split']]
7
+ Usher::Splitter.for_delimiters(['/', '.']).split('/test/this/split').should == [[:/, 'test', :/,'this', :/, 'split']]
8
8
  end
9
9
 
10
10
  it "should split on ' ' delimited routes as well" do
11
- Usher::Splitter.delimiter(' ').split('test this split').should == [['test', 'this', 'split']]
11
+ Usher::Splitter.for_delimiters([' ']).split('test this split').should == [['test', :' ', 'this', :' ', 'split']]
12
12
  end
13
13
 
14
14
  it "should split on ' ' delimited routes for more complex routes as well" do
15
- Usher::Splitter.delimiter(' ').split('(test|this) split').should == [['test', 'split'], ['this', 'split']]
15
+ Usher::Splitter.for_delimiters([' ']).split('(test|this) split').should == [['test', :' ', 'split'], ['this', :' ', 'split']]
16
16
  end
17
17
 
18
18
  it "should group optional parts with brackets" do
19
- Usher::Splitter.delimiter('/').split('/test/this(/split)').should == [
20
- ['test', 'this'],
21
- ['test', 'this', 'split']
19
+ Usher::Splitter.for_delimiters(['/', '.']).split('/test/this(/split)').should == [
20
+ [:/, 'test', :/, 'this'],
21
+ [:/, 'test', :/, 'this', :/, 'split']
22
22
  ]
23
23
  end
24
24
 
25
25
  it "should group exclusive optional parts with brackets and pipes" do
26
- Usher::Splitter.delimiter('/').split('/test/this(/split|/split2)').should == [
27
- ['test', 'this', 'split'],
28
- ['test', 'this', 'split2']
26
+ Usher::Splitter.for_delimiters(['/', '.']).split('/test/this(/split|/split2)').should == [
27
+ [:/, 'test', :/, 'this',:/, 'split'],
28
+ [:/, 'test', :/, 'this',:/, 'split2']
29
29
  ]
30
30
  end
31
31
 
32
32
  it "should group exclusive optional-optional parts with brackets and pipes" do
33
- Usher::Splitter.delimiter('/').split('/test/this((/split|/split2))').should == [
34
- ['test', 'this'],
35
- ['test', 'this', 'split'],
36
- ['test', 'this', 'split2']
33
+ Usher::Splitter.for_delimiters(['/', '.']).split('/test/this((/split|/split2))').should == [
34
+ [:/, 'test',:/, 'this'],
35
+ [:/, 'test',:/, 'this', :/, 'split'],
36
+ [:/, 'test',:/, 'this', :/, 'split2']
37
37
  ]
38
38
  end
39
39
 
40
40
  it "should group optional parts with brackets (for non overlapping groups)" do
41
- Usher::Splitter.delimiter('/').split('/test/this(/split)(/split2)') == [
42
- ["test", "this"],
43
- ["test", "this", "split"],
44
- ["test", "this", "split2"],
45
- ["test", "this", "split", "split2"]
41
+ Usher::Splitter.for_delimiters(['/', '.']).split('/test/this(/split)(/split2)') == [
42
+ [:/, "test", :/, "this"],
43
+ [:/, "test", :/, "this", :/, "split"],
44
+ [:/, "test", :/, "this", :/, "split2"],
45
+ [:/, "test", :/, "this", :/, "split", :/, "split2"]
46
46
  ]
47
47
  end
48
48
 
49
49
  it "should group nested-optional parts with brackets" do
50
- Usher::Splitter.delimiter('/').split('/test/this(/split(.:format))') == [
51
- ["test", "this"],
52
- ["test", "this", "split"],
53
- ["test", "this", "split", Usher::Route::Variable.new(:'.:', :format)]
50
+ Usher::Splitter.for_delimiters(['/', '.']).split('/test/this(/split(.:format))') == [
51
+ [:/, "test", :/, "this"],
52
+ [:/, "test", :/, "this", :/, "split"],
53
+ [:/, "test", :/, "this", :/, "split", '.', Usher::Route::Variable.new(:':', :format)]
54
54
  ]
55
55
  end
56
56
 
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.3.0
4
+ version: 0.3.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-04-03 00:00:00 -07:00
12
+ date: 2009-04-06 00:00:00 -07:00
13
13
  default_executable:
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
@@ -20,7 +20,7 @@ dependencies:
20
20
  requirements:
21
21
  - - ">="
22
22
  - !ruby/object:Gem::Version
23
- version: "0"
23
+ version: 0.0.2
24
24
  version:
25
25
  description: A general purpose routing library
26
26
  email: joshbuddy@gmail.com