usher 0.4.8

Sign up to get free protection for your applications and to get access to all the features.
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