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.
- checksums.yaml +4 -4
- data/.travis.yml +4 -2
- data/Gemfile +7 -0
- data/Gemfile.lock +27 -83
- data/README.md +69 -192
- data/Rakefile +3 -18
- data/benchmark.rb +31 -0
- data/lib/pendragon.rb +60 -44
- data/lib/pendragon/constants.rb +27 -0
- data/lib/pendragon/errors.rb +72 -0
- data/lib/pendragon/linear.rb +9 -0
- data/lib/pendragon/realism.rb +49 -0
- data/lib/pendragon/router.rb +295 -142
- data/lib/pendragon/version.rb +1 -1
- data/pendragon.gemspec +1 -6
- data/test/helper.rb +9 -84
- data/test/router/test_linear.rb +6 -0
- data/test/router/test_realism.rb +6 -0
- data/test/supports/shared_examples_for_routing.rb +277 -0
- data/test/test_router.rb +40 -0
- metadata +22 -101
- data/lib/pendragon/configuration.rb +0 -42
- data/lib/pendragon/engine/compiler.rb +0 -74
- data/lib/pendragon/engine/recognizer.rb +0 -72
- data/lib/pendragon/error.rb +0 -71
- data/lib/pendragon/matcher.rb +0 -93
- data/lib/pendragon/padrino.rb +0 -15
- data/lib/pendragon/padrino/ext/class_methods.rb +0 -318
- data/lib/pendragon/padrino/ext/instance_methods.rb +0 -63
- data/lib/pendragon/padrino/route.rb +0 -50
- data/lib/pendragon/padrino/router.rb +0 -34
- data/lib/pendragon/route.rb +0 -124
- data/test/compile_helper.rb +0 -3
- data/test/padrino_test.rb +0 -2113
- data/test/pendragon_configuration.rb +0 -32
- data/test/pendragon_test.rb +0 -229
data/Rakefile
CHANGED
@@ -2,24 +2,9 @@ require 'rake'
|
|
2
2
|
require 'rake/testtask'
|
3
3
|
require 'pendragon'
|
4
4
|
|
5
|
-
Rake::TestTask.new(:
|
5
|
+
Rake::TestTask.new(:test) do |test|
|
6
6
|
test.libs << 'test'
|
7
|
-
test.test_files = Dir['test
|
8
|
-
test.verbose = true
|
7
|
+
test.test_files = Dir['test/**/test_*.rb']
|
9
8
|
end
|
10
9
|
|
11
|
-
|
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
|
data/benchmark.rb
ADDED
@@ -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
|
data/lib/pendragon.rb
CHANGED
@@ -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
|
-
#
|
6
|
-
|
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
|
-
|
9
|
-
|
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
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
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
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
end
|
50
|
-
|
51
|
-
|
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
|
-
|
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,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
|
data/lib/pendragon/router.rb
CHANGED
@@ -1,188 +1,341 @@
|
|
1
|
-
require 'pendragon/
|
2
|
-
require 'pendragon/
|
3
|
-
require '
|
4
|
-
require '
|
5
|
-
require '
|
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
|
-
#
|
22
|
-
attr_accessor :
|
9
|
+
# @!visibility private
|
10
|
+
attr_accessor :prefix
|
23
11
|
|
24
|
-
#
|
25
|
-
|
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
|
-
#
|
28
|
-
#
|
29
|
+
# Adds event listener in router class.
|
30
|
+
#
|
31
|
+
# @example
|
32
|
+
# require 'pendragon'
|
29
33
|
#
|
30
|
-
#
|
31
|
-
#
|
32
|
-
#
|
33
|
-
#
|
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
|
-
# @
|
37
|
-
#
|
38
|
-
#
|
39
|
-
#
|
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
|
-
|
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
|
-
#
|
53
|
-
#
|
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
|
-
|
56
|
-
|
57
|
-
|
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
|
-
#
|
64
|
-
# @
|
65
|
-
|
66
|
-
|
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
|
-
#
|
76
|
-
#
|
77
|
-
|
78
|
-
|
79
|
-
|
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
|
-
#
|
85
|
-
# @
|
86
|
-
def
|
87
|
-
|
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
|
-
#
|
93
|
-
|
94
|
-
|
95
|
-
|
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
|
-
#
|
100
|
-
#
|
101
|
-
def
|
102
|
-
|
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
|
-
#
|
108
|
-
|
109
|
-
|
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
|
-
#
|
113
|
-
|
114
|
-
|
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
|
-
#
|
118
|
-
# @
|
119
|
-
|
120
|
-
|
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
|
-
#
|
126
|
-
# @
|
127
|
-
|
128
|
-
|
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
|
-
|
134
|
-
|
135
|
-
#
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
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
|
-
#
|
147
|
-
|
148
|
-
|
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
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
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
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
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
|
-
|
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
|