hanami-router 2.0.0.alpha1 → 2.0.0.alpha2

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,213 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "delegate"
4
- require "hanami/utils/class"
5
- require "hanami/utils/string"
6
-
7
- module Hanami
8
- module Routing
9
- # Routes endpoint
10
- #
11
- # @since 2.0.0
12
- # @api private
13
- module Endpoint
14
- # @since 2.0.0
15
- # @api private
16
- #
17
- # FIXME: Shall this be the default of Utils::Class.load! ?
18
- DEFAULT_NAMESPACE = Object
19
-
20
- # Controller / action separator for Hanami
21
- #
22
- # @since 2.0.0
23
- # @api private
24
- #
25
- # @example
26
- # require "hanami/router"
27
- #
28
- # Hanami::Router.new do
29
- # get "/home", to: "home#index"
30
- # end
31
- ACTION_SEPARATOR = "#"
32
-
33
- # Replacement to load an action from the string name.
34
- #
35
- # Please note that the `"/"` value is required by `Hanami::Utils::String#classify`.
36
- #
37
- # Given the `"home#index"` string, with the `Web::Controllers` namespace,
38
- # it will try to load `Web::Controllers::Home::Index` action.
39
- #
40
- # @since 2.0.0
41
- # @api private
42
- ACTION_SEPARATOR_REPLACEMENT = "/"
43
-
44
- # Find an endpoint for the given name
45
- #
46
- # @param name [String,Class,Proc,Object] the endpoint expressed as name
47
- # (`String`), as a Rack class application (`Class`), as a Rack
48
- # compatible proc (`Proc`), or as any other Rack compatible object
49
- # (`Object`)
50
- # @param namespace [Module] the Ruby module where to lookup the endpoint
51
- # @param configuration [Hanami::Controller::Configuration] the action
52
- # configuration
53
- #
54
- # @raise [Hanami::Routing::NotCallableEndpointError] if the found object
55
- # doesn't implement Rack protocol (`#call`)
56
- #
57
- # @return [Object, Hanami::Routing::LazyEndpoint] a Rack compatible
58
- # endpoint
59
- #
60
- # @since 2.0.0
61
- # @api private
62
- def self.find(name, namespace, configuration = nil)
63
- endpoint = case name
64
- when String
65
- find_string(name, namespace || DEFAULT_NAMESPACE, configuration)
66
- when Class
67
- name.respond_to?(:call) ? name : name.new
68
- else
69
- name
70
- end
71
-
72
- raise NotCallableEndpointError.new(endpoint) unless endpoint.respond_to?(:call)
73
-
74
- endpoint
75
- end
76
-
77
- # @since 1.0.1
78
- # @api private
79
- def redirect?
80
- false
81
- end
82
-
83
- # @since 1.0.1
84
- # @api private
85
- def destination_path
86
- end
87
-
88
- # Find an endpoint from its name
89
- #
90
- # @param name [String] the endpoint name
91
- # @param namespace [Module] the Ruby module where to lookup the endpoint
92
- # @param configuration [Hanami::Controller::Configuration] the action
93
- # configuration
94
- #
95
- # @return [Object, Hanami::Routing::LazyEndpoint] a Rack compatible
96
- # endpoint
97
- #
98
- # @since 2.0.0
99
- # @api private
100
- #
101
- # @example Basic Usage
102
- # Hanami::Routing::Endpoint.find("MyMiddleware")
103
- # # => #<MyMiddleware:0x007ff6df06f468>
104
- #
105
- # @example Hanami Action
106
- # Hanami::Routing::Endpoint.find("home#index", Web::Controllers)
107
- # # => #<Web::Controllers::Home::Index:0x007ff6df06f468>
108
- def self.find_string(name, namespace, configuration)
109
- n = Utils::String.new(name.sub(ACTION_SEPARATOR, ACTION_SEPARATOR_REPLACEMENT)).classify.to_s
110
- klass = Utils::Class.load!(n, namespace)
111
-
112
- if hanami_action?(name, n)
113
- klass.new(configuration: configuration)
114
- else
115
- klass.new
116
- end
117
- rescue NameError
118
- Hanami::Routing::LazyEndpoint.new(n, namespace)
119
- end
120
-
121
- private_class_method :find_string
122
-
123
- def self.hanami_action?(name, endpoint)
124
- name != endpoint
125
- end
126
-
127
- private_class_method :hanami_action?
128
- end
129
-
130
- # Routing endpoint
131
- # This is the object that responds to an HTTP request made against a certain
132
- # path.
133
- #
134
- # The router will use this class for the same use cases of `ClassEndpoint`,
135
- # but when the target class can't be found, instead of raise a `LoadError`
136
- # we reference in a lazy endpoint.
137
- #
138
- # For each incoming HTTP request, it will look for the referenced class,
139
- # then it will instantiate and invoke #call on the object.
140
- #
141
- # This behavior is required to solve a chicken-egg situation when we try
142
- # to load the router first and then the application with all its endpoints.
143
- #
144
- # @since 0.1.0
145
- #
146
- # @api private
147
- #
148
- # @see Hanami::Routing::ClassEndpoint
149
- class LazyEndpoint < SimpleDelegator
150
- # Initialize the lazy endpoint
151
- #
152
- # @since 0.1.0
153
- # @api private
154
- def initialize(name, namespace)
155
- @name = name
156
- @namespace = namespace
157
- end
158
-
159
- # Rack interface
160
- #
161
- # @raise [EndpointNotFound] when the endpoint can't be found.
162
- #
163
- # @since 0.1.0
164
- # @api private
165
- def call(env)
166
- obj.call(env)
167
- end
168
-
169
- # @since 0.2.0
170
- # @api private
171
- def inspect
172
- # TODO: review this implementation once the namespace feature will be
173
- # cleaned up.
174
- result = begin
175
- klass
176
- rescue
177
- nil
178
- end
179
-
180
- if result.nil?
181
- result = @name
182
- result = "#{@namespace}::#{result}" if @namespace != Object
183
- end
184
-
185
- result
186
- end
187
-
188
- # @since 1.0.0
189
- # @api private
190
- def routable?
191
- !__getobj__.nil?
192
- rescue ArgumentError
193
- false
194
- end
195
-
196
- private
197
-
198
- # @since 0.1.0
199
- # @api private
200
- def obj
201
- klass.new
202
- end
203
-
204
- # @since 0.2.0
205
- # @api private
206
- def klass
207
- Utils::Class.load!(@name, @namespace)
208
- rescue NameError => e
209
- raise EndpointNotFound.new(e.message)
210
- end
211
- end
212
- end
213
- end
@@ -1,242 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "hanami/utils/string"
4
- require "hanami/utils/class"
5
- require "hanami/routing/endpoint"
6
-
7
- module Hanami
8
- module Routing
9
- # Resolve duck-typed endpoints
10
- #
11
- # @since 0.1.0
12
- #
13
- # @api private
14
- class EndpointResolver
15
- # @since 0.2.0
16
- # @api private
17
- NAMING_PATTERN = "%<controller>s::%<action>s"
18
-
19
- # @since 0.7.0
20
- # @api private
21
- DEFAULT_RESPONSE = [404, { "X-Cascade" => "pass" }, "Not Found"].freeze
22
-
23
- # Default separator for controller and action.
24
- # A different separator can be passed to #initialize with the `:separator` option.
25
- #
26
- # @see #initialize
27
- # @see #resolve
28
- #
29
- # @since 0.1.0
30
- #
31
- # @example
32
- # require 'hanami/router'
33
- #
34
- # router = Hanami::Router.new do
35
- # get '/', to: 'articles#show'
36
- # end
37
- ACTION_SEPARATOR = "#"
38
-
39
- attr_reader :action_separator
40
-
41
- # Initialize an endpoint resolver
42
- #
43
- # @param options [Hash] the options used to customize lookup behavior
44
- #
45
- # @option options [Class] :endpoint the endpoint class that is returned
46
- # by `#resolve`. (defaults to `Hanami::Routing::Endpoint`)
47
- #
48
- # @option options [Class,Module] :namespace the Ruby namespace where to
49
- # lookup for controllers and actions. (defaults to `Object`)
50
- #
51
- # @option options [String] :pattern the string to interpolate in order
52
- # to return an action name. This string SHOULD contain
53
- # <tt>'%{controller}'</tt> and <tt>'%{action}'</tt>, all the other keys
54
- # will be ignored.
55
- # See the examples below.
56
- #
57
- # @option options [String] :action_separator the separator between controller and
58
- # action name. (defaults to `ACTION_SEPARATOR`)
59
- #
60
- # @return [Hanami::Routing::EndpointResolver] self
61
- #
62
- # @since 0.1.0
63
- # @api private
64
- #
65
- # @example Specify custom endpoint class
66
- # require 'hanami/router'
67
- #
68
- # resolver = Hanami::Routing::EndpointResolver.new(endpoint: CustomEndpoint)
69
- # router = Hanami::Router.new(resolver: resolver)
70
- #
71
- # router.get('/', to: endpoint).dest # => #<CustomEndpoint:0x007f97f3359570 ...>
72
- #
73
- # @example Specify custom Ruby namespace
74
- # require 'hanami/router'
75
- #
76
- # resolver = Hanami::Routing::EndpointResolver.new(namespace: MyApp)
77
- # router = Hanami::Router.new(resolver: resolver)
78
- #
79
- # router.get('/', to: 'articles#show')
80
- # # => Will look for: MyApp::Articles::Show
81
- #
82
- #
83
- #
84
- # @example Specify custom pattern
85
- # require 'hanami/router'
86
- #
87
- # resolver = Hanami::Routing::EndpointResolver.new(pattern: '%{controller}Controller::%{action}')
88
- # router = Hanami::Router.new(resolver: resolver)
89
- #
90
- # router.get('/', to: 'articles#show')
91
- # # => Will look for: ArticlesController::Show
92
- #
93
- #
94
- #
95
- # @example Specify custom controller-action separator
96
- # require 'hanami/router'
97
- #
98
- # resolver = Hanami::Routing::EndpointResolver.new(separator: '@')
99
- # router = Hanami::Router.new(resolver: resolver)
100
- #
101
- # router.get('/', to: 'articles@show')
102
- # # => Will look for: Articles::Show
103
- def initialize(options = {})
104
- @endpoint_class = options[:endpoint] || Endpoint
105
- @namespace = options[:namespace] || Object
106
- @action_separator = options[:action_separator] || ACTION_SEPARATOR
107
- @pattern = options[:pattern] || NAMING_PATTERN
108
- end
109
-
110
- # Resolve the given set of HTTP verb, path, endpoint and options.
111
- # If it fails to resolve, it will mount the default endpoint to the given
112
- # path, which returns an 404 (Not Found).
113
- #
114
- # @param options [Hash] the options required to resolve the endpoint
115
- #
116
- # @option options [String,Proc,Class,Object#call] :to the endpoint
117
- # @option options [String] :namespace an optional routing namespace
118
- #
119
- # @return [Endpoint] this may vary according to the :endpoint option
120
- # passed to #initialize
121
- #
122
- # @since 0.1.0
123
- # @api private
124
- #
125
- # @see #initialize
126
- # @see #find
127
- #
128
- # @example Resolve to a Proc
129
- # require 'hanami/router'
130
- #
131
- # router = Hanami::Router.new
132
- # router.get '/', to: ->(env) { [200, {}, ['Hi!']] }
133
- #
134
- # @example Resolve to a class
135
- # require 'hanami/router'
136
- #
137
- # router = Hanami::Router.new
138
- # router.get '/', to: RackMiddleware
139
- #
140
- # @example Resolve to a Rack compatible object (respond to #call)
141
- # require 'hanami/router'
142
- #
143
- # router = Hanami::Router.new
144
- # router.get '/', to: AnotherMiddleware.new
145
- #
146
- # @example Resolve to a Hanami::Action from a string (see Hanami::Controller framework)
147
- # require 'hanami/router'
148
- #
149
- # router = Hanami::Router.new
150
- # router.get '/', to: 'articles#show'
151
- #
152
- # @example Resolve to a Hanami::Action (see Hanami::Controller framework)
153
- # require 'hanami/router'
154
- #
155
- # router = Hanami::Router.new
156
- # router.get '/', to: Articles::Show
157
- #
158
- # @example Resolve a redirect with a namespace
159
- # require 'hanami/router'
160
- #
161
- # router = Hanami::Router.new
162
- # router.namespace 'users' do
163
- # get '/home', to: ->(env) { ... }
164
- # redirect '/dashboard', to: '/home'
165
- # end
166
- #
167
- # # GET /users/dashboard => 301 Location: "/users/home"
168
- def resolve(options, &endpoint)
169
- result = endpoint || find(options)
170
- resolve_callable(result) || resolve_matchable(result) || default
171
- end
172
-
173
- # Finds a path from the given options.
174
- #
175
- # @param options [Hash] the path description
176
- # @option options [String,Proc,Class,Object#call] :to the endpoint
177
- # @option options [String] :namespace an optional namespace
178
- #
179
- # @since 0.1.0
180
- # @api private
181
- #
182
- # @return [Object]
183
- def find(options)
184
- options[:to]
185
- end
186
-
187
- protected
188
-
189
- # @api private
190
- def default
191
- @endpoint_class.new(
192
- ->(_env) { DEFAULT_RESPONSE }
193
- )
194
- end
195
-
196
- # @api private
197
- def constantize(string)
198
- klass = Utils::Class.load!(string, @namespace)
199
- if klass.respond_to?(:call)
200
- Endpoint.new(klass)
201
- else
202
- ClassEndpoint.new(klass)
203
- end
204
- rescue NameError
205
- LazyEndpoint.new(string, @namespace)
206
- end
207
-
208
- # @api private
209
- def classify(string)
210
- Utils::String.transform(string, :underscore, :classify)
211
- end
212
-
213
- private
214
-
215
- # @api private
216
- def resolve_callable(callable)
217
- if callable.respond_to?(:call)
218
- @endpoint_class.new(callable)
219
- elsif callable.is_a?(Class) && callable.instance_methods.include?(:call)
220
- @endpoint_class.new(callable.new)
221
- end
222
- end
223
-
224
- # @api private
225
- def resolve_matchable(matchable)
226
- return unless matchable.respond_to?(:match)
227
-
228
- constantize(
229
- resolve_action(matchable) || classify(matchable)
230
- )
231
- end
232
-
233
- # @api private
234
- def resolve_action(string)
235
- return unless string.match?(action_separator)
236
-
237
- controller, action = string.split(action_separator).map { |token| classify(token) }
238
- format(@pattern, controller: controller, action: action)
239
- end
240
- end
241
- end
242
- end