usher 0.4.8

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.
Files changed (43) hide show
  1. data/History.txt +3 -0
  2. data/Manifest.txt +35 -0
  3. data/README.rdoc +126 -0
  4. data/Rakefile +72 -0
  5. data/VERSION.yml +4 -0
  6. data/lib/usher/exceptions.rb +5 -0
  7. data/lib/usher/generate.rb +131 -0
  8. data/lib/usher/grapher.rb +65 -0
  9. data/lib/usher/interface/email_interface.rb +27 -0
  10. data/lib/usher/interface/merb_interface.rb +61 -0
  11. data/lib/usher/interface/rack_interface/mapper.rb +0 -0
  12. data/lib/usher/interface/rack_interface/route.rb +9 -0
  13. data/lib/usher/interface/rack_interface.rb +37 -0
  14. data/lib/usher/interface/rails2_2_interface/mapper.rb +44 -0
  15. data/lib/usher/interface/rails2_2_interface.rb +135 -0
  16. data/lib/usher/interface/rails2_3_interface.rb +135 -0
  17. data/lib/usher/interface.rb +27 -0
  18. data/lib/usher/node.rb +138 -0
  19. data/lib/usher/route/path.rb +24 -0
  20. data/lib/usher/route/request_method.rb +22 -0
  21. data/lib/usher/route/variable.rb +37 -0
  22. data/lib/usher/route.rb +58 -0
  23. data/lib/usher/splitter.rb +159 -0
  24. data/lib/usher.rb +184 -0
  25. data/rails/init.rb +8 -0
  26. data/spec/private/email/recognize_spec.rb +38 -0
  27. data/spec/private/generate_spec.rb +141 -0
  28. data/spec/private/grapher_spec.rb +41 -0
  29. data/spec/private/path_spec.rb +68 -0
  30. data/spec/private/rack/dispatch_spec.rb +29 -0
  31. data/spec/private/rails2_2/compat.rb +1 -0
  32. data/spec/private/rails2_2/generate_spec.rb +28 -0
  33. data/spec/private/rails2_2/path_spec.rb +16 -0
  34. data/spec/private/rails2_2/recognize_spec.rb +79 -0
  35. data/spec/private/rails2_3/compat.rb +1 -0
  36. data/spec/private/rails2_3/generate_spec.rb +28 -0
  37. data/spec/private/rails2_3/path_spec.rb +16 -0
  38. data/spec/private/rails2_3/recognize_spec.rb +79 -0
  39. data/spec/private/recognize_spec.rb +178 -0
  40. data/spec/private/request_method_spec.rb +15 -0
  41. data/spec/private/split_spec.rb +76 -0
  42. data/spec/spec.opts +7 -0
  43. metadata +120 -0
