hanami-router 1.3.2 → 2.0.0.alpha5

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 (46) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +43 -0
  3. data/LICENSE.md +1 -1
  4. data/README.md +98 -444
  5. data/hanami-router.gemspec +23 -19
  6. data/lib/hanami/middleware/body_parser.rb +21 -15
  7. data/lib/hanami/middleware/body_parser/class_interface.rb +62 -56
  8. data/lib/hanami/middleware/body_parser/errors.rb +7 -4
  9. data/lib/hanami/middleware/body_parser/json_parser.rb +9 -7
  10. data/lib/hanami/middleware/body_parser/parser.rb +58 -0
  11. data/lib/hanami/middleware/error.rb +16 -0
  12. data/lib/hanami/router.rb +608 -955
  13. data/lib/hanami/router/block.rb +88 -0
  14. data/lib/hanami/router/error.rb +67 -0
  15. data/lib/hanami/router/inspector.rb +38 -0
  16. data/lib/hanami/router/node.rb +91 -0
  17. data/lib/hanami/router/params.rb +35 -0
  18. data/lib/hanami/router/prefix.rb +67 -0
  19. data/lib/hanami/router/recognized_route.rb +92 -0
  20. data/lib/hanami/router/redirect.rb +33 -0
  21. data/lib/hanami/router/route.rb +130 -0
  22. data/lib/hanami/router/segment.rb +19 -0
  23. data/lib/hanami/router/trie.rb +63 -0
  24. data/lib/hanami/router/url_helpers.rb +40 -0
  25. data/lib/hanami/router/version.rb +4 -1
  26. metadata +61 -39
  27. data/lib/hanami-router.rb +0 -1
  28. data/lib/hanami/routing/endpoint.rb +0 -195
  29. data/lib/hanami/routing/endpoint_resolver.rb +0 -238
  30. data/lib/hanami/routing/error.rb +0 -7
  31. data/lib/hanami/routing/force_ssl.rb +0 -212
  32. data/lib/hanami/routing/http_router.rb +0 -220
  33. data/lib/hanami/routing/http_router_monkey_patch.rb +0 -38
  34. data/lib/hanami/routing/namespace.rb +0 -98
  35. data/lib/hanami/routing/parsers.rb +0 -113
  36. data/lib/hanami/routing/parsing/json_parser.rb +0 -33
  37. data/lib/hanami/routing/parsing/parser.rb +0 -61
  38. data/lib/hanami/routing/recognized_route.rb +0 -219
  39. data/lib/hanami/routing/resource.rb +0 -119
  40. data/lib/hanami/routing/resource/action.rb +0 -402
  41. data/lib/hanami/routing/resource/nested.rb +0 -41
  42. data/lib/hanami/routing/resource/options.rb +0 -74
  43. data/lib/hanami/routing/resources.rb +0 -48
  44. data/lib/hanami/routing/resources/action.rb +0 -156
  45. data/lib/hanami/routing/route.rb +0 -71
  46. data/lib/hanami/routing/routes_inspector.rb +0 -221
