hanami-router 1.3.2 → 2.0.0.alpha1

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 (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