@@ -0,0 +1,135 @@
1
+ require File.join(File.dirname(__FILE__), 'rails2_2_interface', 'mapper')
2
+
3
+ class Usher
4
+ module Interface
5
+ class Rails2_2Interface
6
+
7
+ attr_reader :usher
8
+ attr_accessor :configuration_file
9
+
10
+ def initialize
11
+ reset!
12
+ end
13
+
14
+ def reset!
15
+ @usher ||= Usher.new
16
+ @url_generator ||= Usher::Generators::URL.new(@usher)
17
+ @module ||= Module.new
18
+ @module.instance_methods.each do |selector|
19
+ @module.class_eval { remove_method selector }
20
+ end
21
+ @controller_action_route_added = false
22
+ @controller_route_added = false
23
+ @usher.reset!
24
+ end
25
+
26
+ def add_route(path, options = {})
27
+ if !@controller_action_route_added && path =~ %r{^/?:controller/:action/:id$}
28
+ add_route('/:controller/:action', options.dup)
29
+ @controller_action_route_added = true
30
+ end
31
+
32
+ if !@controller_route_added && path =~ %r{^/?:controller/:action$}
33
+ add_route('/:controller', options.merge({:action => 'index'}))
34
+ @controller_route_added = true
35
+ end
36
+
37
+ options[:action] = 'index' unless options[:action]
38
+
39
+ path[0, 0] = '/' unless path[0] == ?/
40
+ route = @usher.add_route(path, options)
41
+ raise "your route must include a controller" unless route.paths.first.dynamic_keys.include?(:controller) || route.destination.include?(:controller)
42
+ route
43
+ end
44
+
45
+ def recognize(request)
46
+ node = @usher.recognize(request)
47
+ params = node.params.inject({}){|h,(k,v)| h[k]=v; h }
48
+ request.path_parameters = (node.params.empty? ? node.path.route.destination : node.path.route.destination.merge(params)).with_indifferent_access
49
+ "#{request.path_parameters[:controller].camelize}Controller".constantize
50
+ rescue
51
+ raise ActionController::RoutingError, "No route matches #{request.path.inspect} with #{request.inspect}"
52
+ end
53
+
54
+ def add_named_route(name, route, options = {})
55
+ @usher.add_route(route, options).name(name)
56
+ end
57
+
58
+ def route_count
59
+ @usher.route_count
60
+ end
61
+
62
+ def empty?
63
+ @usher.route_count.zero?
64
+ end
65
+
66
+ def generate(options, recall = {}, method = :generate, route_name = nil)
67
+ route = if(route_name)
68
+ @usher.named_routes[route_name]
69
+ else
70
+ merged_options = options
71
+ merged_options[:controller] = recall[:controller] unless options.key?(:controller)
72
+ unless options.key?(:action)
73
+ options[:action] = ''
74
+ end
75
+ path_for_options(merged_options)
76
+ end
77
+ case method
78
+ when :generate
79
+ merged_options ||= recall.merge(options)
80
+ url = generate_url(route, merged_options)
81
+ url.slice!(-1) if url[-1] == ?/
82
+ url
83
+ else
84
+ raise "method #{method} not recognized"
85
+ end
86
+ end
87
+
88
+ def generate_url(route, params)
89
+ @url_generator.generate(route, params)
90
+ end
91
+
92
+ def path_for_options(options)
93
+ @usher.path_for_options(options)
94
+ end
95
+
96
+ def named_routes
97
+ @usher.named_routes
98
+ end
99
+
100
+ def reload
101
+ @usher.reset!
102
+ if @configuration_file
103
+ Kernel.load(@configuration_file)
104
+ else
105
+ @usher.add_route ":controller/:action/:id"
106
+ end
107
+ end
108
+
109
+ def load_routes!
110
+ reload
111
+ end
112
+
113
+ def draw
114
+ reset!
115
+ yield Mapper.new(self)
116
+ install_helpers
117
+ end
118
+
119
+ def install_helpers(destinations = [ActionController::Base, ActionView::Base], regenerate_code = false)
120
+ #*_url and hash_for_*_url
121
+ Array(destinations).each do |d| d.module_eval { include Helpers }
122
+ @usher.named_routes.keys.each do |name|
123
+ @module.module_eval <<-end_eval # We use module_eval to avoid leaks
124
+ def #{name}_url(options = {})
125
+ ActionController::Routing::UsherRoutes.generate(options, {}, :generate, :#{name})
126
+ end
127
+ end_eval
128
+ end
129
+ d.__send__(:include, @module)
130
+ end
131
+ end
132
+
133
+ end
134
+ end
135
+ end
@@ -0,0 +1,135 @@
1
+ class Usher
2
+ module Interface
3
+ class Rails2_3Interface
4
+
5
+ attr_reader :configuration_files
6
+
7
+ def named_routes
8
+ @router.named_routes
9
+ end
10
+
11
+ def add_named_route(name, route, options = {})
12
+ @router.add_route(route, options).name(name)
13
+ end
14
+
15
+ def add_route(path, options = {})
16
+ if !@controller_action_route_added && path =~ %r{^/?:controller/:action/:id$}
17
+ add_route('/:controller/:action', options.dup)
18
+ @controller_action_route_added = true
19
+ end
20
+
21
+ if !@controller_route_added && path =~ %r{^/?:controller/:action$}
22
+ add_route('/:controller', options.merge({:action => 'index'}))
23
+ @controller_route_added = true
24
+ end
25
+
26
+ options[:action] = 'index' unless options[:action]
27
+
28
+ path[0, 0] = '/' unless path[0] == ?/
29
+ route = @router.add_route(path, options).to(options)
30
+
31
+ raise "your route must include a controller" unless (route.paths.first.dynamic_keys.include?(:controller) || route.destination.include?(:controller))
32
+ route
33
+ end
34
+
35
+ def initialize
36
+ reset!
37
+ end
38
+
39
+ def add_configuration_file(file)
40
+ @configuration_files << file
41
+ end
42
+
43
+ def reload!
44
+ if configuration_files.any?
45
+ configuration_files.each { |config| load(config) }
46
+ else
47
+ add_route ":controller/:action/:id"
48
+ end
49
+
50
+ end
51
+ alias_method :reload, :reload!
52
+
53
+ def route_count
54
+ routes.size
55
+ end
56
+
57
+ def routes
58
+ @router.routes
59
+ end
60
+
61
+ def call(env)
62
+ request = ActionController::Request.new(env)
63
+ app = recognize(request)
64
+ app.call(env).to_a
65
+ end
66
+
67
+ def recognize(request)
68
+ response = @router.recognize(request)
69
+ request.path_parameters = (response.params.empty? ? response.path.route.destination : response.path.route.destination.merge(response.params.inject({}){|h,(k,v)| h[k]=v; h })).with_indifferent_access
70
+ response.params.each { |pair| request.path_parameters[pair.first] = pair.last }
71
+ "#{request.path_parameters[:controller].camelize}Controller".constantize
72
+ end
73
+
74
+ def reset!
75
+ @router = Usher.new
76
+ @url_generator = Usher::Generators::URL.new(@router)
77
+ @configuration_files = []
78
+ @module ||= Module.new
79
+ @controller_route_added = false
80
+ @controller_action_route_added = false
81
+ end
82
+
83
+ def draw
84
+ reset!
85
+ yield ActionController::Routing::RouteSet::Mapper.new(self)
86
+ install_helpers
87
+ end
88
+
89
+ def install_helpers(destinations = [ActionController::Base, ActionView::Base], regenerate_code = false)
90
+ #*_url and hash_for_*_url
91
+ Array(destinations).each do |d| d.module_eval { include Helpers }
92
+ @router.named_routes.keys.each do |name|
93
+ @module.module_eval <<-end_eval # We use module_eval to avoid leaks
94
+ def #{name}_url(options = {})
95
+ ActionController::Routing::UsherRoutes.generate(options, {}, :generate, :#{name})
96
+ end
97
+ end_eval
98
+ end
99
+ d.__send__(:include, @module)
100
+ end
101
+ end
102
+
103
+ def generate(options, recall = {}, method = :generate, route_name = nil)
104
+ route = if(route_name)
105
+ @router.named_routes[route_name]
106
+ else
107
+ merged_options = options
108
+ merged_options[:controller] = recall[:controller] unless options.key?(:controller)
109
+ unless options.key?(:action)
110
+ options[:action] = ''
111
+ end
112
+ path_for_options(merged_options)
113
+ end
114
+ case method
115
+ when :generate
116
+ merged_options ||= recall.merge(options)
117
+ url = generate_url(route, merged_options)
118
+ url.slice!(-1) if url[-1] == ?/
119
+ url
120
+ else
121
+ raise "method #{method} not recognized"
122
+ end
123
+ end
124
+
125
+ def generate_url(route, params)
126
+ @url_generator.generate(route, params)
127
+ end
128
+
129
+ def path_for_options(options)
130
+ @router.path_for_options(options)
131
+ end
132
+
133
+ end
134
+ end
135
+ end
@@ -0,0 +1,27 @@
1
+ class Usher
2
+ module Interface
3
+ autoload :Rails2_2Interface, File.join(File.dirname(__FILE__), 'interface', 'rails2_2_interface')
4
+ autoload :Rails2_3Interface, File.join(File.dirname(__FILE__), 'interface', 'rails2_3_interface')
5
+ autoload :MerbInterface, File.join(File.dirname(__FILE__), 'interface', 'merb_interface')
6
+ autoload :RackInterface, File.join(File.dirname(__FILE__), 'interface', 'rack_interface')
7
+ autoload :EmailInterface, File.join(File.dirname(__FILE__), 'interface', 'email_interface')
8
+
9
+ def self.for(type, &blk)
10
+ case type
11
+ when :rails2_2
12
+ Rails2_2Interface.new(&blk)
13
+ when :rails2_3
14
+ Rails2_3Interface.new(&blk)
15
+ when :merb
16
+ MerbInterface.new(&blk)
17
+ when :rack
18
+ RackInterface.new(&blk)
19
+ when :email
20
+ EmailInterface.new(&blk)
21
+ end
22
+
23
+ end
24
+
25
+
26
+ end
27
+ end
data/lib/usher/node.rb ADDED
@@ -0,0 +1,138 @@
1
+ require 'fuzzy_hash'
2
+
3
+ class Usher
4
+
5
+ class Node
6
+
7
+ Response = Struct.new(:path, :params)
8
+
9
+ attr_reader :lookup
10
+ attr_accessor :terminates, :exclusive_type, :parent, :value, :request_methods, :globs_capture_separators
11
+
12
+ def initialize(parent, value)
13
+ @parent = parent
14
+ @value = value
15
+ @lookup = Hash.new
16
+ @exclusive_type = nil
17
+ end
18
+
19
+ def upgrade_lookup
20
+ @lookup = FuzzyHash.new(@lookup)
21
+ end
22
+
23
+ def depth
24
+ @depth ||= @parent && @parent.is_a?(Node) ? @parent.depth + 1 : 0
25
+ end
26
+
27
+ def self.root(route_set, request_methods, globs_capture_separators)
28
+ root = self.new(route_set, nil)
29
+ root.request_methods = request_methods
30
+ root.globs_capture_separators = globs_capture_separators
31
+ root
32
+ end
33
+
34
+ def terminates?
35
+ @terminates
36
+ end
37
+
38
+ def pp
39
+ $stdout << " " * depth
40
+ $stdout << "#{depth}: #{value.inspect} #{!!terminates?}\n"
41
+ @lookup.each do |k,v|
42
+ $stdout << " " * (depth + 1)
43
+ $stdout << "#{k} ==> \n"
44
+ v.pp
45
+ end
46
+ end
47
+
48
+ def add(route)
49
+ route.paths.each do |path|
50
+ parts = path.parts.dup
51
+ request_methods.each do |type|
52
+ parts.push(Route::RequestMethod.new(type, route.conditions[type])) if route.conditions && route.conditions.key?(type)
53
+ end
54
+
55
+ current_node = self
56
+ until parts.size.zero?
57
+ key = parts.shift
58
+ target_node = case key
59
+ when Route::RequestMethod
60
+ current_node.upgrade_lookup if key.value.is_a?(Regexp)
61
+ if current_node.exclusive_type == key.type
62
+ current_node.lookup[key.value] ||= Node.new(current_node, key)
63
+ elsif current_node.lookup.empty?
64
+ current_node.exclusive_type = key.type
65
+ current_node.lookup[key.value] ||= Node.new(current_node, key)
66
+ else
67
+ parts.unshift(key)
68
+ current_node.lookup[nil] ||= Node.new(current_node, Route::RequestMethod.new(current_node.exclusive_type, nil))
69
+ end
70
+ else
71
+ key.globs_capture_separators = globs_capture_separators if key.is_a?(Route::Variable)
72
+
73
+ if !key.is_a?(Route::Variable)
74
+ current_node.upgrade_lookup if key.is_a?(Regexp)
75
+ current_node.lookup[key] ||= Node.new(current_node, key)
76
+ elsif key.regex_matcher
77
+ current_node.upgrade_lookup
78
+ current_node.lookup[key.regex_matcher] ||= Node.new(current_node, key)
79
+ else
80
+ current_node.lookup[nil] ||= Node.new(current_node, key)
81
+ end
82
+ end
83
+ current_node = target_node
84
+ end
85
+ current_node.terminates = path
86
+ end
87
+ route
88
+ end
89
+
90
+ def find(usher, request, path, params = [])
91
+ if exclusive_type
92
+ [lookup[request.send(exclusive_type)], lookup[nil]].each do |n|
93
+ if n && (ret = n.find(usher, request, path.dup, params.dup))
94
+ return ret
95
+ end
96
+ end
97
+ elsif path.size.zero? && terminates?
98
+ Response.new(terminates, params)
99
+ elsif !path.size.zero? && (next_part = lookup[part = path.shift] || lookup[nil])
100
+ case next_part.value
101
+ when Route::Variable
102
+ case next_part.value.type
103
+ when :*
104
+ params << [next_part.value.name, []] unless params.last && params.last.first == next_part.value.name
105
+ loop do
106
+ if (next_part.value.look_ahead === part || (!usher.splitter.delimiter_chars.include?(part[0]) && next_part.value.regex_matcher && !next_part.value.regex_matcher.match(part)))
107
+ path.unshift(part)
108
+ path.unshift(next_part.parent.value) if usher.splitter.delimiter_chars.include?(next_part.parent.value[0])
109
+ break
110
+ elsif next_part.value.globs_capture_separators
111
+ params.last.last << part
112
+ elsif !usher.splitter.delimiter_chars.include?(part[0])
113
+ next_part.value.valid!(part)
114
+ params.last.last << part
115
+ end
116
+ if path.size.zero?
117
+ break
118
+ else
119
+ part = path.shift
120
+ end
121
+ end
122
+ when :':'
123
+ var = next_part.value
124
+ var.valid!(part)
125
+ params << [var.name, part]
126
+ until (var.look_ahead === path.first) || path.empty?
127
+ params.last.last << path.shift
128
+ end
129
+ end
130
+ end
131
+ next_part.find(usher, request, path, params)
132
+ else
133
+ nil
134
+ end
135
+ end
136
+
137
+ end
138
+ end
@@ -0,0 +1,24 @@
1
+ class Usher
2
+ class Route
3
+ class Path
4
+
5
+ attr_reader :dynamic_parts, :dynamic_map, :dynamic_indicies, :route, :parts, :dynamic_required_keys, :dynamic_keys
6
+
7
+ def initialize(route, parts)
8
+ @route = route
9
+ @parts = parts
10
+ @dynamic_indicies = []
11
+ @parts.each_index{|i| @dynamic_indicies << i if @parts[i].is_a?(Variable)}
12
+ @dynamic_parts = @parts.values_at(*@dynamic_indicies)
13
+ @dynamic_map = {}
14
+ @dynamic_parts.each{|p| @dynamic_map[p.name] = p }
15
+ @dynamic_keys = @dynamic_map.keys
16
+ @dynamic_required_keys = @dynamic_parts.select{|dp| !dp.default_value}.map{|dp| dp.name}
17
+ end
18
+
19
+ def can_generate_from?(keys)
20
+ (@dynamic_required_keys - keys).size.zero?
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,22 @@
1
+ class Usher
2
+ class Route
3
+ class RequestMethod
4
+
5
+ attr_reader :type, :value
6
+
7
+ def initialize(type, value)
8
+ @type = type
9
+ @value = value
10
+ end
11
+
12
+ def hash
13
+ type.hash + value.hash
14
+ end
15
+
16
+ def eql?(o)
17
+ o.is_a?(self.class) && o.type == type && o.value == value
18
+ end
19
+ alias == eql?
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,37 @@
1
+ class Usher
2
+ class Route
3
+ class Variable
4
+ attr_reader :type, :name, :validator, :regex_matcher
5
+ attr_accessor :look_ahead, :globs_capture_separators, :default_value
6
+
7
+ def initialize(type, name, validator = nil, regex_matcher = nil, globs_capture_separators = false)
8
+ @type = type
9
+ @name = :"#{name}"
10
+ @validator = validator
11
+ @regex_matcher = regex_matcher
12
+ @globs_capture_separators = globs_capture_separators
13
+ end
14
+
15
+ def to_s
16
+ "#{type}#{name}"
17
+ end
18
+
19
+ def valid!(val)
20
+ case @validator
21
+ when Proc
22
+ begin
23
+ @validator.call(val)
24
+ rescue Exception => e
25
+ raise ValidationException.new("#{val} does not conform to #{@validator}, root cause #{e.inspect}")
26
+ end
27
+ else
28
+ @validator === val or raise(ValidationException.new("#{val} does not conform to #{@validator}, root cause #{e.inspect}"))
29
+ end if @validator
30
+ end
31
+
32
+ def ==(o)
33
+ o && (o.type == @type && o.name == @name && o.validator == @validator)
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,58 @@
1
+ require File.join(File.dirname(__FILE__), 'route', 'path')
2
+ require File.join(File.dirname(__FILE__), 'route', 'variable')
3
+ require File.join(File.dirname(__FILE__), 'route', 'request_method')
4
+
5
+ class Usher
6
+ class Route
7
+ attr_reader :paths, :original_path, :requirements, :conditions, :destination, :named, :generate_with
8
+
9
+ GenerateWith = Struct.new(:scheme, :port, :host)
10
+
11
+ def initialize(original_path, router, conditions, requirements, default_values, generate_with) # :nodoc:
12
+ @original_path = original_path
13
+ @router = router
14
+ @requirements = requirements
15
+ @conditions = conditions
16
+ @default_values = default_values
17
+ @paths = @router.splitter.split(@original_path, @requirements, @default_values).collect {|path| Path.new(self, path)}
18
+ @generate_with = GenerateWith.new(generate_with[:scheme], generate_with[:port], generate_with[:host]) if generate_with
19
+ end
20
+
21
+ def grapher
22
+ unless @grapher
23
+ @grapher = Grapher.new
24
+ @grapher.add_route(self)
25
+ end
26
+ @grapher
27
+ end
28
+
29
+ def find_matching_path(params)
30
+ @paths.size == 1 ? @paths.first : grapher.find_matching_path(params)
31
+ end
32
+
33
+
34
+ # Sets +options+ on a route. Returns +self+.
35
+ #
36
+ # Request = Struct.new(:path)
37
+ # set = Usher.new
38
+ # route = set.add_route('/test')
39
+ # route.to(:controller => 'testing', :action => 'index')
40
+ # set.recognize(Request.new('/test')).first.params => {:controller => 'testing', :action => 'index'}
41
+ def to(options = nil, &block)
42
+ @destination = (block_given? ? block : options)
43
+ self
44
+ end
45
+
46
+ # Sets route as referenceable from +name+. Returns +self+.
47
+ #
48
+ # set = Usher.new
49
+ # route = set.add_route('/test').name(:route)
50
+ # set.generate_url(:route) => '/test'
51
+ def name(name)
52
+ @named = name
53
+ @router.name(name, self)
54
+ self
55
+ end
56
+
57
+ end
58
+ end