halcyon 0.3.7
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 +135 -0
- data/bin/halcyon +152 -0
- data/lib/halcyon.rb +54 -0
- data/lib/halcyon/client.rb +43 -0
- data/lib/halcyon/client/base.rb +236 -0
- data/lib/halcyon/client/exceptions.rb +53 -0
- data/lib/halcyon/client/router.rb +106 -0
- data/lib/halcyon/exceptions.rb +19 -0
- data/lib/halcyon/server.rb +55 -0
- data/lib/halcyon/server/base.rb +392 -0
- data/lib/halcyon/server/exceptions.rb +53 -0
- data/lib/halcyon/server/router.rb +100 -0
- data/lib/halcyon/support/hashext.rb +59 -0
- metadata +104 -0
@@ -0,0 +1,53 @@
|
|
1
|
+
#--
|
2
|
+
# Created by Matt Todd on 2007-12-14.
|
3
|
+
# Copyright (c) 2007. All rights reserved.
|
4
|
+
#++
|
5
|
+
|
6
|
+
#--
|
7
|
+
# module
|
8
|
+
#++
|
9
|
+
|
10
|
+
module Halcyon
|
11
|
+
class Client
|
12
|
+
class Base
|
13
|
+
module Exceptions #:nodoc:
|
14
|
+
|
15
|
+
#--
|
16
|
+
# Base Halcyon Exception
|
17
|
+
#++
|
18
|
+
|
19
|
+
class Base < Exception #:nodoc:
|
20
|
+
attr_accessor :status, :error
|
21
|
+
def initialize(status, error)
|
22
|
+
@status = status
|
23
|
+
@error = error
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
#--
|
28
|
+
# Exception classes
|
29
|
+
#++
|
30
|
+
|
31
|
+
Halcyon::Exceptions::HTTP_ERROR_CODES.to_a.each do |http_error|
|
32
|
+
status, body = http_error
|
33
|
+
class_eval(
|
34
|
+
"class #{body.gsub(/ /,'')} < Base\n"+
|
35
|
+
" def initialize(s=#{status}, e='#{body}')\n"+
|
36
|
+
" super s, e\n"+
|
37
|
+
" end\n"+
|
38
|
+
"end"
|
39
|
+
);
|
40
|
+
end
|
41
|
+
|
42
|
+
#--
|
43
|
+
# Exception Lookup
|
44
|
+
#++
|
45
|
+
|
46
|
+
def self.lookup(status)
|
47
|
+
self.const_get(Halcyon::Exceptions::HTTP_ERROR_CODES[status].gsub(/ /,'').to_sym)
|
48
|
+
end
|
49
|
+
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,106 @@
|
|
1
|
+
#--
|
2
|
+
# Created by Matt Todd on 2007-12-14.
|
3
|
+
# Copyright (c) 2007. All rights reserved.
|
4
|
+
#++
|
5
|
+
|
6
|
+
#--
|
7
|
+
# module
|
8
|
+
#++
|
9
|
+
|
10
|
+
module Halcyon
|
11
|
+
class Client
|
12
|
+
|
13
|
+
# = Reverse Routing
|
14
|
+
#
|
15
|
+
# Handles URL generation from route params and action names to complement
|
16
|
+
# the routing ability in the Server.
|
17
|
+
#
|
18
|
+
# == Usage
|
19
|
+
#
|
20
|
+
# class Simple < Halcyon::Client::Base
|
21
|
+
# route do |r|
|
22
|
+
# r.match('/path/to/match').to(:action => 'do_stuff')
|
23
|
+
# {:action => 'not_found'} # the default route
|
24
|
+
# end
|
25
|
+
# def greet(name)
|
26
|
+
# get(url_for(__method__, :name => name))
|
27
|
+
# end
|
28
|
+
# end
|
29
|
+
#
|
30
|
+
# == Default Routes
|
31
|
+
#
|
32
|
+
# The default route is selected if and only if no other routes matched the
|
33
|
+
# action and params supplied as a fallback query to supply. This should
|
34
|
+
# generate an error in most cases, unless you plan to handle this exception
|
35
|
+
# specifically.
|
36
|
+
class Router
|
37
|
+
|
38
|
+
# Retrieves the last value from the +route+ call in Halcyon::Client::Base
|
39
|
+
# and, if it's a Hash, sets it to +@@default_route+ to designate the
|
40
|
+
# failover route. If +route+ is not a Hash, though, the internal default
|
41
|
+
# should be used instead (as the last returned value is probably a Route
|
42
|
+
# object returned by the +r.match().to()+ call).
|
43
|
+
#
|
44
|
+
# Used exclusively internally.
|
45
|
+
def self.default_to route
|
46
|
+
@@default_route = route.is_a?(Hash) ? route : {:action => 'not_found'}
|
47
|
+
end
|
48
|
+
|
49
|
+
# This method performs the param matching and URL generation based on the
|
50
|
+
# inputs from the +url_for+ method. (Caution: not for the feint hearted.)
|
51
|
+
def self.route(action, params)
|
52
|
+
r = nil
|
53
|
+
@@routes.each do |r|
|
54
|
+
path, pars = r
|
55
|
+
if pars[:action] == action
|
56
|
+
# if the actions match up (a pretty good sign of success), make sure the params match up
|
57
|
+
if (!pars.empty? && !params.empty? && (/(:#{params.keys.first})/ =~ path).nil?) ||
|
58
|
+
((pars.empty? && !params.empty?) || (!pars.empty? && params.empty?))
|
59
|
+
r = nil
|
60
|
+
next
|
61
|
+
else
|
62
|
+
break
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
# make sure a route is returned even if no match is found
|
68
|
+
if r.nil?
|
69
|
+
#return default route
|
70
|
+
@@default_route
|
71
|
+
else
|
72
|
+
# params (including action and module if set) for the matching route
|
73
|
+
path = r[0].dup
|
74
|
+
# replace all params with the proper placeholder in the path
|
75
|
+
params.each{|p| path.gsub!(/:#{p[0]}/, p[1]) }
|
76
|
+
path
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
#--
|
81
|
+
# Route building methods
|
82
|
+
#++
|
83
|
+
|
84
|
+
# Sets up the +@@routes+ hash and begins the processing by yielding to the block.
|
85
|
+
def self.prepare
|
86
|
+
@@path = nil
|
87
|
+
@@routes = {}
|
88
|
+
yield self if block_given?
|
89
|
+
end
|
90
|
+
|
91
|
+
# Stores the path temporarily in order to put it in the hash table.
|
92
|
+
def self.match(path)
|
93
|
+
@@path = path
|
94
|
+
self
|
95
|
+
end
|
96
|
+
|
97
|
+
# Adds the final route to the hash table and clears the temporary value.
|
98
|
+
def self.to(params={})
|
99
|
+
@@routes[@@path] = params
|
100
|
+
@@path = nil
|
101
|
+
self
|
102
|
+
end
|
103
|
+
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
#--
|
2
|
+
# Created by Matt Todd on 2007-12-14.
|
3
|
+
# Copyright (c) 2007. All rights reserved.
|
4
|
+
#++
|
5
|
+
|
6
|
+
#--
|
7
|
+
# module
|
8
|
+
#++
|
9
|
+
|
10
|
+
module Halcyon
|
11
|
+
module Exceptions #:nodoc:
|
12
|
+
HTTP_ERROR_CODES = {
|
13
|
+
403 => "Forbidden",
|
14
|
+
404 => "Not Found",
|
15
|
+
406 => "Not Acceptable",
|
16
|
+
415 => "Unsupported Media Type"
|
17
|
+
}
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
#--
|
2
|
+
# Created by Matt Todd on 2007-12-14.
|
3
|
+
# Copyright (c) 2007. All rights reserved.
|
4
|
+
#++
|
5
|
+
|
6
|
+
$:.unshift File.dirname(File.join('..', __FILE__))
|
7
|
+
$:.unshift File.dirname(__FILE__)
|
8
|
+
|
9
|
+
#--
|
10
|
+
# dependencies
|
11
|
+
#++
|
12
|
+
|
13
|
+
%w(halcyon rubygems rack json).each {|dep|require dep}
|
14
|
+
|
15
|
+
#--
|
16
|
+
# module
|
17
|
+
#++
|
18
|
+
|
19
|
+
module Halcyon
|
20
|
+
|
21
|
+
# = Server Communication and Protocol
|
22
|
+
#
|
23
|
+
# Server tries to comply with appropriate HTTP response codes, as found at
|
24
|
+
# <http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html>. However, all
|
25
|
+
# responses are JSON encoded as the server expects a JSON parser on the
|
26
|
+
# client side since the server should not be processing requests directly
|
27
|
+
# through the browser. The server expects the User-Agent to be one of:
|
28
|
+
# +"User-Agent" => "JSON/1.1.1 Compatible (en-US) Halcyon/0.0.12 Client/0.0.1"+
|
29
|
+
# +"User-Agent" => "JSON/1.1.1 Compatible"+
|
30
|
+
# The server also expects to accept application/json and be originated
|
31
|
+
# from the local host (though this can be overridden).
|
32
|
+
#
|
33
|
+
# = Usage
|
34
|
+
#
|
35
|
+
# For documentation on using Halcyon, check out the Halcyon::Server::Base and
|
36
|
+
# Halcyon::Client::Base classes which contain much more usage documentation.
|
37
|
+
class Server
|
38
|
+
VERSION.replace [0,3,7]
|
39
|
+
def self.version
|
40
|
+
VERSION.join('.')
|
41
|
+
end
|
42
|
+
|
43
|
+
#--
|
44
|
+
# module dependencies
|
45
|
+
#++
|
46
|
+
|
47
|
+
autoload :Base, 'halcyon/server/base'
|
48
|
+
autoload :Exceptions, 'halcyon/server/exceptions'
|
49
|
+
autoload :Router, 'halcyon/server/router'
|
50
|
+
|
51
|
+
end
|
52
|
+
|
53
|
+
end
|
54
|
+
|
55
|
+
%w(server/exceptions).each {|dep|require dep}
|
@@ -0,0 +1,392 @@
|
|
1
|
+
#--
|
2
|
+
# Created by Matt Todd on 2007-12-14.
|
3
|
+
# Copyright (c) 2007. All rights reserved.
|
4
|
+
#++
|
5
|
+
|
6
|
+
#--
|
7
|
+
# dependencies
|
8
|
+
#++
|
9
|
+
|
10
|
+
%w(logger json).each {|dep|require dep}
|
11
|
+
|
12
|
+
#--
|
13
|
+
# module
|
14
|
+
#++
|
15
|
+
|
16
|
+
module Halcyon
|
17
|
+
class Server
|
18
|
+
|
19
|
+
DEFAULT_OPTIONS = {}
|
20
|
+
ACCEPTABLE_REQUESTS = [
|
21
|
+
["HTTP_USER_AGENT", /JSON\/1\.1\.\d+ Compatible( \(en-US\) Halcyon\/(\d+\.\d+\.\d+) Client\/(\d+\.\d+\.\d+))?/, 406, 'Not Acceptable'],
|
22
|
+
["CONTENT_TYPE", /application\/json/, 415, 'Unsupported Media Type']
|
23
|
+
]
|
24
|
+
ACCEPTABLE_REMOTES = ['localhost', '127.0.0.1']
|
25
|
+
|
26
|
+
# = Building Halcyon Server Apps
|
27
|
+
#
|
28
|
+
# Halcyon apps are actually little servers running on top of Rack instances
|
29
|
+
# which affords a great deal of simplicity and quickness to both design and
|
30
|
+
# performance.
|
31
|
+
#
|
32
|
+
# Building a Halcyon app consists of defining routes to map all requests
|
33
|
+
# against in order to designate what functionality handles what specific
|
34
|
+
# requests, the actual actions (and modules) to actually perform these
|
35
|
+
# requests, and any extensions or configurations you may need or want for
|
36
|
+
# your individual needs.
|
37
|
+
#
|
38
|
+
# == Inheriting from Halcyon::Server::Base
|
39
|
+
#
|
40
|
+
# To begin with, an application would be started by simply defining a class
|
41
|
+
# that inherits from Halcyon::Server::Base.
|
42
|
+
#
|
43
|
+
# class Greeter < Halcyon::Server::Base
|
44
|
+
# end
|
45
|
+
#
|
46
|
+
# Once this task has been completed, routes can be defined.
|
47
|
+
#
|
48
|
+
# class Greeter < Halcyon::Server::Base
|
49
|
+
# route do |r|
|
50
|
+
# r.match('/hello/:name').to(:action => 'greet')
|
51
|
+
# {:action => 'not_found'} # default route
|
52
|
+
# end
|
53
|
+
# end
|
54
|
+
#
|
55
|
+
# Two routes are (effectively) defined here, the first being to watch for
|
56
|
+
# all requests in the format +/hello/:name+ where the word pattern is
|
57
|
+
# stored and transmitted as the appropriate keys in the params hash.
|
58
|
+
#
|
59
|
+
# Once we've got our inputs specified, we can start to handle requests:
|
60
|
+
#
|
61
|
+
# class Greeter < Halcyon::Server::Base
|
62
|
+
# route do |r|
|
63
|
+
# r.match('/hello/:name').to(:action => 'greet')
|
64
|
+
# {:action => 'not_found'} # default route
|
65
|
+
# end
|
66
|
+
# def greet(p); {:status=>200, :body=>"Hi #{p[:name]}"}; end
|
67
|
+
# end
|
68
|
+
#
|
69
|
+
# You will notice that we only define the method +greet+ and that it
|
70
|
+
# returns a Hash object containing a +status+ code and +body+ content.
|
71
|
+
# This is the most basic way to send data, but if all you're doing is
|
72
|
+
# replying that the request was successful and you have data to return,
|
73
|
+
# the method +ok+ (an alias of +standard_response+) with the +body+ param
|
74
|
+
# as its sole parameter is sufficient.
|
75
|
+
#
|
76
|
+
#
|
77
|
+
# def greet(p); ok("Hi #{p[:name]}"); end
|
78
|
+
#
|
79
|
+
# You'll also notice that there's no method called +not_found+; this is
|
80
|
+
# because it is already defined and behaves almost exactly like the +ok+
|
81
|
+
# method. We could certainly overwrite +not_found+, but at this point it
|
82
|
+
# is not necessary.
|
83
|
+
#
|
84
|
+
# You should also realize that the second route is not defined. This is
|
85
|
+
# classified as the default route, the route to follow in the event that no
|
86
|
+
# route actually matches, so it doesn't need any of the extra path to match
|
87
|
+
# against.
|
88
|
+
#
|
89
|
+
# ==
|
90
|
+
class Base
|
91
|
+
|
92
|
+
#--
|
93
|
+
# Request Handling
|
94
|
+
#++
|
95
|
+
|
96
|
+
# = Handling Calls
|
97
|
+
#
|
98
|
+
# Receives the request, handles the route matching, runs the approriate
|
99
|
+
# action based on the route determined (or defaulted to) and finishes by
|
100
|
+
# responding to the client with the content returned.
|
101
|
+
#
|
102
|
+
# == Response and Output
|
103
|
+
#
|
104
|
+
# Halcyon responds in purely JSON format (except perhaps on sever server
|
105
|
+
# malfunctions that aren't caught or intended; read: bugs).
|
106
|
+
#
|
107
|
+
# The standard response is simply a JSON-encoded hash following this
|
108
|
+
# format:
|
109
|
+
#
|
110
|
+
# {:status => http_status_code, :body => response_body}
|
111
|
+
#
|
112
|
+
# Response body can be any object desired (as long as there is a
|
113
|
+
# +to_json+ method for it, which includes most core classes), usually
|
114
|
+
# containing a nested hash with appropriate data.
|
115
|
+
#
|
116
|
+
# DO NOT try to call +to_json+ on the +body+ contents as this will cause
|
117
|
+
# errors when trying to parse JSON.
|
118
|
+
def call(env)
|
119
|
+
@start_time = Time.now if $debug
|
120
|
+
|
121
|
+
# collect env information, create request and response objects, prep for dispatch
|
122
|
+
# puts env.inspect if $debug # request information (huge)
|
123
|
+
@env = env
|
124
|
+
@res = Rack::Response.new
|
125
|
+
@req = Rack::Request.new(env)
|
126
|
+
|
127
|
+
ACCEPTABLE_REMOTES.replace([@env["REMOTE_ADDR"]]) if $debug
|
128
|
+
|
129
|
+
# pre run hook
|
130
|
+
before_run(Time.now - @start_time) if respond_to? :before_run
|
131
|
+
|
132
|
+
# dispatch
|
133
|
+
@res.write(run(Router.route(env)).to_json)
|
134
|
+
|
135
|
+
# post run hook
|
136
|
+
after_run(Time.now - @start_time) if respond_to? :after_run
|
137
|
+
|
138
|
+
puts "Served #{env['REQUEST_URI']} in #{(Time.now - @start_time)}" if $debug
|
139
|
+
|
140
|
+
# finish request
|
141
|
+
@res.finish
|
142
|
+
end
|
143
|
+
|
144
|
+
# = Dispatching Requests
|
145
|
+
#
|
146
|
+
# Dispatches the routed request, handling module resolution and pulling
|
147
|
+
# all of the param values together for the action. This action is called
|
148
|
+
# by +call+ and should be transparent to your server app.
|
149
|
+
#
|
150
|
+
# One of the design elements of this method is that it rescues all
|
151
|
+
# Halcon-specific exceptions (defined innside of ::Base::Exceptions) so
|
152
|
+
# that a proper JSON response may be rendered by +call+.
|
153
|
+
#
|
154
|
+
# With this in mind, it is preferred that, for any errors that should
|
155
|
+
# result in a given HTTP Response code other than 200, an appropriate
|
156
|
+
# exception should be thrown which is then handled by this method's
|
157
|
+
# rescue clause.
|
158
|
+
#
|
159
|
+
# Refer to the Exceptions module to see a list of available Exceptions.
|
160
|
+
#
|
161
|
+
# == Acceptable Requests
|
162
|
+
#
|
163
|
+
# Halcyon is a very picky server when dealing with requests, requiring
|
164
|
+
# that clients match a given remote location, accepting JSON responses,
|
165
|
+
# and matching a certain User-Agent profile. Unless running in debug
|
166
|
+
# mode, Halcyon will reject all requests with a 403 Forbidden response
|
167
|
+
# if these requirements are not met.
|
168
|
+
#
|
169
|
+
# This means, while in development and testing, the debug flag must be
|
170
|
+
# enabled if you intend to perform initial tests through the browser.
|
171
|
+
#
|
172
|
+
# These restrictions may appear to be arbitrary, but it is simply a
|
173
|
+
# measure to prevent a live server running in production mode from being
|
174
|
+
# assaulted by unacceptable clients which keeps the server performing
|
175
|
+
# actual functions without concerning itself with non-acceptable clients.
|
176
|
+
#
|
177
|
+
# The requirements are defined by the Halcyon::Server constants:
|
178
|
+
# * +ACCEPTABLE_REQUESTS+: defines the necessary User-Agent and Accept
|
179
|
+
# headers the client must provide.
|
180
|
+
# * ACCEPTABLE_REMOTES: defines the acceptable remote origins of
|
181
|
+
# any request. This is primarily limited to
|
182
|
+
# only local requests, but can be changed.
|
183
|
+
#
|
184
|
+
# Halcyon servers are intended to be run behind other applications and
|
185
|
+
# primarily only speaking with other apps on the same machine, though
|
186
|
+
# your specific requirements may differ and change that.
|
187
|
+
#
|
188
|
+
# == Hooks, Callbacks, and Authentication
|
189
|
+
#
|
190
|
+
# There is no Authentication mechanism built in to Halcyon (for the time
|
191
|
+
# being), but there are hooks and callbacks for you to be able to ensure
|
192
|
+
# that requests are authenticated, etc.
|
193
|
+
#
|
194
|
+
# In order to set up a callback, simply define one of the following
|
195
|
+
# methods in your app's base class:
|
196
|
+
# * before_run
|
197
|
+
# * before_action
|
198
|
+
# * after_action
|
199
|
+
# * after_run
|
200
|
+
#
|
201
|
+
# This is the exact order in which the callbacks are performed if
|
202
|
+
# defined. Make use of these methods to monitor incoming and outgoing
|
203
|
+
# requests.
|
204
|
+
#
|
205
|
+
# It is preferred for these methods to throw Exceptions::Base exceptions
|
206
|
+
# (or one of its inheriters) instead of handling them manually.
|
207
|
+
def run(route)
|
208
|
+
# make sure the request meets our expectations
|
209
|
+
ACCEPTABLE_REQUESTS.each do |req|
|
210
|
+
raise Exceptions::Base.new(req[2], req[3]) unless @env[req[0]] =~ req[1]
|
211
|
+
end
|
212
|
+
raise Exceptions::Forbidden.new unless ACCEPTABLE_REMOTES.member? @env["REMOTE_ADDR"]
|
213
|
+
|
214
|
+
# pull params
|
215
|
+
params = route.reject{|key, val| [:action, :module].include? key}
|
216
|
+
params.merge!(query_params)
|
217
|
+
|
218
|
+
# pre call hook
|
219
|
+
before_call(route, params) if respond_to? :before_call
|
220
|
+
|
221
|
+
# handle module actions differently than non-module actions
|
222
|
+
if route[:module].nil?
|
223
|
+
# call action
|
224
|
+
res = send(route[:action], params)
|
225
|
+
else
|
226
|
+
# call module action
|
227
|
+
mod = self.dup
|
228
|
+
mod.instance_eval(&(@@modules[route[:module].to_sym]))
|
229
|
+
res = mod.send(route[:action], params)
|
230
|
+
end
|
231
|
+
|
232
|
+
# after call hook
|
233
|
+
after_call if respond_to? :after_call
|
234
|
+
|
235
|
+
res
|
236
|
+
rescue Exceptions::Base => e
|
237
|
+
# puts @env.inspect if $debug
|
238
|
+
# handles all content error exceptions
|
239
|
+
@res.status = e.status
|
240
|
+
{:status => e.status, :body => e.error}
|
241
|
+
end
|
242
|
+
|
243
|
+
#--
|
244
|
+
# Initialization and setup
|
245
|
+
#++
|
246
|
+
|
247
|
+
# Called when the Handler gets started and stores the configuration
|
248
|
+
# options used to start the server.
|
249
|
+
def initialize(options = {})
|
250
|
+
# debug mode handling
|
251
|
+
if $debug
|
252
|
+
puts "Entering debugging mode..."
|
253
|
+
@logger = Logger.new(STDOUT)
|
254
|
+
ACCEPTABLE_REQUESTS.replace([
|
255
|
+
["HTTP_USER_AGENT", /.*/, 406, 'Not Acceptable'],
|
256
|
+
["HTTP_USER_AGENT", /.*/, 415, 'Unsupported Media Type'] # content type isn't set when navigating via browser
|
257
|
+
])
|
258
|
+
end
|
259
|
+
|
260
|
+
# save configuration options
|
261
|
+
@config = DEFAULT_OPTIONS.merge(options)
|
262
|
+
|
263
|
+
# setup logging
|
264
|
+
@logger ||= Logger.new(@config[:log_file])
|
265
|
+
|
266
|
+
puts "Started. Awaiting input. Listening on #{@config[:port]}..." if $debug
|
267
|
+
end
|
268
|
+
|
269
|
+
# = Routing
|
270
|
+
#
|
271
|
+
# Halcyon expects its apps to have routes set up inside of the base class
|
272
|
+
# (the class that inherits from Halcyon::Server::Base). Routes are
|
273
|
+
# defined identically to Merb's routes (since Halcyon Router inherits all
|
274
|
+
# its functionality directly from the Merb Router).
|
275
|
+
#
|
276
|
+
# == Usage
|
277
|
+
#
|
278
|
+
# A sample Halcyon application defining and handling Routes follows:
|
279
|
+
#
|
280
|
+
# class Simple < Halcyon::Server::Base
|
281
|
+
# route do |r|
|
282
|
+
# r.match('/user/show/:id').to(:module => 'user', :action => 'show')
|
283
|
+
# r.match('/hello/:name').to(:action => 'greet')
|
284
|
+
# r.match('/').to(:action => 'index')
|
285
|
+
# {:action => 'not_found'} # default route
|
286
|
+
# end
|
287
|
+
# user do
|
288
|
+
# def show(p); ok(p[:id]); end
|
289
|
+
# end
|
290
|
+
# def greet(p); ok("Hi #{p[:name]}"); end
|
291
|
+
# def index(p); ok("..."); end
|
292
|
+
# def not_found(p); super; end
|
293
|
+
# end
|
294
|
+
#
|
295
|
+
# In this example we define numerous routes for actions and even an
|
296
|
+
# action in the 'user' module as well as handling the event that no route
|
297
|
+
# was matched (thereby passing to not_found).
|
298
|
+
#
|
299
|
+
# == Modules
|
300
|
+
#
|
301
|
+
# A module is simply a named block that whose methods get executed as if
|
302
|
+
# they were in Base but without conflicting any methods with them, very
|
303
|
+
# similar to module in Ruby. All that is required to define a module is
|
304
|
+
# something like this:
|
305
|
+
#
|
306
|
+
# admin do
|
307
|
+
# def users; ok(...); end
|
308
|
+
# end
|
309
|
+
#
|
310
|
+
# This just needs to add one directive when defining what a given route
|
311
|
+
# maps to, such as:
|
312
|
+
#
|
313
|
+
# route do |r|
|
314
|
+
# r.map('/admin/users').to(:module => 'admin', :action => 'users')
|
315
|
+
# end
|
316
|
+
#
|
317
|
+
# or, alternatively, you can just map to:
|
318
|
+
#
|
319
|
+
# r.map('/:module/:action').to()
|
320
|
+
#
|
321
|
+
# though it may be better to just explicitly state the module (for
|
322
|
+
# resolving cleanly when someone starts entering garbage that matches
|
323
|
+
# incorrectly).
|
324
|
+
#
|
325
|
+
# == More Help
|
326
|
+
#
|
327
|
+
# In addition to this, you may also find some of the documentation for
|
328
|
+
# the Router class helpful. However, since the Router is pulled directly
|
329
|
+
# from Merb, you really should look at the documentation for Merb. You
|
330
|
+
# can find the documentation on Merb's website at: http://merbivore.com/
|
331
|
+
def self.route
|
332
|
+
if block_given?
|
333
|
+
Router.prepare do |router|
|
334
|
+
Router.default_to yield(router) || {:action => 'not_found'}
|
335
|
+
end
|
336
|
+
else
|
337
|
+
abort "Halcyon::Server::Base.route expects a block to define routes."
|
338
|
+
end
|
339
|
+
end
|
340
|
+
|
341
|
+
# Registers modules internally. (This is designed in a way to prevent
|
342
|
+
# method naming collisions inside and outside of modules.)
|
343
|
+
def self.method_missing(name, *params, &proc)
|
344
|
+
@@modules ||= {}
|
345
|
+
@@modules[name] = proc
|
346
|
+
end
|
347
|
+
|
348
|
+
#--
|
349
|
+
# Properties and shortcuts
|
350
|
+
#++
|
351
|
+
|
352
|
+
# Takes +msg+ as parameter and formats it into the standard response type
|
353
|
+
# expected by an action's caller. This format is as follows:
|
354
|
+
#
|
355
|
+
# {:status => http_status_code, :body => json_encoded_body}
|
356
|
+
#
|
357
|
+
# The methods +standard_response+, +success+, and +ok+ all handle any
|
358
|
+
# textual message and puts it in the body field, defaulting to the 200
|
359
|
+
# response class status code.
|
360
|
+
def standard_response(body = 'OK')
|
361
|
+
{:status => 200, :body => body}
|
362
|
+
end
|
363
|
+
alias_method :success, :standard_response
|
364
|
+
alias_method :ok, :standard_response
|
365
|
+
|
366
|
+
# Similar to the +standard_response+ method, takes input and responds
|
367
|
+
# accordingly, which is by raising an exception (which handles formatting
|
368
|
+
# the response in the normal response hash).
|
369
|
+
def not_found(body = 'Not Found')
|
370
|
+
body = 'Not Found' if body.is_a?(Hash) && body.empty?
|
371
|
+
raise Exceptions::NotFound.new(404, body)
|
372
|
+
end
|
373
|
+
|
374
|
+
# Returns the params following the ? in a given URL as a hash
|
375
|
+
def query_params
|
376
|
+
@env['QUERY_STRING'].split(/&/).inject({}){|h,kp| k,v = kp.split(/=/); h[k] = v; h}
|
377
|
+
end
|
378
|
+
|
379
|
+
# Returns the URI requested
|
380
|
+
def uri
|
381
|
+
@env['REQUEST_URI']
|
382
|
+
end
|
383
|
+
|
384
|
+
# Returns the Request Method as a lowercase symbol
|
385
|
+
def method
|
386
|
+
@env['REQUEST_METHOD'].downcase.to_sym
|
387
|
+
end
|
388
|
+
|
389
|
+
end
|
390
|
+
|
391
|
+
end
|
392
|
+
end
|