hanami-router 1.3.2 → 2.0.0.alpha1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (35) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +14 -3
  3. data/README.md +192 -154
  4. data/hanami-router.gemspec +23 -20
  5. data/lib/hanami/middleware/body_parser.rb +17 -13
  6. data/lib/hanami/middleware/body_parser/class_interface.rb +56 -56
  7. data/lib/hanami/middleware/body_parser/errors.rb +7 -4
  8. data/lib/hanami/middleware/body_parser/json_parser.rb +5 -3
  9. data/lib/hanami/middleware/error.rb +16 -0
  10. data/lib/hanami/router.rb +262 -149
  11. data/lib/hanami/router/version.rb +3 -1
  12. data/lib/hanami/routing.rb +193 -0
  13. data/lib/hanami/routing/endpoint.rb +122 -104
  14. data/lib/hanami/routing/endpoint_resolver.rb +20 -16
  15. data/lib/hanami/routing/prefix.rb +102 -0
  16. data/lib/hanami/routing/recognized_route.rb +40 -26
  17. data/lib/hanami/routing/resource.rb +9 -7
  18. data/lib/hanami/routing/resource/action.rb +58 -33
  19. data/lib/hanami/routing/resource/nested.rb +4 -1
  20. data/lib/hanami/routing/resource/options.rb +3 -1
  21. data/lib/hanami/routing/resources.rb +6 -4
  22. data/lib/hanami/routing/resources/action.rb +11 -6
  23. data/lib/hanami/routing/routes_inspector.rb +22 -20
  24. data/lib/hanami/routing/scope.rb +112 -0
  25. metadata +47 -25
  26. data/lib/hanami-router.rb +0 -1
  27. data/lib/hanami/routing/error.rb +0 -7
  28. data/lib/hanami/routing/force_ssl.rb +0 -212
  29. data/lib/hanami/routing/http_router.rb +0 -220
  30. data/lib/hanami/routing/http_router_monkey_patch.rb +0 -38
  31. data/lib/hanami/routing/namespace.rb +0 -98
  32. data/lib/hanami/routing/parsers.rb +0 -113
  33. data/lib/hanami/routing/parsing/json_parser.rb +0 -33
  34. data/lib/hanami/routing/parsing/parser.rb +0 -61
  35. data/lib/hanami/routing/route.rb +0 -71
@@ -1,6 +1,8 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Hanami
2
4
  class Router
3
5
  # @since 0.1.0
4
- VERSION = '1.3.2'.freeze
6
+ VERSION = "2.0.0.alpha1"
5
7
  end
6
8
  end
