hanami-router 2.0.0.alpha1 → 2.0.0.alpha2

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.
@@ -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