derailleur 0.0.5
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 +69 -0
- data/TODO +1 -0
- data/lib/derailleur.rb +9 -0
- data/lib/derailleur/base/application.rb +66 -0
- data/lib/derailleur/base/context.rb +63 -0
- data/lib/derailleur/base/grease.rb +108 -0
- data/lib/derailleur/base/handler_generator.rb +22 -0
- data/lib/derailleur/core/application.rb +200 -0
- data/lib/derailleur/core/array_trie.rb +179 -0
- data/lib/derailleur/core/dispatcher.rb +41 -0
- data/lib/derailleur/core/errors.rb +31 -0
- data/lib/derailleur/core/handler.rb +127 -0
- data/lib/derailleur/core/hash_trie.rb +112 -0
- data/lib/derailleur/core/trie.rb +172 -0
- metadata +80 -0
data/Rakefile
ADDED
@@ -0,0 +1,69 @@
|
|
1
|
+
|
2
|
+
require 'rubygems'
|
3
|
+
require 'rake/gempackagetask'
|
4
|
+
|
5
|
+
$LOAD_PATH.unshift('lib')
|
6
|
+
require 'derailleur'
|
7
|
+
|
8
|
+
spec = Gem::Specification.new do |s|
|
9
|
+
|
10
|
+
s.name = 'derailleur'
|
11
|
+
s.rubyforge_project = 'derailleur'
|
12
|
+
s.version = Derailleur::VERSION
|
13
|
+
s.author = Derailleur::AUTHORS.first
|
14
|
+
s.homepage = Derailleur::WEBSITE
|
15
|
+
s.summary = "A super-fast Rack web framework"
|
16
|
+
s.email = "crapooze@gmail.com"
|
17
|
+
s.platform = Gem::Platform::RUBY
|
18
|
+
|
19
|
+
s.files = [
|
20
|
+
'Rakefile',
|
21
|
+
'TODO',
|
22
|
+
'lib/derailleur.rb',
|
23
|
+
'lib/derailleur/base/application.rb',
|
24
|
+
'lib/derailleur/base/grease.rb',
|
25
|
+
'lib/derailleur/base/context.rb',
|
26
|
+
'lib/derailleur/base/handler_generator.rb',
|
27
|
+
'lib/derailleur/core/application.rb',
|
28
|
+
'lib/derailleur/core/array_trie.rb',
|
29
|
+
'lib/derailleur/core/dispatcher.rb',
|
30
|
+
'lib/derailleur/core/errors.rb',
|
31
|
+
'lib/derailleur/core/handler.rb',
|
32
|
+
'lib/derailleur/core/hash_trie.rb',
|
33
|
+
'lib/derailleur/core/trie.rb',
|
34
|
+
]
|
35
|
+
|
36
|
+
s.require_path = 'lib'
|
37
|
+
s.bindir = 'bin'
|
38
|
+
s.executables = []
|
39
|
+
s.has_rdoc = true
|
40
|
+
end
|
41
|
+
|
42
|
+
Rake::GemPackageTask.new(spec) do |pkg|
|
43
|
+
pkg.need_tar = true
|
44
|
+
end
|
45
|
+
|
46
|
+
task :gem => ["pkg/#{spec.name}-#{spec.version}.gem"] do
|
47
|
+
puts "generated #{spec.version}"
|
48
|
+
end
|
49
|
+
|
50
|
+
|
51
|
+
desc "run an example"
|
52
|
+
task :example, :ex, :server, :port do |t, params|
|
53
|
+
path = "./example/#{params[:ex]}.rb"
|
54
|
+
servername = params[:server] || 'thin'
|
55
|
+
port = params[:port] || '3000'
|
56
|
+
if File.file? path
|
57
|
+
require path
|
58
|
+
require 'rack'
|
59
|
+
app = Rack::Builder.new {
|
60
|
+
run ExampleApplication
|
61
|
+
}
|
62
|
+
server = Rack::Handler.get(servername)
|
63
|
+
server.run(app, :Port => port.to_i)
|
64
|
+
else
|
65
|
+
puts "no such example: #{path}
|
66
|
+
use ls example to see the possibilities"
|
67
|
+
|
68
|
+
end
|
69
|
+
end
|
data/TODO
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
* separate classes to hold handoff and grafting logic in trienodes
|
data/lib/derailleur.rb
ADDED
@@ -0,0 +1,66 @@
|
|
1
|
+
|
2
|
+
require 'derailleur/core/application'
|
3
|
+
|
4
|
+
module Derailleur
|
5
|
+
module Application
|
6
|
+
# registers a handler for the GET HTTP method
|
7
|
+
# the handler is either content OR the passed block
|
8
|
+
# if content is true and there is a block, then the handler will be the content
|
9
|
+
# params is a hash of parameters, the only parameter supported is:
|
10
|
+
# * :overwrite , if true, then you can rewrite a handler
|
11
|
+
def get(path, content=nil, params={}, &blk)
|
12
|
+
register_route(path, :GET, content, params, &blk)
|
13
|
+
end
|
14
|
+
|
15
|
+
# removes a handler for the GET HTTP method
|
16
|
+
def unget(path)
|
17
|
+
unregister_route(path, :GET)
|
18
|
+
end
|
19
|
+
|
20
|
+
# registers a handler for the HEAD HTTP method
|
21
|
+
# see get for the use of parameters
|
22
|
+
def head(path, content=nil, params={}, &blk)
|
23
|
+
register_route(path, :HEAD, content, params, &blk)
|
24
|
+
end
|
25
|
+
|
26
|
+
# removes a handler for the HEAD HTTP method
|
27
|
+
def unhead(path)
|
28
|
+
unregister_route(path, :HEAD)
|
29
|
+
end
|
30
|
+
|
31
|
+
# registers a handler for the POST HTTP method
|
32
|
+
# see get for the use of parameters
|
33
|
+
def post(path, content=nil, params={}, &blk)
|
34
|
+
register_route(path, :POST, content, params, &blk)
|
35
|
+
end
|
36
|
+
|
37
|
+
# removes a handler for the POST HTTP method
|
38
|
+
def unpost(path)
|
39
|
+
unregister_route(path, :POST)
|
40
|
+
end
|
41
|
+
|
42
|
+
# registers a handler for the PUT HTTP method
|
43
|
+
# see get for the use of parameters
|
44
|
+
def put(path, content=nil, params={}, &blk)
|
45
|
+
register_route(path, :PUT, content, params, &blk)
|
46
|
+
end
|
47
|
+
|
48
|
+
# removes a handler for the PUT HTTP method
|
49
|
+
def unput(path)
|
50
|
+
unregister_route(path, :PUT)
|
51
|
+
end
|
52
|
+
|
53
|
+
# registers a handler for the DELETE HTTP method
|
54
|
+
# see get for the use of parameters
|
55
|
+
def delete(path, content=nil, params={}, &blk)
|
56
|
+
register_route(path, :DELETE, content, params, &blk)
|
57
|
+
end
|
58
|
+
|
59
|
+
# removes a handler for the DELETE HTTP method
|
60
|
+
def undelete(path)
|
61
|
+
unregister_route(path, :DELETE)
|
62
|
+
end
|
63
|
+
|
64
|
+
#no TRACE CONNECT
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
|
2
|
+
module Derailleur
|
3
|
+
# A context is the place where we handle an incoming HTTP Request.
|
4
|
+
# much like a Rack application, it has an env and the result must
|
5
|
+
# be an array of three items: [status, headers, content]
|
6
|
+
# The content is just an instance_evaluation of a callback passed during
|
7
|
+
# initialization.
|
8
|
+
class Context
|
9
|
+
# The Rack environment
|
10
|
+
attr_reader :env
|
11
|
+
|
12
|
+
# The Derailleur context
|
13
|
+
attr_reader :ctx
|
14
|
+
|
15
|
+
# A hash representing the HTTP response's headers
|
16
|
+
attr_reader :headers
|
17
|
+
|
18
|
+
# The HTTP response's status
|
19
|
+
attr_accessor :status
|
20
|
+
|
21
|
+
# The body of the HTTP response, must comply with Rack's specification
|
22
|
+
attr_accessor :content
|
23
|
+
|
24
|
+
# The block of code that will be instance_evaled to produce the HTTP
|
25
|
+
# response's body
|
26
|
+
attr_reader :blk
|
27
|
+
|
28
|
+
def initialize(env, ctx, &blk)
|
29
|
+
@status = 200
|
30
|
+
@env = env
|
31
|
+
@ctx = ctx
|
32
|
+
@blk = blk
|
33
|
+
@content = nil
|
34
|
+
@headers = {'Content-Type' => 'text/plain'}
|
35
|
+
end
|
36
|
+
|
37
|
+
# Simply instance_evaluates the block blk in the context of self
|
38
|
+
def evaluate!
|
39
|
+
@content = instance_eval &blk
|
40
|
+
end
|
41
|
+
|
42
|
+
# The Derailleur::Application for this context
|
43
|
+
def app
|
44
|
+
ctx['derailleur']
|
45
|
+
end
|
46
|
+
|
47
|
+
# The parameters as parsed from the URL/HTTP-path chunks
|
48
|
+
def params
|
49
|
+
ctx['derailleur.params']
|
50
|
+
end
|
51
|
+
|
52
|
+
# Method that wraps the status, headers and content into an array
|
53
|
+
# for Rack specification.
|
54
|
+
def result
|
55
|
+
[status, headers, content]
|
56
|
+
end
|
57
|
+
|
58
|
+
# Returns the extension name of the HTTP path
|
59
|
+
def extname
|
60
|
+
File.extname(env['PATH_INFO'])
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,108 @@
|
|
1
|
+
|
2
|
+
require 'derailleur/base/application'
|
3
|
+
|
4
|
+
module Derailleur
|
5
|
+
autoload :HandlerGenerator, 'derailleur/base/handler_generator'
|
6
|
+
# The Grease is the module that helps you code at a high level,
|
7
|
+
# with blocks.
|
8
|
+
# Basically, including this method in a class makes the HTTP
|
9
|
+
# methods definition available as registrations.
|
10
|
+
# Then, instanciating the class, it will actually register the routes
|
11
|
+
# in the objects. Including Grease will redefine the "initialize method",
|
12
|
+
# do do not include Grease after defining it. Do the opposite.
|
13
|
+
# See also Grease#initialize_HTTP (you should call it if you don't use
|
14
|
+
# Grease's initialize method.
|
15
|
+
module Grease
|
16
|
+
include Derailleur::Application
|
17
|
+
|
18
|
+
Registration = Struct.new(:sym, :path, :handler)
|
19
|
+
|
20
|
+
def self.included(klass)
|
21
|
+
klass.extend ClassMethods
|
22
|
+
end
|
23
|
+
|
24
|
+
# Transform registration definitions into actual registrations.
|
25
|
+
def register_registrations_of(obj)
|
26
|
+
obj.registrations.each do |reg|
|
27
|
+
send(reg.sym, reg.path, reg.handler)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
# Initialiazes HTTP routes by transforming all the registration definitions
|
32
|
+
# in the class of the object into itself.
|
33
|
+
def initialize_HTTP
|
34
|
+
register_registrations_of(self.class)
|
35
|
+
end
|
36
|
+
|
37
|
+
# If there is no initializer, will create one.
|
38
|
+
# If there is one, then it will try to call super.
|
39
|
+
# As a result include this module into classes *before* defining initialize,
|
40
|
+
# or in classes in which you don't care about initialize.
|
41
|
+
def initialize(*args, &blk)
|
42
|
+
super
|
43
|
+
initialize_HTTP
|
44
|
+
end
|
45
|
+
|
46
|
+
module ClassMethods
|
47
|
+
# Copies the registrations of an object.
|
48
|
+
# no check is done on duplicate registrations.
|
49
|
+
def inherit_HTTP_method(mod)
|
50
|
+
mod.registrations.each do |reg|
|
51
|
+
registrations << reg.dup
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
# Includes a module and then copies its registrations with
|
56
|
+
# inherit_HTTP_method
|
57
|
+
def include_and_inherit_HTTP_method(mod)
|
58
|
+
include mod
|
59
|
+
inherit_HTTP_method(mod)
|
60
|
+
end
|
61
|
+
|
62
|
+
# Returns (and create if needed) the list of registrations
|
63
|
+
def registrations
|
64
|
+
@registrations ||= []
|
65
|
+
end
|
66
|
+
|
67
|
+
attr_writer :handler_generator
|
68
|
+
|
69
|
+
# The class to use to create handlers that will catch requests.
|
70
|
+
def handler_generator
|
71
|
+
@handler_generator ||= Derailleur::HandlerGenerator
|
72
|
+
end
|
73
|
+
|
74
|
+
# Sets a specification to registers a handler at the given path and HTTP
|
75
|
+
# request "GET". For the default handler_generator
|
76
|
+
# (Derailleur::HandlerGenerator), the block blk will be evaluated in a
|
77
|
+
# Derailleur::Context .
|
78
|
+
def get(path, params=nil, &blk)
|
79
|
+
registrations << Registration.new(:get, path,
|
80
|
+
handler_generator.for(params, &blk))
|
81
|
+
end
|
82
|
+
|
83
|
+
# Same as get but for HEAD
|
84
|
+
def head(path, params=nil, &blk)
|
85
|
+
registrations << Registration.new(:head, path, params,
|
86
|
+
handler_generator.for(params, &blk))
|
87
|
+
end
|
88
|
+
|
89
|
+
# Same as get but for POST
|
90
|
+
def post(path, params=nil, &blk)
|
91
|
+
registrations << Registration.new(:post, path, params,
|
92
|
+
handler_generator.for(params, &blk))
|
93
|
+
end
|
94
|
+
|
95
|
+
# Same as get but for PUT
|
96
|
+
def put(path, params=nil, &blk)
|
97
|
+
registrations << Registration.new(:put, path, params,
|
98
|
+
handler_generator.for(params, &blk))
|
99
|
+
end
|
100
|
+
|
101
|
+
# Same as get but for DELETE
|
102
|
+
def delete(path, params=nil, &blk)
|
103
|
+
registrations << Registration.new(:delete, path, params,
|
104
|
+
handler_generator.for(params, &blk))
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
|
2
|
+
require 'derailleur/core/handler'
|
3
|
+
require 'derailleur/base/context'
|
4
|
+
|
5
|
+
module Derailleur
|
6
|
+
module HandlerGenerator
|
7
|
+
# Creates a new subclass of Derailleur::Handler that will create a
|
8
|
+
# Derailleur::Context which in turn will evaluate the block.
|
9
|
+
# The dummy_params parameter is not used, but we need it because
|
10
|
+
# it is the interface for Derailleur.Grease (in case you want to use
|
11
|
+
# a custom HandlerGenerator)
|
12
|
+
def self.for(dummy_params={}, &blk)
|
13
|
+
handler = Class.new(Derailleur::Handler)
|
14
|
+
handler.send(:define_method, :to_rack_output) do
|
15
|
+
context = Context.new(env, ctx, &blk)
|
16
|
+
context.evaluate!
|
17
|
+
context.result
|
18
|
+
end
|
19
|
+
handler
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,200 @@
|
|
1
|
+
|
2
|
+
require 'derailleur/core/errors'
|
3
|
+
|
4
|
+
module Derailleur
|
5
|
+
autoload :InternalErrorHandler, 'derailleur/core/handler'
|
6
|
+
autoload :DefaultRackHandler, 'derailleur/core/handler'
|
7
|
+
autoload :ArrayTrie, 'derailleur/core/array_trie'
|
8
|
+
autoload :Dispatcher, 'derailleur/core/dispatcher'
|
9
|
+
|
10
|
+
# In Derailleur, an application is an object extending the Application
|
11
|
+
# module. It will have routes which hold handlers. By default, we use
|
12
|
+
# Derailleur's components, but you can easily modify your application to use
|
13
|
+
# custom routes' node, HTTP methods dispatcher, and handlers.
|
14
|
+
module Application
|
15
|
+
|
16
|
+
attr_writer :default_handler, :default_root_node_type, :default_dispatcher,
|
17
|
+
:default_internal_error_handler
|
18
|
+
|
19
|
+
# The default error handler ( Derailleur::InternalErrorHandler )
|
20
|
+
def default_internal_error_handler
|
21
|
+
@default_internal_error_handler ||= InternalErrorHandler
|
22
|
+
end
|
23
|
+
|
24
|
+
# The default root node type ( Derailleur::ArrayTrie )
|
25
|
+
# You could change it to ( Derailleur::HashTrie ) for better performance.
|
26
|
+
# The rule of thumb is: benchmark your application with both and pick the
|
27
|
+
# best one.
|
28
|
+
def default_root_node_type
|
29
|
+
@default_root_node_type ||= ArrayTrie
|
30
|
+
end
|
31
|
+
|
32
|
+
# The default node type (is the node_type of the default_root_node_type )
|
33
|
+
def default_node_type
|
34
|
+
default_root_node_type.node_type
|
35
|
+
end
|
36
|
+
|
37
|
+
# The default handler ( Derailleur::DefaultRackHandler )
|
38
|
+
# See Derailleur::Handler to understand how to build your own
|
39
|
+
def default_handler
|
40
|
+
@default_handler ||= DefaultRackHandler
|
41
|
+
end
|
42
|
+
|
43
|
+
# The default HTTP method dispatcher ( Derailleur::Dispatcher )
|
44
|
+
# See Derailleur::Dispatcher if you want a personal one
|
45
|
+
def default_dispatcher
|
46
|
+
@default_dispatcher ||= Dispatcher
|
47
|
+
end
|
48
|
+
|
49
|
+
# An object representing the routes. Usually, it is the root of a Trie
|
50
|
+
def routes
|
51
|
+
@routes ||= default_root_node_type.new
|
52
|
+
end
|
53
|
+
|
54
|
+
# Normalize a path by making sure it starts with '/'
|
55
|
+
def normalize(path)
|
56
|
+
File.join('/', path)
|
57
|
+
end
|
58
|
+
|
59
|
+
# Chunks a path, splitting around '/' separators
|
60
|
+
# there always is an empty name
|
61
|
+
# 'foo/bar' => ['', 'foo', 'bar']
|
62
|
+
def chunk_path(path)
|
63
|
+
normalize(path).split('/')
|
64
|
+
end
|
65
|
+
|
66
|
+
# Builds a route by appending nodes on the path.
|
67
|
+
# The implementation of nodes should create missing nodes on the path.
|
68
|
+
# See ArrayTrieNode#<< or HashTrieNode#<<
|
69
|
+
# Returns the last node for this path
|
70
|
+
def build_route(path)
|
71
|
+
current_node = routes
|
72
|
+
chunk_path(path).each do |chunk|
|
73
|
+
current_node = current_node << chunk
|
74
|
+
end
|
75
|
+
current_node
|
76
|
+
end
|
77
|
+
|
78
|
+
# Return the node corresponding to a given path.
|
79
|
+
# Will (optionally) consecutively yield all the [node, chunk_name]
|
80
|
+
# this is useful when you want to interpret the members of the path
|
81
|
+
# as a parameter.
|
82
|
+
def get_route(path)
|
83
|
+
current_node = routes
|
84
|
+
chunk_path(path).each do |chunk|
|
85
|
+
unless current_node.absorbent?
|
86
|
+
current_node = current_node.child_for_name(chunk)
|
87
|
+
raise NoSuchRoute, "no such path #{path}" unless current_node
|
88
|
+
end
|
89
|
+
yield current_node, chunk if block_given?
|
90
|
+
end
|
91
|
+
current_node
|
92
|
+
end
|
93
|
+
|
94
|
+
|
95
|
+
# Registers an handler for a given path.
|
96
|
+
# The path will be interpreted as an absolute path prefixed by '/' .
|
97
|
+
#
|
98
|
+
# Usually you will not use this method but a method from the
|
99
|
+
# base/application.rb code (with the name of the
|
100
|
+
# HTTP method: e.g. get post put)
|
101
|
+
#
|
102
|
+
# The method argument is the HTTP method name as a symbol (e.g. :GET)
|
103
|
+
# (handler || blk) is the handler set. i.e., if there's both a handler
|
104
|
+
# and a block, the block will be ignored.
|
105
|
+
#
|
106
|
+
# Params is hash of parameters, currently, the only key
|
107
|
+
# looked at is :overwrite to overwrite an handler for an
|
108
|
+
# existing path/method pair.
|
109
|
+
#
|
110
|
+
# Internally, the path will be created node by node when nodes for this
|
111
|
+
# path are missing.
|
112
|
+
# A default_dispatcher will be here to map the various HTTP methods for the
|
113
|
+
# same path to their respective handlers.
|
114
|
+
def register_route(path, method=:default, handler=nil, params={}, &blk)
|
115
|
+
if path == '*'
|
116
|
+
raise ArgumentError.new("Cannot register on #{path} because of ambiguity, in Derailleur, '*' translated'/*' would not catch the path '/' like '/foo/*' doesn't catch '/foo'")
|
117
|
+
end
|
118
|
+
handler = handler || blk
|
119
|
+
node = build_route(path)
|
120
|
+
node.content ||= default_dispatcher.new
|
121
|
+
if (params[:overwrite]) or (not node.content.has_handler?(method))
|
122
|
+
node.content.set_handler(method, handler)
|
123
|
+
else
|
124
|
+
raise RouteObjectAlreadyPresent, "could not overwrite #{method} handler at path #{path}"
|
125
|
+
end
|
126
|
+
node
|
127
|
+
end
|
128
|
+
|
129
|
+
# Removes an handler for a path/method pair.
|
130
|
+
# The path will be interpreted as an absolute path prefixed with '/'
|
131
|
+
def unregister_route(path, method=:default)
|
132
|
+
node = get_route(normalize(path))
|
133
|
+
if node.children.empty?
|
134
|
+
if node.content.no_handler?
|
135
|
+
node.prune!
|
136
|
+
else
|
137
|
+
node.content.set_handler(method, nil)
|
138
|
+
end
|
139
|
+
else
|
140
|
+
node.hand_off_to! default_node_type.new(node.name)
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
# Split a whole branch of the application at the given path, and graft the
|
145
|
+
# branch to the app in second parameter.
|
146
|
+
# This method does NOT prevents you from cancelling handlers in the second
|
147
|
+
# app if any. Because it does not check for handlers in the receiving
|
148
|
+
# branch. Use with care. See ArrayNode#graft!
|
149
|
+
def split_to!(path, app)
|
150
|
+
app_node = app.build_route(path)
|
151
|
+
|
152
|
+
split_node = get_route(normalize(path))
|
153
|
+
split_node.prune!
|
154
|
+
|
155
|
+
app_node.graft!(split_node)
|
156
|
+
end
|
157
|
+
|
158
|
+
# Method implemented to comply to the Rack specification. see
|
159
|
+
# http://rack.rubyforge.org/doc/files/SPEC.html to understand what to
|
160
|
+
# return.
|
161
|
+
#
|
162
|
+
# If everything goes right, an instance of default_handler will serve
|
163
|
+
# the request.
|
164
|
+
#
|
165
|
+
# The routing handler will be created with three params
|
166
|
+
# - the application handler contained in the dispatcher
|
167
|
+
# - the Rack env
|
168
|
+
# - a context hash with three keys:
|
169
|
+
# * 'derailleur' at self, i.e., a reference to the application
|
170
|
+
# * 'derailleur.params' with the parameters/spalt in the path
|
171
|
+
# * 'derailleur.node' the node responsible for handling this path
|
172
|
+
#
|
173
|
+
# If there is any exception during this, it will be catched and the
|
174
|
+
# default_internal_error_handler will be called.
|
175
|
+
def call(env)
|
176
|
+
begin
|
177
|
+
path = env['PATH_INFO'].sub(/\.\w+$/,'') #ignores the extension if any
|
178
|
+
ctx = {}
|
179
|
+
params = {:splat => []}
|
180
|
+
route = get_route(path) do |node, val|
|
181
|
+
if node.wildcard?
|
182
|
+
params[node.name] = val
|
183
|
+
elsif node.absorbent?
|
184
|
+
params[:splat] << val
|
185
|
+
end
|
186
|
+
end
|
187
|
+
ctx['derailleur.node'] = route
|
188
|
+
ctx['derailleur.params'] = params
|
189
|
+
ctx['derailleur'] = self
|
190
|
+
dispatcher = route.content
|
191
|
+
raise NoSuchRoute, "no dispatcher for #{path}" if dispatcher.nil?
|
192
|
+
handler = dispatcher.send(env['REQUEST_METHOD'])
|
193
|
+
raise NoSuchRoute, "no handler for valid path: #{path}" if handler.nil?
|
194
|
+
default_handler.new(handler, env, ctx).to_rack_output
|
195
|
+
rescue Exception => err
|
196
|
+
default_internal_error_handler.new(err, env, ctx).to_rack_output
|
197
|
+
end
|
198
|
+
end
|
199
|
+
end
|
200
|
+
end
|
@@ -0,0 +1,179 @@
|
|
1
|
+
|
2
|
+
require 'derailleur/core/trie'
|
3
|
+
|
4
|
+
module Derailleur
|
5
|
+
class ArrayTrieNode < TrieNode
|
6
|
+
# A fallback children, in case there is no child matching, this
|
7
|
+
# one is found.
|
8
|
+
attr_accessor :fallback_child
|
9
|
+
|
10
|
+
# An array of normal chidrens
|
11
|
+
attr_reader :normal_children
|
12
|
+
|
13
|
+
def initialize(name, parent=nil)
|
14
|
+
super
|
15
|
+
@normal_children = []
|
16
|
+
end
|
17
|
+
|
18
|
+
# List the children, including the fallback_child (in last position) if any
|
19
|
+
def children
|
20
|
+
ary = normal_children.dup
|
21
|
+
ary << fallback_child if fallback_child
|
22
|
+
ary
|
23
|
+
end
|
24
|
+
|
25
|
+
# Tries adding a new children built from the name
|
26
|
+
# * if the name is for a normal child
|
27
|
+
# * if there already is a normal_children node with this name, returns it
|
28
|
+
# * otherwise creates one
|
29
|
+
# * if the name is for a fallback child
|
30
|
+
# * if there is no fallback child, creates one
|
31
|
+
# * otherwise
|
32
|
+
# * if the wording are identical, will just return the existing one
|
33
|
+
# * if the wording are different, will raise an ArgumentError
|
34
|
+
def << name
|
35
|
+
node = child_with_exact_name(name)
|
36
|
+
if node
|
37
|
+
node
|
38
|
+
else
|
39
|
+
new_node = ArrayTrieNode.new(name, self)
|
40
|
+
ret = if new_node.normal?
|
41
|
+
normal_children << new_node
|
42
|
+
new_node
|
43
|
+
elsif fallback_child
|
44
|
+
raise ArgumentError, "there already is a fallback child in #{self}" unless fallback_child.name == new_node.name
|
45
|
+
fallback_child
|
46
|
+
else
|
47
|
+
@fallback_child = new_node
|
48
|
+
end
|
49
|
+
ret
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
# Returns the normal child with the name passed in parameter
|
54
|
+
# or nil if there is no such child
|
55
|
+
def child_with_exact_name(name)
|
56
|
+
normal_children.find{|n| n.name == name}
|
57
|
+
end
|
58
|
+
|
59
|
+
# Returns the child whose name match the parameter,
|
60
|
+
# or the fallback one, or nil if there is no such child
|
61
|
+
def child_for_name(name)
|
62
|
+
child = child_with_exact_name(name)
|
63
|
+
if child
|
64
|
+
child
|
65
|
+
else
|
66
|
+
fallback_child
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
# Like Enumerable#map, but recursively creates a new array per child
|
71
|
+
# it looks like this:
|
72
|
+
# [root, [child1, [child11, child12]], [child2]]
|
73
|
+
def tree_map(&blk)
|
74
|
+
children.inject([yield(self)]) do |trunk, child|
|
75
|
+
trunk << child.tree_map(&blk)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
# Pruning the node means cutting the tree by removing the link between this
|
80
|
+
# node and its parent. The pruned node thus becomes a root and could be
|
81
|
+
# placed somewhere else. References to this node in the former parent node
|
82
|
+
# are cleaned during the process. The process of pruning is recursive and
|
83
|
+
# stops whenever it encounters a not useless node (see useless?)
|
84
|
+
def prune!
|
85
|
+
return if root? #you cannot prune the root
|
86
|
+
if normal?
|
87
|
+
parent.normal_children.delete(self)
|
88
|
+
else
|
89
|
+
parent.fallback_child = nil
|
90
|
+
end
|
91
|
+
old_parent = parent
|
92
|
+
@parent = nil
|
93
|
+
old_parent.prune! if old_parent.useless?
|
94
|
+
end
|
95
|
+
|
96
|
+
# Hands off the role of this node to another, single, independent, simple,
|
97
|
+
# and useless node (as opposed to useful and complex ones, e.g., a complete
|
98
|
+
# branch of the tree). This is mainly useful for doing a sort of reset on
|
99
|
+
# the node's state without caring much about the content of the current
|
100
|
+
# node, but you only care about the state of the new node. You can see it
|
101
|
+
# like (or actually perform) "changing the class" of the node on the fly.
|
102
|
+
#
|
103
|
+
# To avoid weird situations, hand off must be licit:
|
104
|
+
# - the other node must be useless to avoid losing its useful content
|
105
|
+
# - the nodes must be compatible to prevent weird situations (see
|
106
|
+
# compatible_handoff?)
|
107
|
+
#
|
108
|
+
# Otherwise, an error explaining why the handoff is not licit will be
|
109
|
+
# raised.
|
110
|
+
#
|
111
|
+
# If the handoff can take place, it happens as follow:
|
112
|
+
# - the other node will copy current's children
|
113
|
+
# - this node will clear its references to its children
|
114
|
+
#
|
115
|
+
# Then, if there is a parent to current node, parent's reference will be modified
|
116
|
+
# to point to the replacing node. Finally, the other node will copy
|
117
|
+
# current's parent, and the reference to the parent will be cleared in this
|
118
|
+
# node.
|
119
|
+
def hand_off_to!(other)
|
120
|
+
verify_hand_off_to(other)
|
121
|
+
|
122
|
+
other.normal_children.replace normal_children
|
123
|
+
other.fallback_child = fallback_child
|
124
|
+
@normal_children = []
|
125
|
+
@fallback_child = nil
|
126
|
+
if parent
|
127
|
+
if normal?
|
128
|
+
parent.normal_children.delete(self)
|
129
|
+
parent.normal_children << other
|
130
|
+
else
|
131
|
+
parent.fallback_child = other
|
132
|
+
end
|
133
|
+
other.parent = parent
|
134
|
+
@parent = nil
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
# grafting another node means replacing this node in the tree by the other
|
139
|
+
# node, including its hierarchy (i.e., you can place branches). this
|
140
|
+
# process works by replacing the reference to this node by the other node
|
141
|
+
# in this parent this process also updates the other node's parent
|
142
|
+
#
|
143
|
+
# one can only graft normal node in place of normal nodes, and vice versa,
|
144
|
+
# an incompatible grafting will raise an ArgumentError
|
145
|
+
# moreover, because of the semantics, grafting to a root will raise an error
|
146
|
+
#
|
147
|
+
# see-also compatible_graft?
|
148
|
+
def graft!(other)
|
149
|
+
verify_graft(other)
|
150
|
+
|
151
|
+
other.parent = parent
|
152
|
+
if other.normal?
|
153
|
+
parent.normal_children << other
|
154
|
+
parent.normal_children.delete(self)
|
155
|
+
else
|
156
|
+
parent.fallback_child = other
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
class ArrayTrie < ArrayTrieNode
|
162
|
+
def self.node_type
|
163
|
+
ArrayTrieNode
|
164
|
+
end
|
165
|
+
|
166
|
+
def initialize
|
167
|
+
@parent = nil
|
168
|
+
@normal_children = []
|
169
|
+
end
|
170
|
+
|
171
|
+
def name
|
172
|
+
nil
|
173
|
+
end
|
174
|
+
|
175
|
+
def prune!
|
176
|
+
nil
|
177
|
+
end
|
178
|
+
end
|
179
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
|
2
|
+
module Derailleur
|
3
|
+
HTTP_METHODS = [:GET, :HEAD, :POST, :PUT, :DELETE, :default]
|
4
|
+
Dispatcher = Struct.new(*HTTP_METHODS) do
|
5
|
+
# returns the handler for an HTTP method, or, the default one
|
6
|
+
def get_handler(method)
|
7
|
+
send(method) || send(:default)
|
8
|
+
end
|
9
|
+
|
10
|
+
# returns true if a handler is set for the HTTP method in param
|
11
|
+
# it is an alternative to get_handler which would return a true value
|
12
|
+
# if there is a default handler
|
13
|
+
def has_handler?(method)
|
14
|
+
send(method) && true
|
15
|
+
end
|
16
|
+
|
17
|
+
# sets a handler for the HTTP method in param to val
|
18
|
+
def set_handler(method, val)
|
19
|
+
send("#{method}=", val)
|
20
|
+
end
|
21
|
+
|
22
|
+
# Returns an array of pairs of array with [HTTP-verbs, handler]
|
23
|
+
# an extra item is the :default handler
|
24
|
+
# NB: could also be a hash, but I'm fine with it
|
25
|
+
def handlers
|
26
|
+
HTTP_METHODS.map do |sym|
|
27
|
+
[sym, send(sym)]
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
# returns an array like handlers but restricted to the not nil values
|
32
|
+
def handlers_set
|
33
|
+
handlers.reject{|sym, handler| handler.nil?}
|
34
|
+
end
|
35
|
+
|
36
|
+
# Returns true if there is no handler set for this dispatcher
|
37
|
+
def no_handler?
|
38
|
+
handlers_set.empty?
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
|
2
|
+
module Derailleur
|
3
|
+
# Errors raising in Derailleur are subclasses of StandardError.
|
4
|
+
# Basically, they have an http_status code. This is useful for the various
|
5
|
+
# errors handlers people may write.
|
6
|
+
class ApplicationError < StandardError
|
7
|
+
def http_status
|
8
|
+
500
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
# This error is mainly for people implementing handlers wich instanciates
|
13
|
+
# other handlers. The default handler uses it if it tries to instanciate
|
14
|
+
# another handler before forwarding to it.
|
15
|
+
class InvalidHandler < ApplicationError
|
16
|
+
end
|
17
|
+
|
18
|
+
# A classical kind of not found error in HTTP.
|
19
|
+
class NoSuchRoute < ApplicationError
|
20
|
+
def http_status
|
21
|
+
404
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
# This errors shows up when you try to register twice on the same route
|
26
|
+
# (i.e., path + verb).
|
27
|
+
# Unless you modify the routing on the fly, you should not see this error
|
28
|
+
# very often.
|
29
|
+
class RouteObjectAlreadyPresent < ApplicationError
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,127 @@
|
|
1
|
+
|
2
|
+
require 'derailleur/core/errors'
|
3
|
+
|
4
|
+
module Derailleur
|
5
|
+
class Handler
|
6
|
+
attr_accessor :env, :ctx, :object
|
7
|
+
def initialize(obj=nil, env=nil, ctx=nil)
|
8
|
+
@env = env
|
9
|
+
@ctx = ctx
|
10
|
+
@object = obj
|
11
|
+
end
|
12
|
+
|
13
|
+
# Accessors, and to_rack_output for things behaving like RackHandler
|
14
|
+
module Rack
|
15
|
+
attr_accessor :status, :headers, :page
|
16
|
+
|
17
|
+
# Returns an status, headers, and page array conforming to the Rack
|
18
|
+
# specification
|
19
|
+
def to_rack_output
|
20
|
+
[status, headers, page]
|
21
|
+
end
|
22
|
+
|
23
|
+
# Sets to default values the Rack specification fields
|
24
|
+
# [200, {}, '']
|
25
|
+
def initialize_rack
|
26
|
+
@status = 200
|
27
|
+
@headers = {}
|
28
|
+
@page = ''
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
# The rack handler class instanciates the status, headers, and page to
|
34
|
+
# default values.
|
35
|
+
class RackHandler < Handler
|
36
|
+
include Handler::Rack
|
37
|
+
|
38
|
+
def initialize(obj=nil, env=nil, ctx=nil)
|
39
|
+
super
|
40
|
+
initialize_rack
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
# The default rack handler is a handler that does several default things:
|
45
|
+
# - if the object respond to to_rack_output (like, another handler)
|
46
|
+
# it will set the handler's env to the current one (the handler must also
|
47
|
+
# respond to :env= and :ctx=) and call its to_rack_output method
|
48
|
+
# - if it's a Proc, it calls the proc passing the env and ctx as
|
49
|
+
# block parameters
|
50
|
+
# - if it's a subclass of Handler, it instantiates it
|
51
|
+
# (without object, and with the same env and ctx)
|
52
|
+
class DefaultRackHandler < RackHandler
|
53
|
+
def to_rack_output
|
54
|
+
if object.respond_to? :to_rack_output
|
55
|
+
do_forward_output
|
56
|
+
else
|
57
|
+
do_non_forward_output
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def do_forward_output
|
62
|
+
object.env = env
|
63
|
+
object.ctx = ctx
|
64
|
+
object.to_rack_output
|
65
|
+
end
|
66
|
+
|
67
|
+
def do_proc_output
|
68
|
+
object.call(env, ctx)
|
69
|
+
end
|
70
|
+
|
71
|
+
def do_handler_instantiation_output
|
72
|
+
object.new(nil, env, ctx).to_rack_output
|
73
|
+
end
|
74
|
+
|
75
|
+
def do_non_forward_output
|
76
|
+
ret = case object
|
77
|
+
when Proc
|
78
|
+
do_proc_output
|
79
|
+
when Class
|
80
|
+
if object.ancestors.include? RackHandler
|
81
|
+
do_handler_instantiation_output
|
82
|
+
else
|
83
|
+
raise InvalidHandler, "invalid handler: #{object}"
|
84
|
+
end
|
85
|
+
else
|
86
|
+
raise InvalidHandler, "invalid handler: #{object}"
|
87
|
+
end
|
88
|
+
ret
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
# This handler just call the result to a Rack-like application handler
|
93
|
+
# (the handler object must conform to the Rack specification)
|
94
|
+
# In that case, the Rack application has no view on the ctx variable anymore.
|
95
|
+
class RackApplicationHandler < RackHandler
|
96
|
+
# Simply delegates the method call to the enclosed object.
|
97
|
+
def to_rack_output
|
98
|
+
object.call(env)
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
# This handler returns the error as text/plain object
|
103
|
+
# If the error respond_to http_status, then it will modify the
|
104
|
+
# HTTP status code accordingly.
|
105
|
+
class InternalErrorHandler < RackHandler
|
106
|
+
alias :err :object
|
107
|
+
|
108
|
+
def initialize(err, env, ctx)
|
109
|
+
super
|
110
|
+
if err.respond_to? :http_status
|
111
|
+
@status = err.http_status
|
112
|
+
else
|
113
|
+
@status = 500
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
def headers
|
118
|
+
{'Content-Type' => 'text/plain'}
|
119
|
+
end
|
120
|
+
|
121
|
+
def page
|
122
|
+
[err.class.name,
|
123
|
+
err.message,
|
124
|
+
err.backtrace].join("\n")
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
@@ -0,0 +1,112 @@
|
|
1
|
+
|
2
|
+
require 'derailleur/core/trie'
|
3
|
+
|
4
|
+
module Derailleur
|
5
|
+
class HashTrieNode < TrieNode
|
6
|
+
attr_reader :children_hash
|
7
|
+
|
8
|
+
def initialize(name, parent=nil)
|
9
|
+
super
|
10
|
+
@children_hash = {}
|
11
|
+
end
|
12
|
+
|
13
|
+
def children
|
14
|
+
ret = @children_hash.values
|
15
|
+
ret << @children_hash.default if @children_hash.default
|
16
|
+
ret
|
17
|
+
end
|
18
|
+
|
19
|
+
def << name
|
20
|
+
node = exact_child_for_name(name)
|
21
|
+
if node
|
22
|
+
node
|
23
|
+
else
|
24
|
+
new_node = HashTrieNode.new(name, self)
|
25
|
+
ret = if new_node.normal?
|
26
|
+
children_hash[name] = new_node
|
27
|
+
elsif children_hash.default
|
28
|
+
raise ArgumentError, "there already is a fallback child in #{self}" unless children_hash.default.name == new_node.name
|
29
|
+
children_hash.default
|
30
|
+
else
|
31
|
+
children_hash.default = new_node
|
32
|
+
end
|
33
|
+
ret
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def exact_child_for_name(name)
|
38
|
+
children_hash.values.find{|v| v.name == name}
|
39
|
+
end
|
40
|
+
|
41
|
+
def child_for_name(name)
|
42
|
+
children_hash[name]
|
43
|
+
end
|
44
|
+
|
45
|
+
def tree_map(&blk)
|
46
|
+
children_hash.values.inject([yield(self)]) do |trunk, child|
|
47
|
+
trunk << child.tree_map(&blk)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
# pruning an already pruned node will crash: it has no parent already
|
52
|
+
# also prunes recursively on useless parents
|
53
|
+
# will stop naturally on roots because they have an overloaded prune!
|
54
|
+
def prune!
|
55
|
+
return if root?
|
56
|
+
if normal?
|
57
|
+
parent.children_hash.delete(name)
|
58
|
+
else
|
59
|
+
parent.children_hash.default = nil
|
60
|
+
end
|
61
|
+
old_parent = parent
|
62
|
+
@parent = nil
|
63
|
+
old_parent.prune! if old_parent.useless?
|
64
|
+
end
|
65
|
+
|
66
|
+
def hand_off_to!(other)
|
67
|
+
verify_hand_off_to(other)
|
68
|
+
|
69
|
+
other.children_hash.replace @children_hash
|
70
|
+
@children_hash = {}
|
71
|
+
if parent
|
72
|
+
if normal?
|
73
|
+
parent.children_hash[name] = other
|
74
|
+
else
|
75
|
+
parent.children_hash.default = @children_hash.default
|
76
|
+
end
|
77
|
+
other.parent = parent
|
78
|
+
@parent = nil
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def graft!(other)
|
83
|
+
verify_graft(other)
|
84
|
+
|
85
|
+
other.parent = parent
|
86
|
+
if other.normal?
|
87
|
+
parent.children_hash[name] = other
|
88
|
+
else
|
89
|
+
parent.children_hash.default = other
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
class HashTrie < HashTrieNode
|
95
|
+
def self.node_type
|
96
|
+
HashTrieNode
|
97
|
+
end
|
98
|
+
|
99
|
+
def initialize
|
100
|
+
@parent = nil
|
101
|
+
@children_hash = {}
|
102
|
+
end
|
103
|
+
|
104
|
+
def name
|
105
|
+
nil
|
106
|
+
end
|
107
|
+
|
108
|
+
def prune!
|
109
|
+
nil
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
@@ -0,0 +1,172 @@
|
|
1
|
+
|
2
|
+
module Derailleur
|
3
|
+
class TrieNode
|
4
|
+
# The name for this node
|
5
|
+
attr_reader :name
|
6
|
+
|
7
|
+
# The children of this node
|
8
|
+
attr_reader :children
|
9
|
+
|
10
|
+
# The children of this node
|
11
|
+
attr_accessor :content
|
12
|
+
|
13
|
+
# The parent of this node, or nil if this node is a root
|
14
|
+
attr_accessor :parent
|
15
|
+
|
16
|
+
# This is the default children for the TrieNode
|
17
|
+
# (i.e., we record only the parent)
|
18
|
+
# That's why TrieNode is mainly a parent class and should be
|
19
|
+
# subclassed.
|
20
|
+
class EmptyClass
|
21
|
+
def self.empty?
|
22
|
+
true
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
# Creates a new node whose name and optional parent are set from
|
27
|
+
# the parameters.
|
28
|
+
# There is a check wether the parent is absorbent (which disallow childrens)
|
29
|
+
# The children is set to EmptyClass, which is a placeholder for subclasses
|
30
|
+
#
|
31
|
+
def initialize(name, parent=nil)
|
32
|
+
raise ArgumentError, "cannot be the child of #{parent} because it is an absorbent node" if parent and parent.absorbent?
|
33
|
+
@parent = parent
|
34
|
+
@children = EmptyClass
|
35
|
+
set_normal!
|
36
|
+
self.name = name
|
37
|
+
end
|
38
|
+
|
39
|
+
# sets the name of the node and also does other stuff with special names:
|
40
|
+
# - '*' means absorbent
|
41
|
+
# - /^:/ means wildcard
|
42
|
+
# - otherwise it's normal
|
43
|
+
def name=(val)
|
44
|
+
set_val = if val
|
45
|
+
case val
|
46
|
+
when '*'
|
47
|
+
set_absorbent!
|
48
|
+
when /^:/
|
49
|
+
set_wildcard!
|
50
|
+
else
|
51
|
+
set_normal!
|
52
|
+
end
|
53
|
+
val
|
54
|
+
else
|
55
|
+
set_normal!
|
56
|
+
nil
|
57
|
+
end
|
58
|
+
@name = set_val
|
59
|
+
end
|
60
|
+
|
61
|
+
private
|
62
|
+
|
63
|
+
# Set this node to be an absorbent one.
|
64
|
+
# In the semantics, an absorbent node means that an
|
65
|
+
# entity traversing the trie should stop on this node and should not go any
|
66
|
+
# deeper (i.e., an absorbent node is generally a leaf in the tree, although
|
67
|
+
# no checking enforces this rule).
|
68
|
+
def set_absorbent!
|
69
|
+
set_normal!
|
70
|
+
@absorbent = true
|
71
|
+
end
|
72
|
+
|
73
|
+
# Set this node to be a wildcard one. A wildcard node is a node whose name
|
74
|
+
# is an identifier local to the context of the application traversing it.
|
75
|
+
# The semantics often is "any name".
|
76
|
+
def set_wildcard!
|
77
|
+
set_normal!
|
78
|
+
@wildcard = true
|
79
|
+
end
|
80
|
+
|
81
|
+
# Set this node as normal. That is, cancel any wildcard or absorbent
|
82
|
+
# status.
|
83
|
+
def set_normal!
|
84
|
+
@wildcard = false
|
85
|
+
@absorbent = false
|
86
|
+
end
|
87
|
+
|
88
|
+
public
|
89
|
+
|
90
|
+
# A wildcard node is a node whose name was set with a trailing ':'
|
91
|
+
def wildcard?
|
92
|
+
@wildcard
|
93
|
+
end
|
94
|
+
|
95
|
+
# An absorbent node is a node which cannot be parent, its name is '*'.
|
96
|
+
def absorbent?
|
97
|
+
@absorbent
|
98
|
+
end
|
99
|
+
|
100
|
+
# A normal node is not absorbent and node wildcard
|
101
|
+
def normal?
|
102
|
+
(not wildcard?) and (not absorbent?)
|
103
|
+
end
|
104
|
+
|
105
|
+
# Compares a node with another, based on their name
|
106
|
+
def <=> other
|
107
|
+
name <=> other.name
|
108
|
+
end
|
109
|
+
|
110
|
+
# A useless node has no content and no children, basically
|
111
|
+
# arriving at this point, there's no handler you can ever find.
|
112
|
+
def useless?
|
113
|
+
content.nil? and children.empty?
|
114
|
+
end
|
115
|
+
|
116
|
+
# Returns the root of the tree structure (recursive)
|
117
|
+
def root
|
118
|
+
parent ? parent.root : self
|
119
|
+
end
|
120
|
+
|
121
|
+
# Returns wether or not this node is a root node (i.e., parent is nil)
|
122
|
+
def root?
|
123
|
+
parent.nil?
|
124
|
+
end
|
125
|
+
|
126
|
+
# Returns true wether this node could handoff its position to another one.
|
127
|
+
# It is the case if both nodes are normals or both are non-normal.
|
128
|
+
def compatible_handoff?(other)
|
129
|
+
(normal? and other.normal?) or
|
130
|
+
((not normal?) and (not other.normal?))
|
131
|
+
end
|
132
|
+
|
133
|
+
# Raise argument errors if the handoff to other is not licit.
|
134
|
+
def verify_hand_off_to(other)
|
135
|
+
raise ArgumentError, "cannot hand off to node: #{other} because it is useful" unless other.useless?
|
136
|
+
raise ArgumentError, "uncompatible handoff between #{self} and #{other}" unless compatible_handoff?(other)
|
137
|
+
end
|
138
|
+
|
139
|
+
# Placeholder for the handoff logic, this method should be overridden by
|
140
|
+
# subclasses.
|
141
|
+
def hand_off_to!(other)
|
142
|
+
verify_hand_off_to(other)
|
143
|
+
other.children.replace children
|
144
|
+
other.parent = parent
|
145
|
+
@children = EmptyClass
|
146
|
+
@parent = nil
|
147
|
+
end
|
148
|
+
|
149
|
+
# Returns true wether the other node could be grafted over this one.
|
150
|
+
# It is the case if both nodes are normals or both are non-normal.
|
151
|
+
def compatible_graft?(other)
|
152
|
+
(normal? and other.normal?) or
|
153
|
+
((not normal?) and (not other.normal?))
|
154
|
+
end
|
155
|
+
|
156
|
+
# Raise argument errors if the grafting of the other node is not licit.
|
157
|
+
def verify_graft(other)
|
158
|
+
raise ArgumentError, "incompatible grafting" unless compatible_graft?(other)
|
159
|
+
raise ArgumentError, "cannot graft a root" if root?
|
160
|
+
end
|
161
|
+
|
162
|
+
# Placeholder for the handoff logic, this method should be overridden by
|
163
|
+
# subclasses.
|
164
|
+
def graft!(other)
|
165
|
+
verify_graft(other)
|
166
|
+
other.parent = parent
|
167
|
+
parent.children.add other
|
168
|
+
parent.children.delete self
|
169
|
+
end
|
170
|
+
|
171
|
+
end
|
172
|
+
end
|
metadata
ADDED
@@ -0,0 +1,80 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: derailleur
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 21
|
5
|
+
prerelease: false
|
6
|
+
segments:
|
7
|
+
- 0
|
8
|
+
- 0
|
9
|
+
- 5
|
10
|
+
version: 0.0.5
|
11
|
+
platform: ruby
|
12
|
+
authors:
|
13
|
+
- crapooze
|
14
|
+
autorequire:
|
15
|
+
bindir: bin
|
16
|
+
cert_chain: []
|
17
|
+
|
18
|
+
date: 2011-02-12 00:00:00 +01:00
|
19
|
+
default_executable:
|
20
|
+
dependencies: []
|
21
|
+
|
22
|
+
description:
|
23
|
+
email: crapooze@gmail.com
|
24
|
+
executables: []
|
25
|
+
|
26
|
+
extensions: []
|
27
|
+
|
28
|
+
extra_rdoc_files: []
|
29
|
+
|
30
|
+
files:
|
31
|
+
- Rakefile
|
32
|
+
- TODO
|
33
|
+
- lib/derailleur.rb
|
34
|
+
- lib/derailleur/base/application.rb
|
35
|
+
- lib/derailleur/base/grease.rb
|
36
|
+
- lib/derailleur/base/context.rb
|
37
|
+
- lib/derailleur/base/handler_generator.rb
|
38
|
+
- lib/derailleur/core/application.rb
|
39
|
+
- lib/derailleur/core/array_trie.rb
|
40
|
+
- lib/derailleur/core/dispatcher.rb
|
41
|
+
- lib/derailleur/core/errors.rb
|
42
|
+
- lib/derailleur/core/handler.rb
|
43
|
+
- lib/derailleur/core/hash_trie.rb
|
44
|
+
- lib/derailleur/core/trie.rb
|
45
|
+
has_rdoc: true
|
46
|
+
homepage: http://github.com/crapooze/derailleur
|
47
|
+
licenses: []
|
48
|
+
|
49
|
+
post_install_message:
|
50
|
+
rdoc_options: []
|
51
|
+
|
52
|
+
require_paths:
|
53
|
+
- lib
|
54
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
55
|
+
none: false
|
56
|
+
requirements:
|
57
|
+
- - ">="
|
58
|
+
- !ruby/object:Gem::Version
|
59
|
+
hash: 3
|
60
|
+
segments:
|
61
|
+
- 0
|
62
|
+
version: "0"
|
63
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
64
|
+
none: false
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
hash: 3
|
69
|
+
segments:
|
70
|
+
- 0
|
71
|
+
version: "0"
|
72
|
+
requirements: []
|
73
|
+
|
74
|
+
rubyforge_project: derailleur
|
75
|
+
rubygems_version: 1.3.7
|
76
|
+
signing_key:
|
77
|
+
specification_version: 3
|
78
|
+
summary: A super-fast Rack web framework
|
79
|
+
test_files: []
|
80
|
+
|