pendragon 0.6.2 → 1.0.0

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