halcyon 0.3.7 → 0.3.18
Sign up to get free protection for your applications and to get access to all the features.
- data/Rakefile +21 -8
- data/bin/halcyon +73 -27
- data/lib/halcyon/client/base.rb +22 -4
- data/lib/halcyon/client/exceptions.rb +3 -3
- data/lib/halcyon/client.rb +9 -3
- data/lib/halcyon/exceptions.rb +65 -4
- data/lib/halcyon/server/base.rb +316 -25
- data/lib/halcyon/server/exceptions.rb +3 -3
- data/lib/halcyon/server/router.rb +3 -1
- data/lib/halcyon/server.rb +11 -4
- data/lib/halcyon.rb +3 -9
- metadata +10 -10
data/Rakefile
CHANGED
@@ -4,7 +4,7 @@ $:.unshift(File.expand_path(File.join(File.dirname(__FILE__), "lib")))
|
|
4
4
|
|
5
5
|
include FileUtils
|
6
6
|
|
7
|
-
require 'halcyon'
|
7
|
+
require 'lib/halcyon'
|
8
8
|
|
9
9
|
project = {
|
10
10
|
:name => "halcyon",
|
@@ -21,9 +21,16 @@ project = {
|
|
21
21
|
--op rdoc
|
22
22
|
--line-numbers
|
23
23
|
--inline-source
|
24
|
-
--title "Halcyon\
|
25
|
-
--exclude "^(_darcs|spec|pkg)/"
|
26
|
-
]
|
24
|
+
--title "Halcyon\ Documentation"
|
25
|
+
--exclude "^(_darcs|spec|pkg|.svn)/"
|
26
|
+
],
|
27
|
+
:dependencies => {
|
28
|
+
'json_pure' => '>=1.1.1',
|
29
|
+
'rack' => '>=0.2.0',
|
30
|
+
'merb' => '>=0.4.1'
|
31
|
+
},
|
32
|
+
:requirements => 'install the json gem to get faster JSON parsing',
|
33
|
+
:ruby_version_required => '>=1.8.6'
|
27
34
|
}
|
28
35
|
|
29
36
|
BASEDIR = File.expand_path(File.dirname(__FILE__))
|
@@ -43,10 +50,11 @@ spec = Gem::Specification.new do |s|
|
|
43
50
|
s.executables = project[:bin_files]
|
44
51
|
s.bindir = "bin"
|
45
52
|
s.require_path = "lib"
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
s.
|
53
|
+
project[:dependencies].each{|dep|
|
54
|
+
s.add_dependency(dep[0], dep[1])
|
55
|
+
}
|
56
|
+
s.requirements << project[:requirements]
|
57
|
+
s.required_ruby_version = project[:ruby_version_required]
|
50
58
|
s.files = (project[:rdoc_files] + %w[Rakefile] + Dir["{spec,lib}/**/*"]).uniq
|
51
59
|
end
|
52
60
|
|
@@ -133,3 +141,8 @@ task :pushsite => [:rdoc] do
|
|
133
141
|
sh "rsync -avz doc/ mtodd@halcyon.rubyforge.org:/var/www/gforge-projects/halcyon/doc/"
|
134
142
|
sh "rsync -avz site/ mtodd@halcyon.rubyforge.org:/var/www/gforge-projects/halcyon/"
|
135
143
|
end
|
144
|
+
|
145
|
+
desc "find . -name \"*.rb\" | xargs wc -l | grep total"
|
146
|
+
task :loc do
|
147
|
+
sh "find . -name \"*.rb\" | xargs wc -l | grep total"
|
148
|
+
end
|
data/bin/halcyon
CHANGED
@@ -12,20 +12,14 @@
|
|
12
12
|
# dependencies
|
13
13
|
#++
|
14
14
|
|
15
|
-
%w(optparse).each{|dep|require dep}
|
15
|
+
%w(rubygems halcyon/server optparse).each{|dep|require dep}
|
16
16
|
|
17
17
|
#--
|
18
18
|
# default options
|
19
19
|
#++
|
20
20
|
|
21
21
|
$debug = false
|
22
|
-
options =
|
23
|
-
:environment => 'none',
|
24
|
-
:port => 9267,
|
25
|
-
:host => 'localhost',
|
26
|
-
:server => 'mongrel',
|
27
|
-
:log_file => '/tmp/halcyon.log'
|
28
|
-
}
|
22
|
+
options = Halcyon::Server::DEFAULT_OPTIONS
|
29
23
|
|
30
24
|
#--
|
31
25
|
# parse options
|
@@ -35,15 +29,18 @@ opts = OptionParser.new("", 24, ' ') do |opts|
|
|
35
29
|
opts.banner << "Halcyon, JSON Server Framework\n"
|
36
30
|
opts.banner << "http://halcyon.rubyforge.org/\n"
|
37
31
|
opts.banner << "\n"
|
38
|
-
opts.banner << "Usage: halcyon [options] appname"
|
32
|
+
opts.banner << "Usage: halcyon [options] appname\n"
|
33
|
+
opts.banner << "\n"
|
34
|
+
opts.banner << "Put -c or --config first otherwise it will overwrite higher precedence options."
|
39
35
|
|
40
36
|
opts.separator ""
|
41
37
|
opts.separator "Options:"
|
42
38
|
|
43
|
-
opts.on("-d", "--debug", "set debugging
|
39
|
+
opts.on("-d", "--debug", "set debugging flag (set $debug to true)") { $debug = true }
|
40
|
+
opts.on("-D", "--Debug", "enable verbose debugging (set $debug and $DEBUG to true)") { $debug = true; $DEBUG = true }
|
44
41
|
opts.on("-w", "--warn", "turn warnings on for your script") { $-w = true }
|
45
42
|
|
46
|
-
opts.on("-I", "--include PATH", "specify $LOAD_PATH (
|
43
|
+
opts.on("-I", "--include PATH", "specify $LOAD_PATH (multiples OK)") do |path|
|
47
44
|
$:.unshift(*path.split(":"))
|
48
45
|
end
|
49
46
|
|
@@ -51,8 +48,44 @@ opts = OptionParser.new("", 24, ' ') do |opts|
|
|
51
48
|
require library
|
52
49
|
end
|
53
50
|
|
54
|
-
opts.on("-c", "--config PATH", "configuration
|
55
|
-
|
51
|
+
opts.on("-c", "--config PATH", "load configuration (YAML) from PATH") do |conf_file|
|
52
|
+
if File.exist?(conf_file)
|
53
|
+
require 'yaml'
|
54
|
+
|
55
|
+
# load the config file
|
56
|
+
begin
|
57
|
+
conf = YAML.load_file(conf_file)
|
58
|
+
rescue Errno::EACCES
|
59
|
+
abort("Can't access #{conf_file}, try 'sudo #{$0}'")
|
60
|
+
end
|
61
|
+
|
62
|
+
# store config file path so SIGHUP and SIGUSR2 will reload the config in case it changes
|
63
|
+
options[:config_file] = conf_file
|
64
|
+
|
65
|
+
# parse config
|
66
|
+
case conf
|
67
|
+
when String
|
68
|
+
# config file given was just the commandline options
|
69
|
+
ARGV.replace(conf.split)
|
70
|
+
opts.parse! ARGV
|
71
|
+
when Hash
|
72
|
+
conf.symbolize_keys!
|
73
|
+
options = options.merge(conf)
|
74
|
+
when Array
|
75
|
+
# TODO (MT) support multiple servers (or at least specifying which
|
76
|
+
# server's configuration to load)
|
77
|
+
warn "Your configuration file is setup for multiple servers. This is not a supported feature yet."
|
78
|
+
warn "However, we've pulled the first server entry as this server's configuration."
|
79
|
+
# an array of server configurations
|
80
|
+
# default to the first entry since multiple server configurations isn't
|
81
|
+
# precisely worked out yet.
|
82
|
+
options = options.merge(conf[0])
|
83
|
+
else
|
84
|
+
abort "Config file in an unsupported format. Config files must be YAML or the commandline flags"
|
85
|
+
end
|
86
|
+
else
|
87
|
+
abort "Config file failed to load. #{conf_file} was not found. Correct the path and try again."
|
88
|
+
end
|
56
89
|
end
|
57
90
|
|
58
91
|
opts.on("-s", "--server SERVER", "serve using SERVER (default: #{options[:server]})") do |serv|
|
@@ -71,6 +104,14 @@ opts = OptionParser.new("", 24, ' ') do |opts|
|
|
71
104
|
options[:log_file] = log_file
|
72
105
|
end
|
73
106
|
|
107
|
+
opts.on("-L", "--loglevel LEVEL", "log level (default: #{options[:log_level]})") do |log_file|
|
108
|
+
options[:log_level] = log_file
|
109
|
+
end
|
110
|
+
|
111
|
+
opts.on("-P", "--pidfile PATH", "save PID to PATH (default: #{options[:pid_file]})") do |log_file|
|
112
|
+
options[:pid_file] = log_file
|
113
|
+
end
|
114
|
+
|
74
115
|
opts.on("-e", "--env ENVIRONMENT", "use ENVIRONMENT for defaults (default: #{options[:environment]})") do |env|
|
75
116
|
options[:environment] = env
|
76
117
|
end
|
@@ -96,15 +137,6 @@ end
|
|
96
137
|
abort "Halcyon needs an app to run. Try: halcyon -h" if ARGV.empty?
|
97
138
|
options[:app] = ARGV.shift
|
98
139
|
|
99
|
-
#--
|
100
|
-
# load dependencies
|
101
|
-
#++
|
102
|
-
|
103
|
-
%w(rubygems rack).each{|dep|require dep}
|
104
|
-
|
105
|
-
$:.unshift '/Users/mtodd/Sites/halcyon/trunk/lib/'
|
106
|
-
%w(halcyon/server).each {|dep|require dep}
|
107
|
-
|
108
140
|
#--
|
109
141
|
# load app
|
110
142
|
#++
|
@@ -113,15 +145,29 @@ if !File.exists?("#{options[:app]}.rb")
|
|
113
145
|
abort "Halcyon did not find the app #{options[:app]}. Check your path and try again."
|
114
146
|
end
|
115
147
|
|
116
|
-
|
117
|
-
|
148
|
+
begin
|
149
|
+
require options[:app]
|
150
|
+
app = Object.const_get(File.basename(options[:app]).capitalize.gsub(/_([a-z])/){|m|m[1].chr.capitalize})
|
151
|
+
rescue NameError => e
|
152
|
+
abort "Unable to load #{File.basename(options[:app]).capitalize.gsub(/_([a-z])/){|m|m[1].chr.capitalize}}. Please ensure your server is so named."
|
153
|
+
end
|
118
154
|
|
119
155
|
#--
|
120
156
|
# prepare server
|
121
157
|
#++
|
122
|
-
|
123
|
-
|
124
|
-
|
158
|
+
begin
|
159
|
+
server = Rack::Handler.const_get(options[:server].capitalize)
|
160
|
+
rescue NameError
|
161
|
+
servers = {
|
162
|
+
'cgi' => 'CGI',
|
163
|
+
'fastcgi' => 'FastCGI',
|
164
|
+
'lsws' => 'LSWS',
|
165
|
+
'mongrel' => 'Mongrel',
|
166
|
+
'webrick' => 'WEBrick'
|
167
|
+
}
|
168
|
+
abort "Unsupported server (missing Rack Handler). Did you mean to specify #{options[:server]}?" unless servers.key? options[:server]
|
169
|
+
server = Rack::Handler.const_get(servers[options[:server]])
|
170
|
+
end
|
125
171
|
|
126
172
|
#--
|
127
173
|
# prepare app environment
|
data/lib/halcyon/client/base.rb
CHANGED
@@ -188,7 +188,7 @@ module Halcyon
|
|
188
188
|
req = Net::HTTP::Post.new(uri)
|
189
189
|
req["Content-Type"] = CONTENT_TYPE
|
190
190
|
req["User-Agent"] = USER_AGENT
|
191
|
-
req.body = data
|
191
|
+
req.body = format_body(data)
|
192
192
|
request(req)
|
193
193
|
end
|
194
194
|
|
@@ -205,7 +205,7 @@ module Halcyon
|
|
205
205
|
req = Net::HTTP::Put.new(uri)
|
206
206
|
req["Content-Type"] = CONTENT_TYPE
|
207
207
|
req["User-Agent"] = USER_AGENT
|
208
|
-
req.body = data
|
208
|
+
req.body = format_body(data)
|
209
209
|
request(req)
|
210
210
|
end
|
211
211
|
|
@@ -215,19 +215,37 @@ module Halcyon
|
|
215
215
|
# JSON, and return it to the caller. This is a private method because the
|
216
216
|
# user/developer should be quite satisfied with the +get+, +post+, +put+,
|
217
217
|
# and +delete+ methods.
|
218
|
+
#
|
219
|
+
# == Request Failures
|
220
|
+
#
|
221
|
+
# If the server responds with any kind of failure (anything with a status
|
222
|
+
# that isn't 200), Halcyon will in turn raise the respective exception
|
223
|
+
# (defined in Halcyon::Exceptions) which all inherit from
|
224
|
+
# +Halcyon::Client::Exceptions::Base+. It is up to the client to handle
|
225
|
+
# these exceptions specifically.
|
218
226
|
def request(req)
|
219
227
|
# prepare and send HTTP request
|
220
228
|
res = Net::HTTP.start(@uri.host, @uri.port) {|http|http.request(req)}
|
229
|
+
|
230
|
+
# parse response
|
221
231
|
body = JSON.parse(res.body)
|
222
232
|
body.symbolize_keys! if body.respond_to? :symbolize_keys!
|
223
233
|
|
224
234
|
# handle non-successes
|
225
235
|
raise Halcyon::Client::Base::Exceptions.lookup(body[:status]).new unless res.kind_of? Net::HTTPSuccess
|
226
236
|
|
227
|
-
#
|
237
|
+
# return response
|
228
238
|
body
|
229
239
|
rescue Halcyon::Client::Base::Exceptions::Base => e
|
230
|
-
|
240
|
+
# log exception if logger is in place
|
241
|
+
raise
|
242
|
+
end
|
243
|
+
|
244
|
+
# Formats the data of a POST or PUT request (the body) into an acceptable
|
245
|
+
# format according to Net::HTTP for sending through as a Hash.
|
246
|
+
def format_body(data)
|
247
|
+
data = {:body => data} unless data.is_a? Hash
|
248
|
+
data.map{|key,value|"&#{key}=#{value}&"}.join
|
231
249
|
end
|
232
250
|
|
233
251
|
end
|
@@ -16,7 +16,7 @@ module Halcyon
|
|
16
16
|
# Base Halcyon Exception
|
17
17
|
#++
|
18
18
|
|
19
|
-
class Base <
|
19
|
+
class Base < StandardError #:nodoc:
|
20
20
|
attr_accessor :status, :error
|
21
21
|
def initialize(status, error)
|
22
22
|
@status = status
|
@@ -31,7 +31,7 @@ module Halcyon
|
|
31
31
|
Halcyon::Exceptions::HTTP_ERROR_CODES.to_a.each do |http_error|
|
32
32
|
status, body = http_error
|
33
33
|
class_eval(
|
34
|
-
"class #{body.gsub(/ /,'')} < Base\n"+
|
34
|
+
"class #{body.gsub(/( |\-)/,'')} < Base\n"+
|
35
35
|
" def initialize(s=#{status}, e='#{body}')\n"+
|
36
36
|
" super s, e\n"+
|
37
37
|
" end\n"+
|
@@ -44,7 +44,7 @@ module Halcyon
|
|
44
44
|
#++
|
45
45
|
|
46
46
|
def self.lookup(status)
|
47
|
-
self.const_get(Halcyon::Exceptions::HTTP_ERROR_CODES[status].gsub(/ /,'')
|
47
|
+
self.const_get(Halcyon::Exceptions::HTTP_ERROR_CODES[status].gsub(/( |\-)/,''))
|
48
48
|
end
|
49
49
|
|
50
50
|
end
|
data/lib/halcyon/client.rb
CHANGED
@@ -10,7 +10,13 @@ $:.unshift File.dirname(__FILE__)
|
|
10
10
|
# dependencies
|
11
11
|
#++
|
12
12
|
|
13
|
-
%w(
|
13
|
+
%w(rubygems halcyon).each {|dep|require dep}
|
14
|
+
begin
|
15
|
+
require 'json/ext'
|
16
|
+
rescue LoadError => e
|
17
|
+
warn 'Using the Pure Ruby JSON... install the json gem to get faster JSON parsing.'
|
18
|
+
require 'json/pure'
|
19
|
+
end
|
14
20
|
|
15
21
|
#--
|
16
22
|
# module
|
@@ -26,7 +32,6 @@ module Halcyon
|
|
26
32
|
# For documentation on using Halcyon, check out the Halcyon::Server::Base and
|
27
33
|
# Halcyon::Client::Base classes which contain much more usage documentation.
|
28
34
|
class Client
|
29
|
-
VERSION.replace [0,2,12]
|
30
35
|
def self.version
|
31
36
|
VERSION.join('.')
|
32
37
|
end
|
@@ -36,8 +41,9 @@ module Halcyon
|
|
36
41
|
#++
|
37
42
|
|
38
43
|
autoload :Base, 'halcyon/client/base'
|
39
|
-
autoload :Exceptions, 'halcyon/client/exceptions'
|
40
44
|
autoload :Router, 'halcyon/client/router'
|
41
45
|
|
42
46
|
end
|
43
47
|
end
|
48
|
+
|
49
|
+
%w(halcyon/client/exceptions).each {|dep|require dep}
|
data/lib/halcyon/exceptions.rb
CHANGED
@@ -10,10 +10,71 @@
|
|
10
10
|
module Halcyon
|
11
11
|
module Exceptions #:nodoc:
|
12
12
|
HTTP_ERROR_CODES = {
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
13
|
+
400 => 'Bad Request',
|
14
|
+
401 => 'Unauthorized',
|
15
|
+
402 => 'Payment Required',
|
16
|
+
403 => 'Forbidden',
|
17
|
+
404 => 'Not Found',
|
18
|
+
405 => 'Method Not Allowed',
|
19
|
+
406 => 'Not Acceptable',
|
20
|
+
407 => 'Proxy Authentication Required',
|
21
|
+
408 => 'Request Time-out',
|
22
|
+
409 => 'Conflict',
|
23
|
+
410 => 'Gone',
|
24
|
+
411 => 'Length Required',
|
25
|
+
412 => 'Precondition Failed',
|
26
|
+
413 => 'Request Entity Too Large',
|
27
|
+
414 => 'Request-URI Too Large',
|
28
|
+
415 => 'Unsupported Media Type',
|
29
|
+
500 => 'Internal Server Error',
|
30
|
+
501 => 'Not Implemented',
|
31
|
+
502 => 'Bad Gateway',
|
32
|
+
503 => 'Service Unavailable',
|
33
|
+
504 => 'Gateway Time-out',
|
34
|
+
505 => 'HTTP Version not supported'
|
17
35
|
}
|
18
36
|
end
|
19
37
|
end
|
38
|
+
|
39
|
+
# Taken from Rack's definition:
|
40
|
+
# http://chneukirchen.org/darcs/darcsweb.cgi?r=rack;a=plainblob;f=/lib/rack/utils.rb
|
41
|
+
#
|
42
|
+
# HTTP_STATUS_CODES = {
|
43
|
+
# 100 => 'Continue',
|
44
|
+
# 101 => 'Switching Protocols',
|
45
|
+
# 200 => 'OK',
|
46
|
+
# 201 => 'Created',
|
47
|
+
# 202 => 'Accepted',
|
48
|
+
# 203 => 'Non-Authoritative Information',
|
49
|
+
# 204 => 'No Content',
|
50
|
+
# 205 => 'Reset Content',
|
51
|
+
# 206 => 'Partial Content',
|
52
|
+
# 300 => 'Multiple Choices',
|
53
|
+
# 301 => 'Moved Permanently',
|
54
|
+
# 302 => 'Moved Temporarily',
|
55
|
+
# 303 => 'See Other',
|
56
|
+
# 304 => 'Not Modified',
|
57
|
+
# 305 => 'Use Proxy',
|
58
|
+
# 400 => 'Bad Request',
|
59
|
+
# 401 => 'Unauthorized',
|
60
|
+
# 402 => 'Payment Required',
|
61
|
+
# 403 => 'Forbidden',
|
62
|
+
# 404 => 'Not Found',
|
63
|
+
# 405 => 'Method Not Allowed',
|
64
|
+
# 406 => 'Not Acceptable',
|
65
|
+
# 407 => 'Proxy Authentication Required',
|
66
|
+
# 408 => 'Request Time-out',
|
67
|
+
# 409 => 'Conflict',
|
68
|
+
# 410 => 'Gone',
|
69
|
+
# 411 => 'Length Required',
|
70
|
+
# 412 => 'Precondition Failed',
|
71
|
+
# 413 => 'Request Entity Too Large',
|
72
|
+
# 414 => 'Request-URI Too Large',
|
73
|
+
# 415 => 'Unsupported Media Type',
|
74
|
+
# 500 => 'Internal Server Error',
|
75
|
+
# 501 => 'Not Implemented',
|
76
|
+
# 502 => 'Bad Gateway',
|
77
|
+
# 503 => 'Service Unavailable',
|
78
|
+
# 504 => 'Gateway Time-out',
|
79
|
+
# 505 => 'HTTP Version not supported'
|
80
|
+
# }
|
data/lib/halcyon/server/base.rb
CHANGED
@@ -16,12 +16,25 @@
|
|
16
16
|
module Halcyon
|
17
17
|
class Server
|
18
18
|
|
19
|
-
DEFAULT_OPTIONS = {
|
19
|
+
DEFAULT_OPTIONS = {
|
20
|
+
:environment => 'none',
|
21
|
+
:port => 9267,
|
22
|
+
:host => 'localhost',
|
23
|
+
:server => 'mongrel',
|
24
|
+
:pid_file => '/var/run/halcyon.{server}.{app}.{port}.pid',
|
25
|
+
:log_file => '/var/log/halcyon.{app}.log',
|
26
|
+
:log_level => 'info',
|
27
|
+
:log_format => proc{|s,t,p,m|"#{s} [#{t.strftime("%Y-%m-%d %H:%M:%S")}] (#{$$}) #{p} :: #{m}\n"},
|
28
|
+
# handled internally
|
29
|
+
:acceptable_requests => [],
|
30
|
+
:acceptable_remotes => []
|
31
|
+
}
|
20
32
|
ACCEPTABLE_REQUESTS = [
|
33
|
+
# ENV var to check, Regexp the value should match, the status code to return in case of failure, the message with the code
|
21
34
|
["HTTP_USER_AGENT", /JSON\/1\.1\.\d+ Compatible( \(en-US\) Halcyon\/(\d+\.\d+\.\d+) Client\/(\d+\.\d+\.\d+))?/, 406, 'Not Acceptable'],
|
22
35
|
["CONTENT_TYPE", /application\/json/, 415, 'Unsupported Media Type']
|
23
36
|
]
|
24
|
-
ACCEPTABLE_REMOTES = ['localhost', '127.0.0.1']
|
37
|
+
ACCEPTABLE_REMOTES = ['localhost', '127.0.0.1', '0.0.0.0']
|
25
38
|
|
26
39
|
# = Building Halcyon Server Apps
|
27
40
|
#
|
@@ -86,7 +99,40 @@ module Halcyon
|
|
86
99
|
# route actually matches, so it doesn't need any of the extra path to match
|
87
100
|
# against.
|
88
101
|
#
|
89
|
-
# ==
|
102
|
+
# == The Filesystem
|
103
|
+
#
|
104
|
+
# It's important to note that the +halcyon+ commandline tool expects the to
|
105
|
+
# find your server inheriting +Halcyon::Server::Base+ with the same exact
|
106
|
+
# name as its filename, though with special rules.
|
107
|
+
#
|
108
|
+
# To clarify, when your server is stored in +app_server.rb+, it expects
|
109
|
+
# that your server's class name be +AppServer+ as it capitalizes each word
|
110
|
+
# and removes all underscores, etc.
|
111
|
+
#
|
112
|
+
# Keep this in mind when naming your class and your file.
|
113
|
+
#
|
114
|
+
# NOTE: This really isn't a necessary step if you write your own deployment
|
115
|
+
# script instead of using the +halcyon+ commandline tool (as it is simply
|
116
|
+
# a convenience tool). In such, feel free to name your server however you
|
117
|
+
# prefer and the file likewise.
|
118
|
+
#
|
119
|
+
# == Running Your Server On Your Own
|
120
|
+
#
|
121
|
+
# If you're wanting to run your server without the help of the +halcyon+
|
122
|
+
# commandline tool, you will simply need to initialize the server as you
|
123
|
+
# pass it to the Rack handler of choice along with any configuration
|
124
|
+
# options you desire.
|
125
|
+
#
|
126
|
+
# The following should be enough:
|
127
|
+
#
|
128
|
+
# Rack::Handler::Mongrel.run YourAppName.new(options), :Port => 9267
|
129
|
+
#
|
130
|
+
# Of course Halcyon already handles most of your dependencies for you, so
|
131
|
+
# don't worry about requiring Rack, et al. And again, the options are not
|
132
|
+
# mandatory as the default options are certainly acceptable.
|
133
|
+
#
|
134
|
+
# NOTE: If you want to provide debugging information, just set +$debug+ to
|
135
|
+
# +true+ and you should receive all the debugging information available.
|
90
136
|
class Base
|
91
137
|
|
92
138
|
#--
|
@@ -115,8 +161,52 @@ module Halcyon
|
|
115
161
|
#
|
116
162
|
# DO NOT try to call +to_json+ on the +body+ contents as this will cause
|
117
163
|
# errors when trying to parse JSON.
|
164
|
+
#
|
165
|
+
# == Request and Response
|
166
|
+
#
|
167
|
+
# If you need access to the Request and Response, the instance variables
|
168
|
+
# +@req+ and +@res+ will be sufficient for you.
|
169
|
+
#
|
170
|
+
# If you need specific documentation for these objects, check the
|
171
|
+
# corresponding docs in the Rack documentation.
|
172
|
+
#
|
173
|
+
# == Requests and POST Data
|
174
|
+
#
|
175
|
+
# Most of your requests will have all the data it needs inside of the
|
176
|
+
# +params+ you receive for your action, but for POST and PUT requests
|
177
|
+
# (you are being RESTful, right?) you will need to retrieve your data
|
178
|
+
# from the +POST+ property of the +@req+ request. Here's how:
|
179
|
+
#
|
180
|
+
# @req.POST['key'] => "value"
|
181
|
+
#
|
182
|
+
# As you can see, keys specifically are strings and values as well. What
|
183
|
+
# this means is that your POST data that you send to the server needs to
|
184
|
+
# be careful to provide a flat Hash (if anything other than a Hash is
|
185
|
+
# passed, it is packed up into a hash similar to +{:body=>data}+) or at
|
186
|
+
# least send a complicated structure as a JSON object so that transport
|
187
|
+
# is clean. Resurrecting the object is still on your end for POST data
|
188
|
+
# (though this could change). Here's how you would reconstruct your
|
189
|
+
# special hash:
|
190
|
+
#
|
191
|
+
# value = JSON.parse(@req.POST['key'])
|
192
|
+
#
|
193
|
+
# That will take care of reconstructing your Hash.
|
194
|
+
#
|
195
|
+
# And that is essentially all you need to worry about for retreiving your
|
196
|
+
# POST contents. Sending POST contents should be documented well enough
|
197
|
+
# in Halcyon::Client::Base.
|
198
|
+
#
|
199
|
+
# == Logging
|
200
|
+
#
|
201
|
+
# Logging can be done by logging to +@logger+ when inside the scope of
|
202
|
+
# application instance (inside of your instance methods and modules).
|
203
|
+
#
|
204
|
+
# The +@env+ instance variable has been modified to include a
|
205
|
+
# +halcyon.logger+ property including the given logger. Use this for
|
206
|
+
# logging if you need to step outside of the scope of the current
|
207
|
+
# application instance (just be sure to pass @env along with you).
|
118
208
|
def call(env)
|
119
|
-
@
|
209
|
+
@time_started = Time.now
|
120
210
|
|
121
211
|
# collect env information, create request and response objects, prep for dispatch
|
122
212
|
# puts env.inspect if $debug # request information (huge)
|
@@ -124,18 +214,32 @@ module Halcyon
|
|
124
214
|
@res = Rack::Response.new
|
125
215
|
@req = Rack::Request.new(env)
|
126
216
|
|
127
|
-
|
217
|
+
# add the logger to the @env instance variable for global access if for
|
218
|
+
# some reason the environment needs to be passed outside of the
|
219
|
+
# instance
|
220
|
+
@env['halcyon.logger'] = @logger
|
221
|
+
|
222
|
+
# set the acceptable remotes to include the remote IP if debugging is enabled
|
223
|
+
@config[:acceptable_remotes] << @env["REMOTE_ADDR"] if $debug
|
128
224
|
|
129
225
|
# pre run hook
|
130
|
-
before_run(Time.now - @
|
226
|
+
before_run(Time.now - @time_started) if respond_to? :before_run
|
227
|
+
|
228
|
+
# prepare route and provide it for callers
|
229
|
+
route = Router.route(@env)
|
230
|
+
@env['halcyon.route'] = route
|
131
231
|
|
132
232
|
# dispatch
|
133
|
-
@res.write(run(
|
233
|
+
@res.write(run(route).to_json)
|
134
234
|
|
135
235
|
# post run hook
|
136
|
-
after_run(Time.now - @
|
236
|
+
after_run(Time.now - @time_started) if respond_to? :after_run
|
137
237
|
|
138
|
-
|
238
|
+
@time_finished = Time.now - @time_started
|
239
|
+
|
240
|
+
# logs access in the following format: [200] / => index (0.0029s;343.79req/s)
|
241
|
+
req_time, req_per_sec = ((@time_finished*1e4).round.to_f/1e4), (((1.0/@time_finished)*1e2).round.to_f/1e2)
|
242
|
+
@logger.info "[#{@res.status}] #{@env['REQUEST_URI']} => #{route[:module].to_s}#{((route[:module].nil?) ? "" : "::")}#{route[:action]} (#{req_time}s;#{req_per_sec}req/s)"
|
139
243
|
|
140
244
|
# finish request
|
141
245
|
@res.finish
|
@@ -206,10 +310,10 @@ module Halcyon
|
|
206
310
|
# (or one of its inheriters) instead of handling them manually.
|
207
311
|
def run(route)
|
208
312
|
# make sure the request meets our expectations
|
209
|
-
|
313
|
+
@config[:acceptable_requests].each do |req|
|
210
314
|
raise Exceptions::Base.new(req[2], req[3]) unless @env[req[0]] =~ req[1]
|
211
315
|
end
|
212
|
-
raise Exceptions::Forbidden.new unless
|
316
|
+
raise Exceptions::Forbidden.new unless @config[:acceptable_remotes].member? @env["REMOTE_ADDR"]
|
213
317
|
|
214
318
|
# pull params
|
215
319
|
params = route.reject{|key, val| [:action, :module].include? key}
|
@@ -234,7 +338,7 @@ module Halcyon
|
|
234
338
|
|
235
339
|
res
|
236
340
|
rescue Exceptions::Base => e
|
237
|
-
#
|
341
|
+
@logger.warn "#{uri} => #{e.error}"
|
238
342
|
# handles all content error exceptions
|
239
343
|
@res.status = e.status
|
240
344
|
{:status => e.status, :body => e.error}
|
@@ -246,24 +350,209 @@ module Halcyon
|
|
246
350
|
|
247
351
|
# Called when the Handler gets started and stores the configuration
|
248
352
|
# options used to start the server.
|
353
|
+
#
|
354
|
+
# Feel free to define initialize for your app (which is only called once
|
355
|
+
# per server instance), just be sure to call +super+.
|
356
|
+
#
|
357
|
+
# == PID File
|
358
|
+
#
|
359
|
+
# A PID file is created when the server is first initialized with the
|
360
|
+
# current process ID. Where it is located depends on the default option,
|
361
|
+
# the config file, the commandline option, and the debug status,
|
362
|
+
# increasing in precedence in that order.
|
363
|
+
#
|
364
|
+
# By default, the PID file is placed in +/var/run/+ and is named
|
365
|
+
# +halcyon.{server}.{app}.{port}.pid+ where +{server}+ is replaced by the
|
366
|
+
# running server, +{app}+ is the app name (suffixed with +#debug+ if
|
367
|
+
# running in debug mode), and +{port}+ being the server port (if there
|
368
|
+
# are multiple servers running, this helps clarify).
|
369
|
+
#
|
370
|
+
# There is an option to numerically label your server via the +{n}+
|
371
|
+
# value, but this is deprecated and will be removed soon. Using the
|
372
|
+
# +{port}+ option makes much more sense and creates much more meaning.
|
249
373
|
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
374
|
# save configuration options
|
261
375
|
@config = DEFAULT_OPTIONS.merge(options)
|
262
376
|
|
377
|
+
# apply name options to log_file and pid_file configs
|
378
|
+
apply_log_and_pid_file_name_options
|
379
|
+
|
380
|
+
# debug mode handling
|
381
|
+
enable_debugging if $debug
|
382
|
+
|
263
383
|
# setup logging
|
264
|
-
|
384
|
+
setup_logging unless $debug
|
385
|
+
|
386
|
+
# setup request filtering
|
387
|
+
setup_request_filters unless $debug
|
388
|
+
|
389
|
+
# create PID file
|
390
|
+
@pid = File.new(@config[:pid_file].gsub('{n}', server_cluster_number), "w", 0644)
|
391
|
+
@pid << "#{$$}\n"; @pid.close
|
392
|
+
|
393
|
+
# log existence and ready status
|
394
|
+
@logger.info "PID file created. PID is #{$$}."
|
395
|
+
@logger.info "Started. Awaiting connectivity. Listening on #{@config[:port]}..."
|
396
|
+
|
397
|
+
# trap signals to die (when killed by the user) gracefully
|
398
|
+
finalize = Proc.new do
|
399
|
+
@logger.info "Shutting down #{$$}."
|
400
|
+
@logger.close
|
401
|
+
File.delete(@pid.path)
|
402
|
+
exit
|
403
|
+
end
|
404
|
+
# http://en.wikipedia.org/wiki/Signal_%28computing%29
|
405
|
+
%w(INT KILL TERM QUIT HUP).each{|sig|trap(sig, finalize)}
|
265
406
|
|
266
|
-
|
407
|
+
# listen for USR1 signals and toggle debugging accordingly
|
408
|
+
trap("USR1") do
|
409
|
+
if $debug
|
410
|
+
disable_debugging
|
411
|
+
else
|
412
|
+
enable_debugging
|
413
|
+
end
|
414
|
+
end
|
415
|
+
end
|
416
|
+
|
417
|
+
# Retreives the server cluster sequence number for the PID file.
|
418
|
+
#
|
419
|
+
# This is deprecated and will be removed soon, probably for the 0.4.0
|
420
|
+
# release. Use of the +{port}+ value is much more appropriate and
|
421
|
+
# meaningful.
|
422
|
+
def server_cluster_number
|
423
|
+
# if there are no +{n}+ references in the PID file name, then simply
|
424
|
+
# return 0 as the cluster number. (This is the preferred behavior and
|
425
|
+
# this test allows the method to fail fast. +{n}+ is deprecated and
|
426
|
+
# will be removed before 0.4.0 is released.)
|
427
|
+
return 0.to_s if @config[:pid_file]['{n}'].nil?
|
428
|
+
|
429
|
+
# warn users that they're using a deprecated convention.
|
430
|
+
warn "Your PID file name contains '{n}' (#{@config[:pid_file]}). This is deprecatd and will be removed by the 0.4.0 release. Use '{port}' instead."
|
431
|
+
|
432
|
+
# counts the number of PID files already existing.
|
433
|
+
server_count = Dir[@config[:pid_file].gsub('{n}','*')].length
|
434
|
+
# since the counting starts at 0, if the file with the count exists,
|
435
|
+
# then one of the lesser number servers isn't running, so check each
|
436
|
+
# PID file until the one not running is found.
|
437
|
+
# if no files exist, then 0 will be the count, which won't exist, so
|
438
|
+
# it will be the default number.
|
439
|
+
while File.exist?(@config[:pid_file].gsub('{n}',server_count.to_s))
|
440
|
+
server_count -= 1
|
441
|
+
end
|
442
|
+
# return that number.
|
443
|
+
server_count.to_s
|
444
|
+
end
|
445
|
+
|
446
|
+
# If the server receives a SIGUSR1 signal it will toggle debugging. This
|
447
|
+
# method is used to setup logging and the request handling methods for
|
448
|
+
# debugging.
|
449
|
+
def enable_debugging
|
450
|
+
$debug = true
|
451
|
+
# setup logger to STDOUT and log entering debugging mode
|
452
|
+
@logger = Logger.new(STDOUT)
|
453
|
+
@logger.progname = "#{self.class}#debug"
|
454
|
+
@logger.level = Logger::DEBUG
|
455
|
+
@logger.formatter = @config[:log_format]
|
456
|
+
@logger.info "Entering debugging mode..."
|
457
|
+
|
458
|
+
# set the PID file name to /tmp/ unless PID file already exists
|
459
|
+
@config[:pid_file] = '/tmp/halcyon.{server}.{app}.{port}.pid' unless @pid.is_a? File
|
460
|
+
apply_log_and_pid_file_name_options # reapply for {server}, {app}, and {port} to be set
|
461
|
+
|
462
|
+
# modify acceptable request's profiles
|
463
|
+
@config[:acceptable_requests] = [
|
464
|
+
["HTTP_USER_AGENT", /.*/, 406, 'Not Acceptable'],
|
465
|
+
["HTTP_USER_AGENT", /.*/, 415, 'Unsupported Media Type'] # content type isn't set when navigating via browser
|
466
|
+
]
|
467
|
+
@logger.debug "ACCEPTABLE_REQUESTS modified to accept all User Agents (browsers)"
|
468
|
+
rescue Errno::EACCES
|
469
|
+
abort "Can't access #{@config[:pid_file]}, try 'sudo #{$0}'"
|
470
|
+
end
|
471
|
+
|
472
|
+
# Disables all of the affects of debugging mode and returns logging and
|
473
|
+
# request filtering back to normal.
|
474
|
+
#
|
475
|
+
# Refer to +enable_debugging+ for more information.
|
476
|
+
def disable_debugging
|
477
|
+
# disable logging and log leaving debugging mode
|
478
|
+
$debug = false
|
479
|
+
@logger.info "Leaving debugging mode."
|
480
|
+
|
481
|
+
# setup normal logging
|
482
|
+
setup_logging
|
483
|
+
|
484
|
+
# reenable request filtering
|
485
|
+
setup_request_filters
|
486
|
+
end
|
487
|
+
|
488
|
+
# Sets up logging based on the configuration options in +@config+, which
|
489
|
+
# is set (in order of lowest to highest precedence) in the default
|
490
|
+
# options, in the configuration file provided, on the commandline, and
|
491
|
+
# debug mode options.
|
492
|
+
#
|
493
|
+
# == Levels
|
494
|
+
#
|
495
|
+
# The accepted level values are as follows:
|
496
|
+
#
|
497
|
+
# debug
|
498
|
+
# info
|
499
|
+
# warn
|
500
|
+
# error
|
501
|
+
# fatal
|
502
|
+
# unknown
|
503
|
+
#
|
504
|
+
# These are the exact way you can refer to the logger level you'd like to
|
505
|
+
# log at from all points of option specification (listed above in order
|
506
|
+
# of ascending precedence).
|
507
|
+
#
|
508
|
+
# If a bogus value is entered, a warning will be issued and the value
|
509
|
+
# will be defaulted to 'debug'. (So don't mess up.)
|
510
|
+
def setup_logging
|
511
|
+
# get the logging level based on the name supplied
|
512
|
+
level = {
|
513
|
+
'debug' => Logger::DEBUG,
|
514
|
+
'info' => Logger::INFO,
|
515
|
+
'warn' => Logger::WARN,
|
516
|
+
'error' => Logger::ERROR,
|
517
|
+
'fatal' => Logger::FATAL,
|
518
|
+
'unknown' => Logger::UNKNOWN # wtf?
|
519
|
+
}[@config[:log_level]]
|
520
|
+
if level.nil?
|
521
|
+
warn "Logging level specified not acceptable. Defaulting to 'debug'. Check the documentation for the acceptable values."
|
522
|
+
@config[:log_level] = 'debug'
|
523
|
+
level = Logger::DEBUG
|
524
|
+
end
|
525
|
+
|
526
|
+
# setup the logger
|
527
|
+
@logger = Logger.new(@config[:log_file])
|
528
|
+
@logger.progname = self.class
|
529
|
+
@logger.level = level
|
530
|
+
@logger.formatter = @config[:log_format]
|
531
|
+
rescue Errno::EACCES
|
532
|
+
abort "Can't access #{@config[:log_file]}, try 'sudo #{$0}'"
|
533
|
+
end
|
534
|
+
|
535
|
+
# Sets up request filters based on User-Agent, Content-Type, and Remote
|
536
|
+
# IP/address values.
|
537
|
+
#
|
538
|
+
# Extracted from +initialize+ to reduce repetition.
|
539
|
+
def setup_request_filters
|
540
|
+
@config[:acceptable_requests] = ACCEPTABLE_REQUESTS
|
541
|
+
@config[:acceptable_remotes] = ACCEPTABLE_REMOTES
|
542
|
+
end
|
543
|
+
|
544
|
+
# Searches through the PID file name and the Log file name stored in the
|
545
|
+
# +@config+ variable for +{server}+, +{app}+, and +{port}+ values and
|
546
|
+
# sets them accordingly.
|
547
|
+
def apply_log_and_pid_file_name_options
|
548
|
+
# DEFAULT :pid_file => '/var/run/halcyon.{server}.{app}.{port}.pid',
|
549
|
+
@config[:pid_file].gsub!('{server}', @config[:server])
|
550
|
+
@config[:pid_file].gsub!('{port}', @config[:port].to_s)
|
551
|
+
@config[:pid_file].gsub!('{app}', File.basename(@config[:app]))
|
552
|
+
# DEFAULT :log_file => '/var/log/halcyon.{app}.log',
|
553
|
+
@config[:log_file].gsub!('{server}', @config[:server])
|
554
|
+
@config[:log_file].gsub!('{port}', @config[:port].to_s)
|
555
|
+
@config[:log_file].gsub!('{app}', File.basename(@config[:app]))
|
267
556
|
end
|
268
557
|
|
269
558
|
# = Routing
|
@@ -378,7 +667,9 @@ module Halcyon
|
|
378
667
|
|
379
668
|
# Returns the URI requested
|
380
669
|
def uri
|
381
|
-
|
670
|
+
# special parsing is done to remove the protocol, host, and port that
|
671
|
+
# some Handlers leave in there. (Fixes inconsistencies.)
|
672
|
+
URI.parse(@env['REQUEST_URI']).path
|
382
673
|
end
|
383
674
|
|
384
675
|
# Returns the Request Method as a lowercase symbol
|
@@ -16,7 +16,7 @@ module Halcyon
|
|
16
16
|
# Base Halcyon Exception
|
17
17
|
#++
|
18
18
|
|
19
|
-
class Base <
|
19
|
+
class Base < StandardError #:nodoc:
|
20
20
|
attr_accessor :status, :error
|
21
21
|
def initialize(status, error)
|
22
22
|
@status = status
|
@@ -31,7 +31,7 @@ module Halcyon
|
|
31
31
|
Halcyon::Exceptions::HTTP_ERROR_CODES.to_a.each do |http_error|
|
32
32
|
status, body = http_error
|
33
33
|
class_eval(
|
34
|
-
"class #{body.gsub(/ /,'')} < Base\n"+
|
34
|
+
"class #{body.gsub(/( |\-)/,'')} < Base\n"+
|
35
35
|
" def initialize(s=#{status}, e='#{body}')\n"+
|
36
36
|
" super s, e\n"+
|
37
37
|
" end\n"+
|
@@ -44,7 +44,7 @@ module Halcyon
|
|
44
44
|
#++
|
45
45
|
|
46
46
|
def self.lookup(status)
|
47
|
-
self.const_get(Halcyon::Exceptions::HTTP_ERROR_CODES[status].gsub(/ /,'')
|
47
|
+
self.const_get(Halcyon::Exceptions::HTTP_ERROR_CODES[status].gsub(/( |\-)/,''))
|
48
48
|
end
|
49
49
|
|
50
50
|
end
|
@@ -75,7 +75,8 @@ module Halcyon
|
|
75
75
|
# params list defined in the +to+ routing definition, opting for the
|
76
76
|
# default route if no match is made.
|
77
77
|
def self.route(env)
|
78
|
-
|
78
|
+
# pull out the path requested (WEBrick keeps the host and port and protocol in REQUEST_URI)
|
79
|
+
uri = URI.parse(env['REQUEST_URI']).path
|
79
80
|
|
80
81
|
# prepare request
|
81
82
|
path = (uri ? uri.split('?').first : '').sub(/\/+/, '/')
|
@@ -88,6 +89,7 @@ module Halcyon
|
|
88
89
|
# make sure a route is returned even if no match is found
|
89
90
|
if route[0].nil?
|
90
91
|
#return default route
|
92
|
+
env['halcyon.logger'].debug "No route found. Using default."
|
91
93
|
@@default_route
|
92
94
|
else
|
93
95
|
# params (including action and module if set) for the matching route
|
data/lib/halcyon/server.rb
CHANGED
@@ -10,7 +10,13 @@ $:.unshift File.dirname(__FILE__)
|
|
10
10
|
# dependencies
|
11
11
|
#++
|
12
12
|
|
13
|
-
%w(halcyon
|
13
|
+
%w(rubygems halcyon rack).each {|dep|require dep}
|
14
|
+
begin
|
15
|
+
require 'json/ext'
|
16
|
+
rescue LoadError => e
|
17
|
+
warn 'Using the Pure Ruby JSON... install the json gem to get faster JSON parsing.'
|
18
|
+
require 'json/pure'
|
19
|
+
end
|
14
20
|
|
15
21
|
#--
|
16
22
|
# module
|
@@ -35,7 +41,6 @@ module Halcyon
|
|
35
41
|
# For documentation on using Halcyon, check out the Halcyon::Server::Base and
|
36
42
|
# Halcyon::Client::Base classes which contain much more usage documentation.
|
37
43
|
class Server
|
38
|
-
VERSION.replace [0,3,7]
|
39
44
|
def self.version
|
40
45
|
VERSION.join('.')
|
41
46
|
end
|
@@ -45,11 +50,13 @@ module Halcyon
|
|
45
50
|
#++
|
46
51
|
|
47
52
|
autoload :Base, 'halcyon/server/base'
|
48
|
-
autoload :Exceptions, 'halcyon/server/exceptions'
|
49
53
|
autoload :Router, 'halcyon/server/router'
|
50
54
|
|
51
55
|
end
|
52
56
|
|
53
57
|
end
|
54
58
|
|
55
|
-
|
59
|
+
# Loads the Exceptions class first which sets up all the dynamically generated
|
60
|
+
# exceptions used by the system. Must occur before Base is loaded since Base
|
61
|
+
# depends on it.
|
62
|
+
%w(halcyon/server/exceptions).each {|dep|require dep}
|
data/lib/halcyon.rb
CHANGED
@@ -21,7 +21,7 @@ end
|
|
21
21
|
#++
|
22
22
|
|
23
23
|
module Halcyon
|
24
|
-
VERSION = [0,3,
|
24
|
+
VERSION = [0,3,18]
|
25
25
|
def self.version
|
26
26
|
VERSION.join('.')
|
27
27
|
end
|
@@ -42,13 +42,7 @@ module Halcyon
|
|
42
42
|
def introduction
|
43
43
|
abort "READ THE DAMNED RDOCS FOO"
|
44
44
|
end
|
45
|
-
|
46
|
-
#--
|
47
|
-
# module dependencies
|
48
|
-
#++
|
49
45
|
|
50
|
-
autoload :Exceptions, 'halcyon/exceptions'
|
51
|
-
autoload :Server, 'halcyon/server'
|
52
|
-
autoload :Client, 'halcyon/client'
|
53
|
-
|
54
46
|
end
|
47
|
+
|
48
|
+
%w(halcyon/exceptions).each {|dep|require dep}
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: halcyon
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.3.
|
4
|
+
version: 0.3.18
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Matt Todd
|
@@ -9,7 +9,7 @@ autorequire:
|
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
11
|
|
12
|
-
date: 2007-12-
|
12
|
+
date: 2007-12-31 00:00:00 -05:00
|
13
13
|
default_executable:
|
14
14
|
dependencies:
|
15
15
|
- !ruby/object:Gem::Dependency
|
@@ -22,22 +22,22 @@ dependencies:
|
|
22
22
|
version: 0.2.0
|
23
23
|
version:
|
24
24
|
- !ruby/object:Gem::Dependency
|
25
|
-
name:
|
25
|
+
name: merb
|
26
26
|
version_requirement:
|
27
27
|
version_requirements: !ruby/object:Gem::Requirement
|
28
28
|
requirements:
|
29
29
|
- - ">="
|
30
30
|
- !ruby/object:Gem::Version
|
31
|
-
version:
|
31
|
+
version: 0.4.1
|
32
32
|
version:
|
33
33
|
- !ruby/object:Gem::Dependency
|
34
|
-
name:
|
34
|
+
name: json_pure
|
35
35
|
version_requirement:
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
37
37
|
requirements:
|
38
38
|
- - ">="
|
39
39
|
- !ruby/object:Gem::Version
|
40
|
-
version:
|
40
|
+
version: 1.1.1
|
41
41
|
version:
|
42
42
|
description: A JSON App Server Framework
|
43
43
|
email: chiology@gmail.com
|
@@ -76,9 +76,9 @@ rdoc_options:
|
|
76
76
|
- --line-numbers
|
77
77
|
- --inline-source
|
78
78
|
- --title
|
79
|
-
- "\"Halcyon
|
79
|
+
- "\"Halcyon Documentation\""
|
80
80
|
- --exclude
|
81
|
-
- "\"^(_darcs|spec|pkg)/\""
|
81
|
+
- "\"^(_darcs|spec|pkg|.svn)/\""
|
82
82
|
require_paths:
|
83
83
|
- lib
|
84
84
|
required_ruby_version: !ruby/object:Gem::Requirement
|
@@ -93,8 +93,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
93
93
|
- !ruby/object:Gem::Version
|
94
94
|
version: "0"
|
95
95
|
version:
|
96
|
-
requirements:
|
97
|
-
|
96
|
+
requirements:
|
97
|
+
- install the json gem to get faster JSON parsing
|
98
98
|
rubyforge_project:
|
99
99
|
rubygems_version: 0.9.5
|
100
100
|
signing_key:
|