data/lib/hanami-router.rb DELETED
@@ -1 +0,0 @@
1
- require 'hanami/router'
@@ -1,195 +0,0 @@
1
- require 'delegate'
2
- require 'hanami/routing/error'
3
- require 'hanami/utils/class'
4
-
5
- module Hanami
6
- module Routing
7
- # Endpoint not found
8
- # This is raised when the router fails to load an endpoint at the runtime.
9
- #
10
- # @since 0.1.0
11
- class EndpointNotFound < Hanami::Routing::Error
12
- end
13
-
14
- # Routing endpoint
15
- # This is the object that responds to an HTTP request made against a certain
16
- # path.
17
- #
18
- # The router will use this class for:
19
- #
20
- # * Procs and any Rack compatible object (respond to #call)
21
- #
22
- # @since 0.1.0
23
- #
24
- # @api private
25
- #
26
- # @example
27
- # require 'hanami/router'
28
- #
29
- # Hanami::Router.new do
30
- # get '/proc', to: ->(env) { [200, {}, ['This will use Hanami::Routing::Endpoint']] }
31
- # get '/rack-app', to: RackApp.new
32
- # end
33
- class Endpoint < SimpleDelegator
34
- # @since 0.2.0
35
- # @api private
36
- def inspect
37
- case __getobj__
38
- when Proc
39
- source, line = __getobj__.source_location
40
- lambda_inspector = " (lambda)" if __getobj__.lambda?
41
-
42
- "#<Proc@#{ ::File.expand_path(source) }:#{ line }#{ lambda_inspector }>"
43
- when Class
44
- __getobj__
45
- else
46
- "#<#{ __getobj__.class }>"
47
- end
48
- end
49
-
50
- # @since 1.0.0
51
- # @api private
52
- def routable?
53
- !__getobj__.nil?
54
- rescue ArgumentError
55
- end
56
-
57
- # @since 1.0.1
58
- # @api private
59
- def redirect?
60
- false
61
- end
62
-
63
- # @since 1.0.1
64
- # @api private
65
- def destination_path
66
- end
67
- end
68
-
69
- # Routing endpoint
70
- # This is the object that responds to an HTTP request made against a certain
71
- # path.
72
- #
73
- # The router will use this class for:
74
- #
75
- # * Classes
76
- # * Hanami::Action endpoints referenced as a class
77
- # * Hanami::Action endpoints referenced a string
78
- # * RESTful resource(s)
79
- #
80
- # @since 0.1.0
81
- #
82
- # @api private
83
- #
84
- # @example
85
- # require 'hanami/router'
86
- #
87
- # Hanami::Router.new do
88
- # get '/class', to: RackMiddleware
89
- # get '/hanami-action-class', to: Dashboard::Index
90
- # get '/hanami-action-string', to: 'dashboard#index'
91
- #
92
- # resource 'identity'
93
- # resources 'articles'
94
- # end
95
- class ClassEndpoint < Endpoint
96
- # Rack interface
97
- #
98
- # @since 0.1.0
99
- # @api private
100
- def call(env)
101
- __getobj__.new.call(env)
102
- end
103
- end
104
-
105
- # Routing endpoint
106
- # This is the object that responds to an HTTP request made against a certain
107
- # path.
108
- #
109
- # The router will use this class for the same use cases of `ClassEndpoint`,
110
- # but when the target class can't be found, instead of raise a `LoadError`
111
- # we reference in a lazy endpoint.
112
- #
113
- # For each incoming HTTP request, it will look for the referenced class,
114
- # then it will instantiate and invoke #call on the object.
115
- #
116
- # This behavior is required to solve a chicken-egg situation when we try
117
- # to load the router first and then the application with all its endpoints.
118
- #
119
- # @since 0.1.0
120
- #
121
- # @api private
122
- #
123
- # @see Hanami::Routing::ClassEndpoint
124
- class LazyEndpoint < Endpoint
125
- # Initialize the lazy endpoint
126
- #
127
- # @since 0.1.0
128
- # @api private
129
- def initialize(name, namespace)
130
- @name, @namespace = name, namespace
131
- end
132
-
133
- # Rack interface
134
- #
135
- # @raise [EndpointNotFound] when the endpoint can't be found.
136
- #
137
- # @since 0.1.0
138
- # @api private
139
- def call(env)
140
- obj.call(env)
141
- end
142
-
143
- # @since 0.2.0
144
- # @api private
145
- def inspect
146
- # TODO review this implementation once the namespace feature will be
147
- # cleaned up.
148
- result = klass rescue nil
149
-
150
- if result.nil?
151
- result = @name
152
- result = "#{ @namespace }::#{ result }" if @namespace != Object
153
- end
154
-
155
- result
156
- end
157
-
158
- private
159
- # @since 0.1.0
160
- # @api private
161
- def obj
162
- klass.new
163
- end
164
-
165
- # @since 0.2.0
166
- # @api private
167
- def klass
168
- Utils::Class.load!(@name, @namespace)
169
- rescue NameError => e
170
- raise EndpointNotFound.new(e.message)
171
- end
172
- end
173
-
174
- # @since 1.0.1
175
- # @api private
176
- class RedirectEndpoint < Endpoint
177
- # @since 1.0.1
178
- # @api private
179
- attr_reader :destination_path
180
-
181
- # @since 1.0.1
182
- # @api private
183
- def initialize(destination_path, destination)
184
- @destination_path = destination_path
185
- super(destination)
186
- end
187
-
188
- # @since 1.0.1
189
- # @api private
190
- def redirect?
191
- true
192
- end
193
- end
194
- end
195
- end
@@ -1,238 +0,0 @@
1
- require 'hanami/utils/string'
2
- require 'hanami/utils/class'
3
- require 'hanami/routing/endpoint'
4
-
5
- module Hanami
6
- module Routing
7
- # Resolve duck-typed endpoints
8
- #
9
- # @since 0.1.0
10
- #
11
- # @api private
12
- class EndpointResolver
13
- # @since 0.2.0
14
- # @api private
15
- NAMING_PATTERN = '%{controller}::%{action}'.freeze
16
-
17
- # @since 0.7.0
18
- # @api private
19
- DEFAULT_RESPONSE = [404, {'X-Cascade' => 'pass'}, 'Not Found'].freeze
20
-
21
- # Default separator for controller and action.
22
- # A different separator can be passed to #initialize with the `:separator` option.
23
- #
24
- # @see #initialize
25
- # @see #resolve
26
- #
27
- # @since 0.1.0
28
- #
29
- # @example
30
- # require 'hanami/router'
31
- #
32
- # router = Hanami::Router.new do
33
- # get '/', to: 'articles#show'
34
- # end
35
- ACTION_SEPARATOR = '#'.freeze
36
-
37
- attr_reader :action_separator
38
-
39
- # Initialize an endpoint resolver
40
- #
41
- # @param options [Hash] the options used to customize lookup behavior
42
- #
43
- # @option options [Class] :endpoint the endpoint class that is returned
44
- # by `#resolve`. (defaults to `Hanami::Routing::Endpoint`)
45
- #
46
- # @option options [Class,Module] :namespace the Ruby namespace where to
47
- # lookup for controllers and actions. (defaults to `Object`)
48
- #
49
- # @option options [String] :pattern the string to interpolate in order
50
- # to return an action name. This string SHOULD contain
51
- # <tt>'%{controller}'</tt> and <tt>'%{action}'</tt>, all the other keys
52
- # will be ignored.
53
- # See the examples below.
54
- #
55
- # @option options [String] :action_separator the separator between controller and
56
- # action name. (defaults to `ACTION_SEPARATOR`)
57
- #
58
- # @return [Hanami::Routing::EndpointResolver] self
59
- #
60
- # @since 0.1.0
61
- # @api private
62
- #
63
- # @example Specify custom endpoint class
64
- # require 'hanami/router'
65
- #
66
- # resolver = Hanami::Routing::EndpointResolver.new(endpoint: CustomEndpoint)
67
- # router = Hanami::Router.new(resolver: resolver)
68
- #
69
- # router.get('/', to: endpoint).dest # => #<CustomEndpoint:0x007f97f3359570 ...>
70
- #
71
- # @example Specify custom Ruby namespace
72
- # require 'hanami/router'
73
- #
74
- # resolver = Hanami::Routing::EndpointResolver.new(namespace: MyApp)
75
- # router = Hanami::Router.new(resolver: resolver)
76
- #
77
- # router.get('/', to: 'articles#show')
78
- # # => Will look for: MyApp::Articles::Show
79
- #
80
- #
81
- #
82
- # @example Specify custom pattern
83
- # require 'hanami/router'
84
- #
85
- # resolver = Hanami::Routing::EndpointResolver.new(pattern: '%{controller}Controller::%{action}')
86
- # router = Hanami::Router.new(resolver: resolver)
87
- #
88
- # router.get('/', to: 'articles#show')
89
- # # => Will look for: ArticlesController::Show
90
- #
91
- #
92
- #
93
- # @example Specify custom controller-action separator
94
- # require 'hanami/router'
95
- #
96
- # resolver = Hanami::Routing::EndpointResolver.new(separator: '@')
97
- # router = Hanami::Router.new(resolver: resolver)
98
- #
99
- # router.get('/', to: 'articles@show')
100
- # # => Will look for: Articles::Show
101
- def initialize(options = {})
102
- @endpoint_class = options[:endpoint] || Endpoint
103
- @namespace = options[:namespace] || Object
104
- @action_separator = options[:action_separator] || ACTION_SEPARATOR
105
- @pattern = options[:pattern] || NAMING_PATTERN
106
- end
107
-
108
- # Resolve the given set of HTTP verb, path, endpoint and options.
109
- # If it fails to resolve, it will mount the default endpoint to the given
110
- # path, which returns an 404 (Not Found).
111
- #
112
- # @param options [Hash] the options required to resolve the endpoint
113
- #
114
- # @option options [String,Proc,Class,Object#call] :to the endpoint
115
- # @option options [String] :namespace an optional routing namespace
116
- #
117
- # @return [Endpoint] this may vary according to the :endpoint option
118
- # passed to #initialize
119
- #
120
- # @since 0.1.0
121
- # @api private
122
- #
123
- # @see #initialize
124
- # @see #find
125
- #
126
- # @example Resolve to a Proc
127
- # require 'hanami/router'
128
- #
129
- # router = Hanami::Router.new
130
- # router.get '/', to: ->(env) { [200, {}, ['Hi!']] }
131
- #
132
- # @example Resolve to a class
133
- # require 'hanami/router'
134
- #
135
- # router = Hanami::Router.new
136
- # router.get '/', to: RackMiddleware
137
- #
138
- # @example Resolve to a Rack compatible object (respond to #call)
139
- # require 'hanami/router'
140
- #
141
- # router = Hanami::Router.new
142
- # router.get '/', to: AnotherMiddleware.new
143
- #
144
- # @example Resolve to a Hanami::Action from a string (see Hanami::Controller framework)
145
- # require 'hanami/router'
146
- #
147
- # router = Hanami::Router.new
148
- # router.get '/', to: 'articles#show'
149
- #
150
- # @example Resolve to a Hanami::Action (see Hanami::Controller framework)
151
- # require 'hanami/router'
152
- #
153
- # router = Hanami::Router.new
154
- # router.get '/', to: Articles::Show
155
- #
156
- # @example Resolve a redirect with a namespace
157
- # require 'hanami/router'
158
- #
159
- # router = Hanami::Router.new
160
- # router.namespace 'users' do
161
- # get '/home', to: ->(env) { ... }
162
- # redirect '/dashboard', to: '/home'
163
- # end
164
- #
165
- # # GET /users/dashboard => 301 Location: "/users/home"
166
- def resolve(options, &endpoint)
167
- result = endpoint || find(options)
168
- resolve_callable(result) || resolve_matchable(result) || default
169
- end
170
-
171
- # Finds a path from the given options.
172
- #
173
- # @param options [Hash] the path description
174
- # @option options [String,Proc,Class,Object#call] :to the endpoint
175
- # @option options [String] :namespace an optional namespace
176
- #
177
- # @since 0.1.0
178
- # @api private
179
- #
180
- # @return [Object]
181
- def find(options)
182
- options[:to]
183
- end
184
-
185
- protected
186
- # @api private
187
- def default
188
- @endpoint_class.new(
189
- ->(env) { DEFAULT_RESPONSE }
190
- )
191
- end
192
-
193
- # @api private
194
- def constantize(string)
195
- klass = Utils::Class.load!(string, @namespace)
196
- if klass.respond_to?(:call)
197
- Endpoint.new(klass)
198
- else
199
- ClassEndpoint.new(klass)
200
- end
201
- rescue NameError
202
- LazyEndpoint.new(string, @namespace)
203
- end
204
-
205
- # @api private
206
- def classify(string)
207
- Utils::String.transform(string, :underscore, :classify)
208
- end
209
-
210
- private
211
- # @api private
212
- def resolve_callable(callable)
213
- if callable.respond_to?(:call)
214
- @endpoint_class.new(callable)
215
- elsif callable.is_a?(Class) && callable.instance_methods.include?(:call)
216
- @endpoint_class.new(callable.new)
217
- end
218
- end
219
-
220
- # @api private
221
- def resolve_matchable(matchable)
222
- if matchable.respond_to?(:match)
223
- constantize(
224
- resolve_action(matchable) || classify(matchable)
225
- )
226
- end
227
- end
228
-
229
- # @api private
230
- def resolve_action(string)
231
- if string.match(action_separator)
232
- controller, action = string.split(action_separator).map {|token| classify(token) }
233
- @pattern % {controller: controller, action: action}
234
- end
235
- end
236
- end
237
- end
238
- end
@@ -1,7 +0,0 @@
1
- module Hanami
2
- module Routing
3
- # @since 0.5.0
4
- class Error < ::StandardError
5
- end
6
- end
7
- end
@@ -1,212 +0,0 @@
1
- require 'rack/request'
2
- require 'hanami/utils/deprecation'
3
-
4
- module Hanami
5
- module Routing
6
- # Force ssl
7
- #
8
- # Redirect response to the secure equivalent resource (https)
9
- #
10
- # @since 0.4.1
11
- # @api private
12
- class ForceSsl
13
- # Https scheme
14
- #
15
- # @since 0.4.1
16
- # @api private
17
- SSL_SCHEME = 'https'.freeze
18
-
19
- # @since 0.4.1
20
- # @api private
21
- HTTPS = 'HTTPS'.freeze
22
-
23
- # @since 0.4.1
24
- # @api private
25
- ON = 'on'.freeze
26
-
27
- # Location header
28
- #
29
- # @since 0.4.1
30
- # @api private
31
- LOCATION_HEADER = 'Location'.freeze
32
-
33
- # Default http port
34
- #
35
- # @since 0.4.1
36
- # @api private
37
- DEFAULT_HTTP_PORT = 80
38
-
39
- # Default ssl port
40
- #
41
- # @since 0.4.1
42
- # @api private
43
- DEFAULT_SSL_PORT = 443
44
-
45
- # Moved Permanently http code
46
- #
47
- # @since 0.4.1
48
- # @api private
49
- MOVED_PERMANENTLY_HTTP_CODE = 301
50
-
51
- # Temporary Redirect http code
52
- #
53
- # @since 0.4.1
54
- # @api private
55
- TEMPORARY_REDIRECT_HTTP_CODE = 307
56
-
57
- # @since 0.4.1
58
- # @api private
59
- HTTP_X_FORWARDED_SSL = 'HTTP_X_FORWARDED_SSL'.freeze
60
-
61
- # @since 0.4.1
62
- # @api private
63
- HTTP_X_FORWARDED_SCHEME = 'HTTP_X_FORWARDED_SCHEME'.freeze
64
-
65
- # @since 0.4.1
66
- # @api private
67
- HTTP_X_FORWARDED_PROTO = 'HTTP_X_FORWARDED_PROTO'.freeze
68
-
69
- # @since 0.4.1
70
- # @api private
71
- HTTP_X_FORWARDED_PROTO_SEPARATOR = ','.freeze
72
-
73
- # @since 0.4.1
74
- # @api private
75
- RACK_URL_SCHEME = 'rack.url_scheme'.freeze
76
-
77
- # @since 0.4.1
78
- # @api private
79
- REQUEST_METHOD = 'REQUEST_METHOD'.freeze
80
-
81
- # @since 0.4.1
82
- # @api private
83
- IDEMPOTENT_METHODS = ['GET', 'HEAD'].freeze
84
-
85
- EMPTY_BODY = [].freeze
86
-
87
- # Initialize ForceSsl.
88
- #
89
- # @param active [Boolean] activate redirection to SSL
90
- # @param options [Hash] set of options
91
- # @option options [String] :host
92
- # @option options [Integer] :port
93
- #
94
- # @since 0.4.1
95
- # @api private
96
- def initialize(active, options = {})
97
- @active = active
98
- @host = options[:host]
99
- @port = options[:port]
100
-
101
- _redefine_call
102
- end
103
-
104
- # Set 301 status and Location header if this feature is activated.
105
- #
106
- # @param env [Hash] a Rack env instance
107
- #
108
- # @return [Array]
109
- #
110
- # @see Hanami::Routing::HttpRouter#call
111
- #
112
- # @since 0.4.1
113
- # @api private
114
- def call(env)
115
- end
116
-
117
- # Check if router has to force the response with ssl
118
- #
119
- # @return [Boolean]
120
- #
121
- # @since 0.4.1
122
- # @api private
123
- def force?(env)
124
- !ssl?(env)
125
- end
126
-
127
- private
128
-
129
- # @since 0.4.1
130
- # @api private
131
- attr_reader :host
132
-
133
- # Return full url to redirect
134
- #
135
- # @param env [Hash] Rack env
136
- #
137
- # @return [String]
138
- #
139
- # @since 0.4.1
140
- # @api private
141
- def full_url(env)
142
- "#{ SSL_SCHEME }://#{ host }:#{ port }#{ ::Rack::Request.new(env).fullpath }"
143
- end
144
-
145
- # Return redirect code
146
- #
147
- # @param env [Hash] Rack env
148
- #
149
- # @return [Integer]
150
- #
151
- # @since 0.4.1
152
- # @api private
153
- def redirect_code(env)
154
- if IDEMPOTENT_METHODS.include?(env[REQUEST_METHOD])
155
- MOVED_PERMANENTLY_HTTP_CODE
156
- else
157
- TEMPORARY_REDIRECT_HTTP_CODE
158
- end
159
- end
160
-
161
- # Return correct default port for full url
162
- #
163
- # @return [Integer]
164
- #
165
- # @since 0.4.1
166
- # @api private
167
- def port
168
- if @port == DEFAULT_HTTP_PORT
169
- DEFAULT_SSL_PORT
170
- else
171
- @port
172
- end
173
- end
174
-
175
- # @since 0.4.1
176
- # @api private
177
- def _redefine_call
178
- return unless @active
179
-
180
- Hanami::Utils::Deprecation.new('force_ssl option is deprecated, please delegate this behaviour to Nginx/Apache or use a Rack middleware like `rack-ssl`')
181
-
182
- define_singleton_method :call do |env|
183
- [redirect_code(env), { LOCATION_HEADER => full_url(env) }, EMPTY_BODY] if force?(env)
184
- end
185
- end
186
-
187
- # Adapted from Rack::Request#scheme
188
- #
189
- # @since 0.4.1
190
- # @api private
191
- def scheme(env)
192
- if env[HTTPS] == ON
193
- SSL_SCHEME
194
- elsif env[HTTP_X_FORWARDED_SSL] == ON
195
- SSL_SCHEME
196
- elsif env[HTTP_X_FORWARDED_SCHEME]
197
- env[HTTP_X_FORWARDED_SCHEME]
198
- elsif env[HTTP_X_FORWARDED_PROTO]
199
- env[HTTP_X_FORWARDED_PROTO].split(HTTP_X_FORWARDED_PROTO_SEPARATOR)[0]
200
- else
201
- env[RACK_URL_SCHEME]
202
- end
203
- end
204
-
205
- # @since 0.4.1
206
- # @api private
207
- def ssl?(env)
208
- scheme(env) == SSL_SCHEME
209
- end
210
- end
211
- end
212
- end