pendragon 0.6.2 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/Rakefile CHANGED
@@ -2,24 +2,9 @@ require 'rake'
2
2
  require 'rake/testtask'
3
3
  require 'pendragon'
4
4
 
5
- Rake::TestTask.new(:test_without_compiler) do |test|
5
+ Rake::TestTask.new(:test) do |test|
6
6
  test.libs << 'test'
7
- test.test_files = Dir['test/**/*_test.rb']
8
- test.verbose = true
7
+ test.test_files = Dir['test/**/test_*.rb']
9
8
  end
10
9
 
11
- Rake::TestTask.new(:test_with_compiler) do |test|
12
- test.libs << 'test'
13
- test.ruby_opts = ["-r compile_helper.rb"]
14
- test.test_files = Dir['test/**/*_test.rb']
15
- test.verbose = true
16
- end
17
-
18
- Rake::TestTask.new(:configuration) do |test|
19
- test.libs << 'test'
20
- test.test_files = Dir['test/**/*_configuration.rb']
21
- test.verbose = true
22
- end
23
-
24
- task :test => [:test_without_compiler, :test_with_compiler, :configuration]
25
- task :default => :test
10
+ task default: :test
@@ -0,0 +1,31 @@
1
+ require 'benchmark'
2
+ require 'pendragon'
3
+ require 'rack'
4
+
5
+ routers = %i[liner realism radix].map do |type|
6
+ Pendragon[type].new do
7
+ 1000.times { |n| get "/#{n}", to: ->(env) { [200, {}, [n.to_s]] } }
8
+ namespace :foo do
9
+ get '/:user_id' do
10
+ [200, {}, ['yahoo']]
11
+ end
12
+ end
13
+ end
14
+ end
15
+
16
+ env = Rack::MockRequest.env_for("/999")
17
+
18
+ routers.each do |router|
19
+ p "router_class: #{router.class}"
20
+ p router.call(env)
21
+ end
22
+
23
+ Benchmark.bm do |x|
24
+ routers.each do |router|
25
+ x.report do
26
+ 10000.times do |n|
27
+ router.call(env)
28
+ end
29
+ end
30
+ end
31
+ end
@@ -1,55 +1,71 @@
1
1
  require 'pendragon/router'
2
+ require 'thread'
2
3
 
3
4
  module Pendragon
5
+ # Type to use if no type is given.
6
+ # @api private
7
+ DEFAULT_TYPE = :realism
4
8
 
5
- # Allow the verbs of these.
6
- HTTP_VERBS = %w[GET POST PUT PATCH DELETE HEAD OPTIONS LINK UNLINK].freeze
9
+ # Creates a new router.
10
+ #
11
+ # @example creating new routes.
12
+ # require 'pendragon'
13
+ #
14
+ # Pendragon.new do
15
+ # get('/') { [200, {}, ['hello world']] }
16
+ # namespace :users do
17
+ # get('/', to: ->(env) { [200, {}, ['User page index']] })
18
+ # get('/:id', to: UserApplication.new)
19
+ # end
20
+ # end
21
+ #
22
+ # @yield block for definig routes, it will be evaluated in instance context.
23
+ # @yieldreturn [Pendragon::Router]
24
+ def self.new(type: DEFAULT_TYPE, &block)
25
+ type ||= DEFAULT_TYPE
26
+ self[type].new(&block)
27
+ end
7
28
 
8
- class << self
9
- # A new instance of Pendragon::Router
10
- # @see Pendragon::Router#initialize
11
- def new(&block)
12
- Router.new(&block)
13
- end
14
-
15
- # @deprecated
16
- # Yields Pendragon configuration block
17
- # @example
18
- # Pendragon.configure do |config|
19
- # config.enable_compiler = true
20
- # end
21
- # @see Pendragon::Configuration
22
- def configure(&block)
23
- configuration_warning(:configure)
24
- block.call(configuration) if block_given?
25
- configuration
26
- end
27
-
28
- # @deprecated
29
- # Returns Pendragon configuration
30
- def configuration
31
- configuration_warning(:configuration)
32
- @configuration ||= Configuration.new
33
- end
29
+ @mutex ||= Mutex.new
30
+ @types ||= {}
34
31
 
