derailleur 0.0.5

Sign up to get free protection for your applications and to get access to all the features.
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,9 @@
1
+
2
+ module Derailleur
3
+ VERSION = "0.0.5"
4
+ AUTHORS = ['crapooze']
5
+ WEBSITE = "http://github.com/crapooze/derailleur"
6
+ LICENCE = "MIT"
7
+ autoload :Application, 'derailleur/base/application'
8
+ autoload :Grease, 'derailleur/base/grease'
9
+ end
@@ -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
+