@@ -0,0 +1,193 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "uri"
4
+ require "rack/utils"
5
+ require "mustermann/rails"
6
+
7
+ # Hanami
8
+ #
9
+ # @since 0.1.0
10
+ module Hanami
11
+ # Hanami routing
12
+ #
13
+ # @since 0.1.0
14
+ module Routing
15
+ PATH_INFO = "PATH_INFO"
16
+ QUERY_STRING = "QUERY_STRING"
17
+ REQUEST_METHOD = "REQUEST_METHOD"
18
+
19
+ HTTP_VERBS = %w[get post delete put patch trace options link unlink].freeze
20
+
21
+ PARAMS = "router.params"
22
+
23
+ def self.http_verbs
24
+ HTTP_VERBS
25
+ end
26
+
27
+ # @since 0.5.0
28
+ class Error < ::StandardError
29
+ end
30
+
31
+ # Invalid route
32
+ # This is raised when the router fails to recognize a route, because of the
33
+ # given arguments.
34
+ #
35
+ # @since 0.1.0
36
+ class InvalidRouteException < Error
37
+ end
38
+
39
+ # Endpoint not found
40
+ # This is raised when the router fails to load an endpoint at the runtime.
41
+ #
42
+ # @since 0.1.0
43
+ class EndpointNotFound < Error
44
+ end
45
+
46
+ # @since 2.0.0
47
+ class NotCallableEndpointError < Error
48
+ def initialize(endpoint)
49
+ super("#{endpoint.inspect} isn't compatible with Rack. Please make sure it implements #call.")
50
+ end
51
+ end
52
+
53
+ # HTTP redirect
54
+ #
55
+ # @since 2.0.0
56
+ # @api private
57
+ class Redirect
58
+ # @since 2.0.0
59
+ # @api private
60
+ LOCATION = "Location"
61
+
62
+ # @since 2.0.0
63
+ # @api private
64
+ STATUS_RANGE = (300..399).freeze
65
+
66
+ attr_reader :path
67
+ alias destination_path path
68
+
69
+ # Instantiate a new redirect
70
+ #
71
+ # @param path [String] a relative or absolute URI
72
+ # @param status [Integer] a redirect status (an integer between `300` and `399`)
73
+ #
74
+ # @return [Hanami::Routing::Redirect] a new instance
75
+ #
76
+ # @raise [ArgumentError] if path is nil, or status code isn't a redirect
77
+ #
78
+ # @since 2.0.0
79
+ # @api private
80
+ def initialize(path, status)
81
+ raise ArgumentError.new("Path is nil") if path.nil?
82
+ raise ArgumentError.new("Status code isn't a redirect: #{status.inspect}") unless STATUS_RANGE.include?(status)
83
+
84
+ @path = path
85
+ @status = status
86
+ freeze
87
+ end
88
+
89
+ def call(*)
90
+ [@status, { LOCATION => @path }, []]
91
+ end
92
+
93
+ def redirect?
94
+ true
95
+ end
96
+ end
97
+
98
+ # Route
99
+ #
100
+ # @since 0.1.0
101
+ # @api private
102
+ class Route
103
+ # @since 0.7.0
104
+ # @api private
105
+ def initialize(verb, path, endpoint, constraints)
106
+ @verb = verb
107
+ @path = Mustermann.new(path, type: :rails, version: "5.0", capture: constraints)
108
+ @endpoint = endpoint
109
+ freeze
110
+ end
111
+
112
+ # @since 0.1.0
113
+ # @api private
114
+ def call(env)
115
+ env[PARAMS] ||= {}
116
+ env[PARAMS].merge!(Rack::Utils.parse_nested_query(env[QUERY_STRING]))
117
+ env[PARAMS].merge!(@path.params(env[PATH_INFO]))
118
+ env[PARAMS] = Utils::Hash.deep_symbolize(env[PARAMS])
119
+
120
+ @endpoint.call(env)
121
+ end
122
+
123
+ # @since 0.1.0
124
+ # @api private
125
+ def path(args)
126
+ @path.expand(:append, args)
127
+ rescue Mustermann::ExpandError => e
128
+ raise Hanami::Routing::InvalidRouteException.new(e.message)
129
+ end
130
+
131
+ # @since 2.0.0
132
+ # @api private
133
+ def match?(env)
134
+ match_path?(env) &&
135
+ @verb.include?(env[REQUEST_METHOD])
136
+ end
137
+
138
+ # @since 2.0.0
139
+ # @api private
140
+ def match_path?(env)
141
+ @path =~ env[PATH_INFO]
142
+ end
143
+ end
144
+
145
+ # @since 2.0.0
146
+ # @api private
147
+ module Uri
148
+ # @since 2.0.0
149
+ # @api private
150
+ HTTP = "http"
151
+
152
+ # @since 2.0.0
153
+ # @api private
154
+ HTTPS = "https"
155
+
156
+ # @since 2.0.0
157
+ # @api private
158
+ DEFAULT_SCHEME = HTTP
159
+
160
+ # Build a URI string from the given arguments
161
+ #
162
+ # @param scheme [String] the URI scheme: one of `"http"` or `"https"`
163
+ # @param host [String] the URI host
164
+ # @param port [String,Integer] the URI port
165
+ #
166
+ # @raise [ArgumentError] if one of `scheme`, `host`, `port` is `nil`, or
167
+ # if `scheme` is unknown
168
+ #
169
+ # @since 2.0.0
170
+ # @api private
171
+ def self.build(scheme:, host:, port:)
172
+ raise ArgumentError.new("host is nil") if host.nil?
173
+ raise ArgumentError.new("port is nil") if port.nil?
174
+
175
+ case scheme
176
+ when HTTP
177
+ URI::HTTP
178
+ when HTTPS
179
+ URI::HTTPS
180
+ else
181
+ raise ArgumentError.new("Unknown scheme: #{scheme.inspect}")
182
+ end.build(scheme: scheme, host: host, port: port).to_s
183
+ end
184
+ end
185
+
186
+ require "hanami/routing/endpoint"
187
+ require "hanami/routing/prefix"
188
+ require "hanami/routing/scope"
189
+ require "hanami/routing/resource"
190
+ require "hanami/routing/resources"
191
+ require "hanami/routing/recognized_route"
192
+ end
193
+ end
@@ -1,57 +1,77 @@
1
- require 'delegate'
2
- require 'hanami/routing/error'
3
- require 'hanami/utils/class'
1
+ # frozen_string_literal: true
2
+
3
+ require "delegate"
4
+ require "hanami/utils/class"
5
+ require "hanami/utils/string"
4
6
 