35
- # @deprecated
36
- # Resets Pendragon configuration
37
- def reset_configuration!
38
- @configuration = nil
32
+ # Returns router by given name.
33
+ #
34
+ # @example
35
+ # Pendragon[:realism] #=> Pendragon::Realism
36
+ #
37
+ # @param [Symbol] name a router type identifier
38
+ # @raise [ArgumentError] if the name is not supported
39
+ # @return [Class, #new]
40
+ def self.[](name)
41
+ @types.fetch(normalized = normalize_type(name)) do
42
+ @mutex.synchronize do
43
+ error = try_require "pendragon/#{normalized}"
44
+ @types.fetch(normalized) do
45
+ fail ArgumentError,
46
+ "unsupported type %p #{ " (#{error.message})" if error }" % name
47
+ end
48
+ end
39
49
  end
50
+ end
40
51
 
41
- # @!visibility private
42
- def configuration_warning(method)
43
- warn <<-WARN
44
- Pendragon.#{method} is deprecated because it isn't thread-safe.
45
- Please use new syntax.
46
- Pendragon.new do |config|
47
- config.auto_rack_format = false
48
- config.enable_compiler = true
49
- end
50
- WARN
51
- end
52
+ # @return [LoadError, nil]
53
+ # @!visibility private
54
+ def self.try_require(path)
55
+ require(path)
56
+ nil
57
+ rescue LoadError => error
58
+ raise(error) unless error.path == path
59
+ error
60
+ end
61
+
62
+ # @!visibility private
63
+ def self.register(name, type)
64
+ @types[normalize_type(name)] = type
65
+ end
52
66
 
53
- private :configuration_warning
67
+ # @!visibility private
68
+ def self.normalize_type(type)
69
+ type.to_s.gsub('-', '_').downcase
54
70
  end
55
71
  end
@@ -0,0 +1,27 @@
1
+ module Pendragon
2
+ # A module for unifying magic numbers
3
+ # @!visibility private
4
+ module Constants
5
+ module Http
6
+ GET = 'GET'.freeze
7
+ POST = 'POST'.freeze
8
+ PUT = 'PUT'.freeze
9
+ DELETE = 'DELETE'.freeze
10
+ HEAD = 'HEAD'.freeze
11
+ OPTIONS = 'OPTIONS'.freeze
12
+
13
+ NOT_FOUND = 404.freeze
14
+ METHOD_NOT_ALLOWED = 405.freeze
15
+ INTERNAL_SERVER_ERROR = 500.freeze
16
+ end
17
+
18
+ module Header
19
+ CASCADE = 'X-Cascade'.freeze
20
+ end
21
+
22
+ module Env
23
+ PATH_INFO = 'PATH_INFO'.freeze
24
+ REQUEST_METHOD = 'REQUEST_METHOD'.freeze
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,72 @@
1
+ require 'rack/utils'
2
+
3
+ module Pendragon
4
+ # Module for creating any error classes.
5
+ module Errors
6
+ # Class for handling HTTP error.
7
+ class Base < StandardError
8
+ attr_accessor :status, :headers, :message
9
+
10
+ # Creates a new error class.
11
+ #
12
+ # @example
13
+ # require 'pendragon/errors'
14
+ #
15
+ # BadRequest = Pendragon::Errors::Base.create(status: 400)
16
+ #
17
+ # @option [Integer] status
18
+ # @option [Hash{String => String}] headers
19
+ # @option [String] message
20
+ # @return [Class]
21
+ def self.create(**options, &block)
22
+ Class.new(self) do
23
+ options.each { |k, v| define_singleton_method(k) { v } }
24
+ class_eval(&block) if block_given?
25
+ end
26
+ end
27
+
28
+ # Returns default message.
29
+ #
30
+ # @see [Rack::Utils::HTTP_STATUS_CODES]
31
+ # @return [String] default message for current status.
32
+ def self.default_message
33
+ @default_message ||= Rack::Utils::HTTP_STATUS_CODES.fetch(status, 'server error').downcase
34
+ end
35
+
36
+ # Returns default headers.
37
+ #
38
+ # @return [Hash{String => String}] HTTP headers
39
+ def self.default_headers
40
+ @default_headers ||= { 'Content-Type' => 'text/plain' }
41
+ end
42
+
43
+ # Constructs an instance of Errors::Base
44
+ #
45
+ # @option [Hash{String => String}] headers
46
+ # @option [Integer] status
47
+ # @option [String] message
48
+ # @options payload
49
+ # @return [Pendragon::Errors::Base]
50
+ def initialize(headers: {}, status: self.class.status, message: self.class.default_message, **payload)
51
+ self.headers = self.class.default_headers.merge(headers)
52
+ self.status, self.message = status, message
53
+ parse_payload(**payload) if payload.kind_of?(Hash) && respond_to?(:parse_payload)
54
+ super(message)
55
+ end
56
+
57
+ # Converts self into response conformed Rack style.
58
+ #
59
+ # @return [Array<Integer, Hash{String => String}, #each>] response
60
+ def to_response
61
+ [status, headers, [message]]
62
+ end
63
+ end
64
+
65
+ NotFound = Base.create(status: 404)
66
+ MethodNotAllowed = Base.create(status: 405) do
67
+ define_method(:parse_payload) do |allows: [], **payload|
68
+ self.headers['Allows'] = allows.join(?,) unless allows.empty?
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,9 @@
1
+ require 'pendragon/router'
2
+
3
+ module Pendragon
4
+ class Linear < Router
5
+ register :linear
6
+
7
+ on(:call) { |env| rotation(env) { |route| route.exec(env) } }
8
+ end
9
+ end
@@ -0,0 +1,49 @@
1
+ require 'pendragon/router'
2
+
3
+ module Pendragon
4
+ class Realism < Router
5
+ register :realism
6
+
7
+ on :call do |env|
8
+ identity(env) || rotation(env) { |route| route.exec(env) }
9
+ end
10
+
11
+ on :compile do |method, routes|
12
+ patterns = routes.map.with_index do |route, index|
13
+ route.index = index
14
+ route.regexp = /(?<_#{index}>#{route.pattern.to_regexp})/
15
+ end
16
+ omap[method] = Regexp.union(patterns)
17
+ end
18
+
19
+ private
20
+
21
+ # @!visibility private
22
+ def omap
23
+ @omap ||= Hash.new { |hash, key| hash[key] = // }
24
+ end
25
+
26
+ # @!visibility private
27
+ def match?(input, method)
28
+ current_regexp = omap[method]
29
+ return unless current_regexp.match(input)
30
+ last_match = Regexp.last_match
31
+ map[method].detect { |route| last_match["_#{route.index}"] }
32
+ end
33
+
34
+ # @!visibility private
35
+ def identity(env, route = nil)
36
+ with_transaction(env) do |input, method|
37
+ route = match?(input, method)
38
+ route.exec(env) if route
39
+ end
40
+ end
41
+
42
+ # @!visibility private
43
+ def with_transaction(env)
44
+ input, method = extract(env)
45
+ response = yield(input, method)
46
+ response && !(cascade = cascade?(response)) ? response : nil
47
+ end
48
+ end
49
+ end
@@ -1,188 +1,341 @@
1
- require 'pendragon/route'
2
- require 'pendragon/matcher'
3
- require 'pendragon/error'
4
- require 'pendragon/configuration'
5
- require 'pendragon/engine/compiler'
6
- require 'rack'
1
+ require 'pendragon/constants'
2
+ require 'pendragon/errors'
3
+ require 'mustermann'
4
+ require 'forwardable'
5
+ require 'ostruct'
7
6
 
8
7
  module Pendragon
9
- # A class for the router
10
- #
11
- # @example Construct with a block which has no argument
12
- # router = Pendragon do
13
- # get("/"){ "hello world" }
14
- # end
15
- #
16
- # @example Construct with a block which has an argument
17
- # router = Pendragon.new do |config|
18
- # config.enable_compiler = true
19
- # end
20
8
  class Router
21
- # The accessors are useful to access from Pendragon::Route
22
- attr_accessor :current, :routes
9
+ # @!visibility private
10
+ attr_accessor :prefix
23
11
 
24
- # @see Pendragon::Configuration#lock?
25
- @@mutex = Mutex.new
12
+ # Registers new router type onto global maps.
13
+ #
14
+ # @example registring new router type.
15
+ # require 'pendragon'
16
+ #
17
+ # class Pendragon::SuperFast < Pendragon::Router
18
+ # register :super_fast
19
+ # end
20
+ #
21
+ # Pendragon[:super_fast] #=> Pendragon::SuperFast
22
+ #
23
+ # @param [Symbol] name a router type identifier
24
+ # @see Pendragon.register
25
+ def self.register(name)
26
+ Pendragon.register(name, self)
27
+ end
26
28
 
27
- # Constructs a new instance of Pendragon::Router
28
- # Possible to pass the block
29
+ # Adds event listener in router class.
30
+ #
31
+ # @example
32
+ # require 'pendragon'
29
33
  #
30
- # @example with a block
31
- # app = Pendragon::Router.new do |config|
32
- # config.enable_compiler = true
33
- # config.auto_rack_format = false
34
+ # class Pendragon::SuperFast < Pendragon::Router
35
+ # register :super_fast
36
+ #
37
+ # on :call do |env|
38
+ # rotation(env) { |route| route.exec(env) }
39
+ # end
40
+ #
41
+ # on :compile do |method, routes|
42
+ # routes.each do |route|
43
+ # route.pattern = route.pattern.to_regexp
44
+ # end
45
+ # end
34
46
  # end
35
47
  #
36
- # @example with base style
37
- # app = Pendragon::Router.new
38
- # app.get("/"){ "hello!" }
39
- # app.post("/"){ "hello post!" }
48
+ # @param [Symbol] event a event name which is :call or :compile
49
+ #
50
+ # @yieldparam [optional, Hash] env a request environment variables on :call event.
51
+ # @yieldreturn [optional, Array<Integer, Hash, #each>, Rack::Response] response
52
+ #
53
+ # @yieldparam [String] method
54
+ # @yieldparam [Array<Pendragon::Route>] routes
55
+ def self.on(event, &listener)
56
+ define_method('on_%s_listener' % event, &listener)
57
+ end
58
+
59
+ # Construcsts an instance of router class.
60
+ #
61
+ # @example construction for router class
62
+ # require 'pendragon'
63
+ #
64
+ # Pendragon.new do
65
+ # get '/', to: -> { [200, {}, ['hello']] }
66
+ # end
67
+ #
68
+ # @yield block a block is evaluated in instance context.
69
+ # @return [Pendragon::Router]
40
70
  def initialize(&block)
41
- reset!
42
- if block_given?
43
- if block.arity.zero?
44
- instance_eval(&block)
45
- else
46
- @configuration = Configuration.new
47
- block.call(configuration)
48
- end
49
- end
71
+ @compiled = false
72
+ instance_eval(&block) if block_given?
50
73
  end
51
74
 
52
- # Finds the routes if request method is valid
53
- # @return the Rack style response
75
+ # Prefixes a namespace to route path inside given block.
76
+ #
77
+ # @example
78
+ # require 'pendragon'
79
+ #
80
+ # Pendragon.new do
81
+ # namespace :foo do
82
+ # # This definition is dispatched to '/foo/bar'.
83
+ # get '/bar', to: -> { [200, {}, ['hello']] }
84
+ # end
85
+ # end
86
+ #
87
+ # @yield block a block is evaluated in instance context.
88
+ def namespace(name, &block)
89
+ fail ArgumentError unless block_given?
90
+ (self.prefix ||= []) << name.to_s
91
+ instance_eval(&block)
92
+ ensure
93
+ prefix.pop
94
+ end
95
+
96
+ # Calls by given env, returns a response conformed Rack style.
97
+ #
98
+ # @example
99
+ # require 'pendragon'
100
+ #
101
+ # router = Pendragon.new do
102
+ # get '/', to: -> { [200, {}, ['hello']] }
103
+ # end
104
+ #
105
+ # env = Rack::MockRequest.env_for('/')
106
+ # router.call(env) #=> [200, {}, ['hello']]
107
+ #
108
+ # @return [Array<Integer, Hash, #each>, Rack::Response] response conformed Rack style
54
109
  def call(env)
55
- request = Rack::Request.new(env)
56
- recognize(request).each do |route, params|
57
- catch(:pass){ return invoke(route, params) }
110
+ catch(:halt) { with_optimization { invoke(env) } }
111
+ end
112
+
113
+ # Class for delegation based structure.
114
+ # @!visibility private
115
+ class Route < OpenStruct
116
+ # @!visibility private
117
+ attr_accessor :pattern
118
+
119
+ # @!visibility private
120
+ attr_reader :request_method, :path
121
+
122
+ extend Forwardable
123
+ def_delegators :@pattern, :match, :params
124
+
125
+ # @!visibility private
126
+ def initialize(method:, pattern:, application:, **attributes)
127
+ super(attributes)
128
+
129
+ @app = application
130
+ @path = pattern
131
+ @pattern = Mustermann.new(pattern)
132
+ @executable = to_executable
133
+ @request_method = method.to_s.upcase
134
+ end
135
+
136
+ # @!visibility private
137
+ def exec(env)
138
+ return @app.call(env) unless executable?
139
+ path_info = env[Constants::Env::PATH_INFO]
140
+ params = pattern.params(path_info)
141
+ captures = pattern.match(path_info).captures
142
+ Context.new(env, params: params, captures: captures).trigger(@executable)
143
+ end
144
+
145
+ private
146
+
147
+ # @!visibility private
148
+ def executable?
149
+ @app.kind_of?(Proc)
150
+ end
151
+
152
+ # @!visibility private
153
+ def to_executable
154
+ return @app unless executable?
155
+ Context.to_method(request_method, path, @app)
156
+ end
157
+
158
+ # Class for providing helpers like :env, :params and :captures.
159
+ # This class will be available if given application is an kind of Proc.
160
+ # @!visibility private
161
+ class Context
162
+ # @!visibility private
163
+ attr_reader :env, :params, :captures
164
+
165
+ # @!visibility private
166
+ def self.generate_method(name, callable)
167
+ define_method(name, &callable)
168
+ method = instance_method(name)
169
+ remove_method(name)
170
+ method
171
+ end
172
+
173
+ # @!visibility private
174
+ def self.to_method(*args, callable)
175
+ unbound = generate_method(args.join(' '), callable)
176
+ if unbound.arity.zero?
177
+ proc { |app, captures| unbound.bind(app).call }
178
+ else
179
+ proc { |app, captures| unbound.bind(app).call(*captures) }
180
+ end
181
+ end
182
+
183
+ # @!visibility private
184
+ def initialize(env, params: {}, captures: [])
185
+ @env = env
186
+ @params = params
187
+ @captures = captures
188
+ end
189
+
190
+ # @!visibility private
191
+ def trigger(executable)
192
+ executable[self, captures]
193
+ end
58
194
  end
59
- rescue BadRequest, NotFound, MethodNotAllowed
60
- $!.call
61
195
  end
62
196
 
63
- # Calls a route, and build return value of the router
64
- # @param [Pendragon::Route] route The route matched with the condition of request
65
- # @param [Hash] params The params will be passed with the route
66
- # @return [Array<Fixnum, Hash, Array>] The return value of the route block
67
- def invoke(route, params)
68
- response = route.arity != 0 ? route.call(params) : route.call
69
- return response unless configuration.auto_rack_format?
70
- status = route.options[:status] || 200
71
- header = {'Content-Type' => 'text/html;charset=utf-8'}.merge(route.options[:header] || {})
72
- [status, header, Array(response)]
197
+ # Appends a route of GET method
198
+ # @see [Pendragon::Router#route]
199
+ def get(path, to: nil, **options, &block)
200
+ route Constants::Http::GET, path, to: to, **options, &block
73
201
  end
74
202
 
75
- # Provides some methods intuitive than #add
76
- # Basic usage is the same as #add
77
- # @see Pendragon::Router#add
78
- def get(path, options = {}, &block); add :get, path, options, &block end
79
- def post(path, options = {}, &block); add :post, path, options, &block end
80
- def delete(path, options = {}, &block); add :delete, path, options, &block end
81
- def put(path, options = {}, &block); add :put, path, options, &block end
82
- def head(path, options = {}, &block); add :head, path, options, &block end
203
+ # Appends a route of POST method
204
+ # @see [Pendragon::Router#route]
205
+ def post(path, to: nil, **options, &block)
206
+ route Constants::Http::POST, path, to: to, **options, &block
207
+ end
83
208
 
84
- # Adds a new route to router
85
- # @return [Pendragon::Route]
86
- def add(verb, path, options = {}, &block)
87
- routes << (route = Route.new(path, verb, options, &block))
88
- route.router = self
89
- route
209
+ # Appends a route of PUT method
210
+ # @see [Pendragon::Router#route]
211
+ def put(path, to: nil, **options, &block)
212
+ route Constants::Http::PUT, path, to: to, **options, &block
90
213
  end
91
214
 
92
- # Resets the router's instance variables
93
- def reset!
94
- @routes = []
95
- @current = 0
96
- @prepared = nil
215
+ # Appends a route of DELETE method
216
+ # @see [Pendragon::Router#route]
217
+ def delete(path, to: nil, **options, &block)
218
+ route Constants::Http::DELETE, path, to: to, **options, &block
97
219
  end
98
220
 
99
- # Prepares the router for route's priority
100
- # This method is executed only once in the initial load
101
- def prepare!
102
- @routes.sort_by!(&:order)
103
- @engine = (configuration.enable_compiler? ? Compiler : Recognizer).new(routes)
104
- @prepared = true
221
+ # Appends a route of HEAD method
222
+ # @see [Pendragon::Router#route]
223
+ def head(path, to: nil, **options, &block)
224
+ route Constants::Http::HEAD, path, to: to, **options, &block
105
225
  end
106
226
 
107
- # @return [Boolean] the router is already prepared?
108
- def prepared?
109
- !!@prepared
227
+ # Appends a route of OPTIONS method
228
+ # @see [Pendragon::Router#route]
229
+ def options(path, to: nil, **options, &block)
230
+ route Constants::Http::OPTIONS, path, to: to, **options, &block
110
231
  end
111
232
 
112
- # Increments for the integrity of priorities
113
- def increment_order!
114
- @current += 1
233
+ # Appends a new route to router.
234
+ #
235
+ # @param [String] method A request method, it should be upcased.
236
+ # @param [String] path The application is dispatched to given path.
237
+ # @option [Class, #call] :to
238
+ def route(method, path, to: nil, **options, &block)
239
+ app = block_given? ? block : to
240
+ fail ArgumentError, 'Rack application could not be found' unless app
241
+ path = ?/ + prefix.join(?/) + path if prefix && !prefix.empty?
242
+ append Route.new(method: method, pattern: path, application: app, **options)
115
243
  end
116
244
 
117
- # Recognizes the route by request
118
- # @param request [Rack::Request]
119
- # @return [Array]
120
- def recognize(request)
121
- prepare! unless prepared?
122
- synchronize { @engine.call(request) }
245
+ # Maps all routes for each request methods.
246
+ # @return [Hash{String => Array}] map
247
+ def map
248
+ @map ||= Hash.new { |hash, key| hash[key] = [] }
123
249
  end
124
250
 
125
- # Recognizes a given path
126
- # @param path_info [String]
127
- # @return [Array]
128
- def recognize_path(path_info)
129
- route, params = recognize(Rack::MockRequest.env_for(path_info)).first
130
- [route.name, params.inject({}){|hash, (key, value)| hash[key] = value; hash }]
251
+ # Maps all routes.
252
+ # @return [Array<Pendragon::Route>] flat_map
253
+ def flat_map
254
+ @flat_map ||= []
131
255
  end
132
256
 
133
- # Returns an expanded path matched with the conditions as arguments
134
- # @return [String, Regexp]
135
- # @example
136
- # router = Pendragon.new
137
- # index = router.get("/:id", :name => :index){}
138
- # router.path(:index, :id => 1) #=> "/1"
139
- # router.path(:index, :id => 2, :foo => "bar") #=> "/1?foo=bar"
140
- def path(name, *args)
141
- extract_with(name, *args) do |route, params, matcher|
142
- matcher.mustermann? ? matcher.expand(params) : route.path
257
+ private
258
+
259
+ # @!visibility private
260
+ def append(route)
261
+ flat_map << route
262
+ map[route.request_method] << route
263
+ end
264
+
265
+ # @!visibility private
266
+ def invoke(env)
267
+ response = on_call_listener(env)
268
+ if !response && (allows = find_allows(env))
269
+ error!(Errors::MethodNotAllowed, allows: allows)
143
270
  end
271
+ response || error!(Errors::NotFound)
144
272
  end
145
273
 
146
- # Returns Pendragon configuration
147
- # @return [Pendragon::Configuration]
148
- def configuration
149
- @configuration || Pendragon.configuration
274
+ # @!visibility private
275
+ def error!(error_class, **payload)
276
+ throw :halt, error_class.new(**payload).to_response
150
277
  end
151
278
 
152
279
  # @!visibility private
153
- # @example
154
- # extract_with(:index) do |route, params|
155
- # route.matcher.mustermann? ? route.matcher.expand(params) : route.path
156
- # end
157
- def extract_with(name, *args)
158
- params = args.delete_at(args.last.is_a?(Hash) ? -1 : 0) || {}
159
- saved_args = args.dup
160
- @routes.each do |route|
161
- next unless route.name == name
162
- matcher = route.matcher
163
- if !args.empty? and matcher.mustermann?
164
- matcher_names = matcher.names
165
- params_for_expand = Hash[matcher_names.map{|matcher_name|
166
- [matcher_name.to_sym, (params[matcher_name] || args.shift)]}]
167
- params_for_expand.merge!(Hash[params.select{|k, v| !matcher_names.include?(name) }])
168
- args = saved_args.dup
169
- else
170
- params_for_expand = params.dup
171
- end
172
- return yield(route, params_for_expand, matcher)
173
- end
174
- raise InvalidRouteException
280
+ def find_allows(env)
281
+ pattern = env[Constants::Env::PATH_INFO]
282
+ hits = flat_map.select { |route| route.match(pattern) }.map(&:request_method)
283
+ hits.empty? ? nil : hits
284
+ end
285
+
286
+ # @!visibility private
287
+ def extract(env, required: [:input, :method])
288
+ extracted = []
289
+ extracted << env[Constants::Env::PATH_INFO] if required.include?(:input)
290
+ extracted << env[Constants::Env::REQUEST_METHOD] if required.include?(:method)
291
+ extracted
175
292
  end
176
293
 
177
294
  # @!visibility private
178
- def synchronize(&block)
179
- if configuration.lock?
180
- @@mutex.synchronize(&block)
181
- else
182
- yield
295
+ def rotation(env, exact_route = nil)
296
+ input, method = extract(env)
297
+ response = nil
298
+ map[method].each do |route|
299
+ next unless route.match(input)
300
+ response = yield(route)
301
+ break(response) unless cascade?(response)
302
+ response = nil
183
303
  end
304
+ response
305
+ end
306
+
307
+ # @!visibility private
308
+ def cascade?(response)
309
+ response && response[1][Constants::Header::CASCADE] == 'pass'
310
+ end
311
+
312
+ # @!visibility private
313
+ def compile
314
+ map.each(&method(:on_compile_listener))
315
+ @compiled = true
184
316
  end
185
317
 
186
- private :extract_with, :synchronize
318
+ # @!visibility private
319
+ def with_optimization
320
+ compile unless compiled?
321
+ yield
322
+ end
323
+
324
+ # Optional event listener
325
+ # @param [String] method A request method like GET, POST
326
+ # @param [Array<Pendragon::Route>] routes All routes associated to the method
327
+ # @!visibility private
328
+ def on_compile_listener(method, routes)
329
+ end
330
+
331
+ # @!visibility private
332
+ def on_call_listener(env)
333
+ fail NotImplementedError
334
+ end
335
+
336
+ # @!visibility private
337
+ def compiled?
338
+ @compiled
339
+ end
187
340
  end
188
341
  end