derailleur 0.0.5
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
|