5
7
  module Hanami
6
8
  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
9
+ # Routes endpoint
23
10
  #
11
+ # @since 2.0.0
24
12
  # @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
13
+ module Endpoint
14
+ # @since 2.0.0
35
15
  # @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
16
+ #
17
+ # FIXME: Shall this be the default of Utils::Class.load! ?
18
+ DEFAULT_NAMESPACE = Object
49
19
 
50
- # @since 1.0.0
20
+ # Controller / action separator for Hanami
21
+ #
22
+ # @since 2.0.0
51
23
  # @api private
52
- def routable?
53
- !__getobj__.nil?
54
- rescue ArgumentError
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
55
75
  end
56
76
 
57
77
  # @since 1.0.1
@@ -64,42 +84,47 @@ module Hanami
64
84
  # @api private
65
85
  def destination_path
66
86
  end
67
- end
68
87
 
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
88
+ # Find an endpoint from its name
97
89
  #
98
- # @since 0.1.0
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
99
  # @api private
100
- def call(env)
101
- __getobj__.new.call(env)
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)
102
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?
103
128
  end
104
129
 
105
130
  # Routing endpoint
@@ -121,13 +146,14 @@ module Hanami
121
146
  # @api private
122
147
  #
123
148
  # @see Hanami::Routing::ClassEndpoint
124
- class LazyEndpoint < Endpoint
149
+ class LazyEndpoint < SimpleDelegator
125
150
  # Initialize the lazy endpoint
126
151
  #
127
152
  # @since 0.1.0
128
153
  # @api private
129
154
  def initialize(name, namespace)
130
- @name, @namespace = name, namespace
155
+ @name = name
156
+ @namespace = namespace
131
157
  end
132
158
 
133
159
  # Rack interface
@@ -143,19 +169,32 @@ module Hanami
143
169
  # @since 0.2.0
144
170
  # @api private
145
171
  def inspect
146
- # TODO review this implementation once the namespace feature will be
172
+ # TODO: review this implementation once the namespace feature will be
147
173
  # cleaned up.
148
- result = klass rescue nil
174
+ result = begin
175
+ klass
176
+ rescue
177
+ nil
178
+ end
149
179
 
150
180
  if result.nil?
151
181
  result = @name
152
- result = "#{ @namespace }::#{ result }" if @namespace != Object
182
+ result = "#{@namespace}::#{result}" if @namespace != Object
153
183
  end
154
184
 
155
185
  result
156
186
  end
157
187
 
188
+ # @since 1.0.0
189
+ # @api private
190
+ def routable?
191
+ !__getobj__.nil?
192
+ rescue ArgumentError
193
+ false
194
+ end
195
+
158
196
  private
197
+
159
198
  # @since 0.1.0
160
199
  # @api private
161
200
  def obj
@@ -170,26 +209,5 @@ module Hanami
170
209
  raise EndpointNotFound.new(e.message)
171
210
  end
172
211
  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
212
  end
195
213
  end