halcyon 0.3.7 → 0.3.18
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 +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:
|