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