halcyon 0.4.0 → 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- data/AUTHORS +1 -0
- data/LICENSE +20 -0
- data/README +107 -0
- data/Rakefile +8 -6
- data/bin/halcyon +3 -204
- data/lib/halcyon.rb +55 -42
- data/lib/halcyon/application.rb +247 -0
- data/lib/halcyon/application/router.rb +86 -0
- data/lib/halcyon/client.rb +187 -35
- data/lib/halcyon/client/ssl.rb +38 -0
- data/lib/halcyon/controller.rb +154 -0
- data/lib/halcyon/exceptions.rb +67 -59
- data/lib/halcyon/logging.rb +31 -0
- data/lib/halcyon/logging/analogger.rb +31 -0
- data/lib/halcyon/logging/helpers.rb +37 -0
- data/lib/halcyon/logging/log4r.rb +25 -0
- data/lib/halcyon/logging/logger.rb +20 -0
- data/lib/halcyon/logging/logging.rb +19 -0
- data/lib/halcyon/runner.rb +141 -0
- data/lib/halcyon/runner/commands.rb +141 -0
- data/lib/halcyon/runner/helpers.rb +9 -0
- data/lib/halcyon/runner/helpers/command_helper.rb +71 -0
- data/spec/halcyon/application_spec.rb +70 -0
- data/spec/halcyon/client_spec.rb +63 -0
- data/spec/halcyon/controller_spec.rb +68 -0
- data/spec/halcyon/halcyon_spec.rb +63 -0
- data/spec/halcyon/logging_spec.rb +31 -0
- data/spec/halcyon/router_spec.rb +37 -12
- data/spec/halcyon/runner_spec.rb +54 -0
- data/spec/spec_helper.rb +75 -9
- data/support/generators/halcyon/USAGE +0 -0
- data/support/generators/halcyon/halcyon_generator.rb +52 -0
- data/support/generators/halcyon/templates/README +26 -0
- data/support/generators/halcyon/templates/Rakefile +32 -0
- data/support/generators/halcyon/templates/app/application.rb +43 -0
- data/support/generators/halcyon/templates/config/config.yml +36 -0
- data/support/generators/halcyon/templates/config/init/environment.rb +11 -0
- data/support/generators/halcyon/templates/config/init/hooks.rb +39 -0
- data/support/generators/halcyon/templates/config/init/requires.rb +10 -0
- data/support/generators/halcyon/templates/config/init/routes.rb +50 -0
- data/support/generators/halcyon/templates/lib/client.rb +77 -0
- data/support/generators/halcyon/templates/runner.ru +8 -0
- data/support/generators/halcyon_flat/USAGE +0 -0
- data/support/generators/halcyon_flat/halcyon_flat_generator.rb +52 -0
- data/support/generators/halcyon_flat/templates/README +26 -0
- data/support/generators/halcyon_flat/templates/Rakefile +32 -0
- data/support/generators/halcyon_flat/templates/app.rb +49 -0
- data/support/generators/halcyon_flat/templates/lib/client.rb +17 -0
- data/support/generators/halcyon_flat/templates/runner.ru +8 -0
- metadata +73 -20
- data/lib/halcyon/client/base.rb +0 -261
- data/lib/halcyon/client/exceptions.rb +0 -41
- data/lib/halcyon/client/router.rb +0 -106
- data/lib/halcyon/server.rb +0 -62
- data/lib/halcyon/server/auth/basic.rb +0 -107
- data/lib/halcyon/server/base.rb +0 -774
- data/lib/halcyon/server/exceptions.rb +0 -41
- data/lib/halcyon/server/router.rb +0 -103
- data/spec/halcyon/error_spec.rb +0 -55
- data/spec/halcyon/server_spec.rb +0 -105
@@ -1,41 +0,0 @@
|
|
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
|
-
# Exception classes
|
17
|
-
#++
|
18
|
-
|
19
|
-
Halcyon::Exceptions::HTTP_ERROR_CODES.to_a.each do |http_error|
|
20
|
-
status, body = http_error
|
21
|
-
class_eval(
|
22
|
-
"class #{body.gsub(/( |\-)/,'')} < Halcyon::Exceptions::Base\n"+
|
23
|
-
" def initialize(s=#{status}, e='#{body}')\n"+
|
24
|
-
" super\n"+
|
25
|
-
" end\n"+
|
26
|
-
"end"
|
27
|
-
);
|
28
|
-
end
|
29
|
-
|
30
|
-
#--
|
31
|
-
# Exception Lookup
|
32
|
-
#++
|
33
|
-
|
34
|
-
def self.lookup(status)
|
35
|
-
self.const_get(Halcyon::Exceptions::HTTP_ERROR_CODES[status].gsub(/( |\-)/,''))
|
36
|
-
end
|
37
|
-
|
38
|
-
end
|
39
|
-
end
|
40
|
-
end
|
41
|
-
end
|
@@ -1,106 +0,0 @@
|
|
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
|
data/lib/halcyon/server.rb
DELETED
@@ -1,62 +0,0 @@
|
|
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(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
|
20
|
-
|
21
|
-
#--
|
22
|
-
# module
|
23
|
-
#++
|
24
|
-
|
25
|
-
module Halcyon
|
26
|
-
|
27
|
-
# = Server Communication and Protocol
|
28
|
-
#
|
29
|
-
# Server tries to comply with appropriate HTTP response codes, as found at
|
30
|
-
# <http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html>. However, all
|
31
|
-
# responses are JSON encoded as the server expects a JSON parser on the
|
32
|
-
# client side since the server should not be processing requests directly
|
33
|
-
# through the browser. The server expects the User-Agent to be one of:
|
34
|
-
# +"User-Agent" => "JSON/1.1.1 Compatible (en-US) Halcyon/0.0.12 Client/0.0.1"+
|
35
|
-
# +"User-Agent" => "JSON/1.1.1 Compatible"+
|
36
|
-
# The server also expects to accept application/json and be originated
|
37
|
-
# from the local host (though this can be overridden).
|
38
|
-
#
|
39
|
-
# = Usage
|
40
|
-
#
|
41
|
-
# For documentation on using Halcyon, check out the Halcyon::Server::Base and
|
42
|
-
# Halcyon::Client::Base classes which contain much more usage documentation.
|
43
|
-
class Server
|
44
|
-
def self.version
|
45
|
-
VERSION.join('.')
|
46
|
-
end
|
47
|
-
|
48
|
-
#--
|
49
|
-
# module dependencies
|
50
|
-
#++
|
51
|
-
|
52
|
-
autoload :Base, 'halcyon/server/base'
|
53
|
-
autoload :Router, 'halcyon/server/router'
|
54
|
-
|
55
|
-
end
|
56
|
-
|
57
|
-
end
|
58
|
-
|
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}
|
@@ -1,107 +0,0 @@
|
|
1
|
-
#!/usr/bin/env ruby
|
2
|
-
#--
|
3
|
-
# Created by Matt Todd on 2008-01-16.
|
4
|
-
# Copyright (c) 2008. All rights reserved.
|
5
|
-
#++
|
6
|
-
|
7
|
-
#--
|
8
|
-
# module
|
9
|
-
#++
|
10
|
-
|
11
|
-
module Halcyon
|
12
|
-
class Server
|
13
|
-
module Auth
|
14
|
-
|
15
|
-
# = Introduction
|
16
|
-
#
|
17
|
-
# The Auth::Basic class provides an alternative to the Server::Base
|
18
|
-
# class for creating servers with HTTP Basic Authentication built in.
|
19
|
-
#
|
20
|
-
# == Usage
|
21
|
-
#
|
22
|
-
# In order to provide for HTTP Basic Authentication in your server,
|
23
|
-
# it would first need to inherit from this class instead of Server::Base
|
24
|
-
# and then provide a method to check for the existence of the credentials
|
25
|
-
# and respond accordingly. This looks like the following:
|
26
|
-
#
|
27
|
-
# class AuthenticatedApp < Halcyon::Server::Auth::Basic
|
28
|
-
# def basic_authorization(username, password)
|
29
|
-
# [username, password] == ['rupert', 'secret']
|
30
|
-
# end
|
31
|
-
# # write normal Halcyon server app here
|
32
|
-
# end
|
33
|
-
#
|
34
|
-
# The credentials passed to the +basic_authorization+ method are pulled
|
35
|
-
# from the appropriate Authorization header value and parsed from the
|
36
|
-
# base64 values. If no Authorization header value is passed, an exception
|
37
|
-
# is thrown resulting in the appropriate response to the client.
|
38
|
-
class Basic < Server::Base
|
39
|
-
|
40
|
-
AUTHORIZATION_KEYS = ['HTTP_AUTHORIZATION', 'X-HTTP_AUTHORIZATION', 'X_HTTP_AUTHORIZATION']
|
41
|
-
|
42
|
-
# Determines the appropriate HTTP Authorization header to refer to when
|
43
|
-
# plucking out the header for processing.
|
44
|
-
def authorization_key
|
45
|
-
@authorization_key ||= AUTHORIZATION_KEYS.detect{|k|@env.has_key?(k)}
|
46
|
-
end
|
47
|
-
|
48
|
-
alias :_run :run
|
49
|
-
|
50
|
-
# Ensures that the HTTP Authentication header is included, the Basic
|
51
|
-
# scheme is being used, and the credentials pass the +basic_auth+
|
52
|
-
# test. If any of these fail, an Unauthorized exception is raised
|
53
|
-
# (except for non-Basic schemes), otherwise the +route+ is +run+
|
54
|
-
# normally.
|
55
|
-
#
|
56
|
-
# See the documentation for the +basic_auth+ class method for details
|
57
|
-
# concerning the credentials and action inclusion/exclusion.
|
58
|
-
def run(route)
|
59
|
-
# test credentials if the action is one specified to be tested
|
60
|
-
if ((@@auth[:except].nil? && @@auth[:only].nil?) || # the default is to test if no restrictions
|
61
|
-
(!@@auth[:only].nil? && @@auth[:only].include?(route[:action].to_sym)) || # but if the action is in the :only directive, test
|
62
|
-
(!@@auth[:except].nil? && !@@auth[:except].include?(route[:action].to_sym))) # or if the action is not in the :except directive, test
|
63
|
-
|
64
|
-
# make sure there's an authorization header
|
65
|
-
raise Base::Exceptions::Unauthorized.new unless !authorization_key.nil?
|
66
|
-
|
67
|
-
# make sure the request is via the Basic protocol
|
68
|
-
scheme = @env[authorization_key].split.first.downcase.to_sym
|
69
|
-
raise Base::Exceptions::BadRequest.new unless scheme == :basic
|
70
|
-
|
71
|
-
# make sure the credentials pass the test
|
72
|
-
credentials = @env[authorization_key].split.last.unpack("m*").first.split(':', 2)
|
73
|
-
raise Base::Exceptions::Unauthorized.new unless @@auth[:method].call(*credentials)
|
74
|
-
end
|
75
|
-
|
76
|
-
# success, so run the route normally
|
77
|
-
_run(route)
|
78
|
-
rescue Halcyon::Exceptions::Base => e
|
79
|
-
@logger.warn "#{uri} => #{e.error}"
|
80
|
-
# handles all content error exceptions
|
81
|
-
@res.status = e.status
|
82
|
-
{:status => e.status, :body => e.error}
|
83
|
-
end
|
84
|
-
|
85
|
-
# Provides a way to define a test as well as set limits on what is
|
86
|
-
# tested for Basic Authorization. This method should be called in the
|
87
|
-
# definition of the server. A simple example would look like:
|
88
|
-
#
|
89
|
-
# class Servr < Halcyon::Server::Auth::Basic
|
90
|
-
# basic_auth :only => [:grant] do |user, pass|
|
91
|
-
# # test credentials
|
92
|
-
# end
|
93
|
-
# # routes and actions follow...
|
94
|
-
# end
|
95
|
-
#
|
96
|
-
# Two acceptable options include <tt>:only</tt> and <tt>:except</tt>.
|
97
|
-
def self.basic_auth(options={}, &proc)
|
98
|
-
instance_eval do
|
99
|
-
@@auth = options.merge(:method => proc)
|
100
|
-
end
|
101
|
-
end
|
102
|
-
|
103
|
-
end
|
104
|
-
|
105
|
-
end
|
106
|
-
end
|
107
|
-
end
|
data/lib/halcyon/server/base.rb
DELETED
@@ -1,774 +0,0 @@
|
|
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
|
-
:root => Dir.pwd,
|
21
|
-
:environment => 'none',
|
22
|
-
:port => 9267,
|
23
|
-
:host => 'localhost',
|
24
|
-
:server => Gem.searcher.find('thin').nil? ? 'mongrel' : 'thin',
|
25
|
-
:pid_file => '/var/run/halcyon.{server}.{app}.{port}.pid',
|
26
|
-
:log_file => '/var/log/halcyon.{app}.log',
|
27
|
-
:log_level => 'info',
|
28
|
-
:log_format => proc{|s,t,p,m|"#{s} [#{t.strftime("%Y-%m-%d %H:%M:%S")}] (#{$$}) #{p} :: #{m}\n"},
|
29
|
-
# handled internally
|
30
|
-
:acceptable_requests => [],
|
31
|
-
:acceptable_remotes => []
|
32
|
-
}
|
33
|
-
ACCEPTABLE_REQUESTS = [
|
34
|
-
# ENV var to check, Regexp the value should match, the status code to return in case of failure, the message with the code
|
35
|
-
["HTTP_USER_AGENT", /JSON\/1\.1\.\d+ Compatible( \(en-US\) Halcyon\/(\d+\.\d+\.\d+) Client\/(\d+\.\d+\.\d+))?/, 406, 'Not Acceptable'],
|
36
|
-
["CONTENT_TYPE", /application\/json/, 415, 'Unsupported Media Type']
|
37
|
-
]
|
38
|
-
ACCEPTABLE_REMOTES = ['localhost', '127.0.0.1', '0.0.0.0']
|
39
|
-
|
40
|
-
# = Building Halcyon Server Apps
|
41
|
-
#
|
42
|
-
# Halcyon apps are actually little servers running on top of Rack instances
|
43
|
-
# which affords a great deal of simplicity and quickness to both design and
|
44
|
-
# performance.
|
45
|
-
#
|
46
|
-
# Building a Halcyon app consists of defining routes to map all requests
|
47
|
-
# against in order to designate what functionality handles what specific
|
48
|
-
# requests, the actual actions (and modules) to actually perform these
|
49
|
-
# requests, and any extensions or configurations you may need or want for
|
50
|
-
# your individual needs.
|
51
|
-
#
|
52
|
-
# == Inheriting from Halcyon::Server::Base
|
53
|
-
#
|
54
|
-
# To begin with, an application would be started by simply defining a class
|
55
|
-
# that inherits from Halcyon::Server::Base.
|
56
|
-
#
|
57
|
-
# class Greeter < Halcyon::Server::Base
|
58
|
-
# end
|
59
|
-
#
|
60
|
-
# Once this task has been completed, routes can be defined.
|
61
|
-
#
|
62
|
-
# class Greeter < Halcyon::Server::Base
|
63
|
-
# route do |r|
|
64
|
-
# r.match('/hello/:name').to(:action => 'greet')
|
65
|
-
# {:action => 'not_found'} # default route
|
66
|
-
# end
|
67
|
-
# end
|
68
|
-
#
|
69
|
-
# Two routes are (effectively) defined here, the first being to watch for
|
70
|
-
# all requests in the format <tt>/hello/:name</tt> where the word pattern
|
71
|
-
# is stored and transmitted as the appropriate keys in the params hash.
|
72
|
-
#
|
73
|
-
# Once we've got our inputs specified, we can start to handle requests:
|
74
|
-
#
|
75
|
-
# class Greeter < Halcyon::Server::Base
|
76
|
-
# route do |r|
|
77
|
-
# r.match('/hello/:name').to(:action => 'greet')
|
78
|
-
# {:action => 'not_found'} # default route
|
79
|
-
# end
|
80
|
-
# def greet; {:status=>200, :body=>"Hi #{params[:name]}"}; end
|
81
|
-
# end
|
82
|
-
#
|
83
|
-
# You will notice that we only define the method +greet+ and that it
|
84
|
-
# returns a Hash object containing a +status+ code and +body+ content.
|
85
|
-
# This is the most basic way to send data, but if all you're doing is
|
86
|
-
# replying that the request was successful and you have data to return,
|
87
|
-
# the method +ok+ (an alias of <tt>standard_response</tt>) with the +body+
|
88
|
-
# param as its sole parameter is sufficient.
|
89
|
-
#
|
90
|
-
#
|
91
|
-
# def greet; ok("Hi #{params[:name]}"); end
|
92
|
-
#
|
93
|
-
# You'll also notice that there's no method called +not_found+; this is
|
94
|
-
# because it is already defined and behaves almost exactly like the +ok+
|
95
|
-
# method. We could certainly overwrite +not_found+, but at this point it
|
96
|
-
# is not necessary.
|
97
|
-
#
|
98
|
-
# You should also realize that the second route is not defined. This is
|
99
|
-
# classified as the default route, the route to follow in the event that no
|
100
|
-
# route actually matches, so it doesn't need any of the extra path to match
|
101
|
-
# against.
|
102
|
-
#
|
103
|
-
# Lastly, the use of +params+ inside the method is simply a method call
|
104
|
-
# to a hash of the parameters gleaned from the route, such as +:name+ or
|
105
|
-
# any other variables passed to it.
|
106
|
-
#
|
107
|
-
# == The Filesystem
|
108
|
-
#
|
109
|
-
# It's important to note that the +halcyon+ commandline tool expects to
|
110
|
-
# find your server inheriting +Halcyon::Server::Base+ with the same exact
|
111
|
-
# name as its filename, though with special rules.
|
112
|
-
#
|
113
|
-
# To clarify, when your server is stored in +app_server.rb+, it expects
|
114
|
-
# that your server's class name be +AppServer+ as it capitalizes each word
|
115
|
-
# and removes all underscores, etc.
|
116
|
-
#
|
117
|
-
# Keep this in mind when naming your class and your file, though this
|
118
|
-
# restriction is only temporary.
|
119
|
-
#
|
120
|
-
# NOTE: This really isn't a necessary step if you write your own deployment
|
121
|
-
# script instead of using the +halcyon+ commandline tool (as it is simply
|
122
|
-
# a convenience tool). In such, feel free to name your server however you
|
123
|
-
# prefer and the file likewise.
|
124
|
-
#
|
125
|
-
# == Running Your Server On Your Own
|
126
|
-
#
|
127
|
-
# If you're wanting to run your server without the help of the +halcyon+
|
128
|
-
# commandline tool, you will simply need to initialize the server as you
|
129
|
-
# pass it to the Rack handler of choice along with any configuration
|
130
|
-
# options you desire.
|
131
|
-
#
|
132
|
-
# The following should be enough:
|
133
|
-
#
|
134
|
-
# Rack::Handler::Mongrel.run YourAppName.new(options), :Port => 9267
|
135
|
-
#
|
136
|
-
# Of course Halcyon already handles most of your dependencies for you, so
|
137
|
-
# don't worry about requiring Rack, et al. And again, the options are not
|
138
|
-
# mandatory as the default options are certainly acceptable.
|
139
|
-
#
|
140
|
-
# NOTE: If you want to provide debugging information, just set +$debug+ to
|
141
|
-
# +true+ and you should receive all the debugging information available.
|
142
|
-
class Base
|
143
|
-
|
144
|
-
#--
|
145
|
-
# Request Handling
|
146
|
-
#++
|
147
|
-
|
148
|
-
# = Handling Calls
|
149
|
-
#
|
150
|
-
# Receives the request, handles the route matching, runs the approriate
|
151
|
-
# action based on the route determined (or defaulted to) and finishes by
|
152
|
-
# responding to the client with the content returned.
|
153
|
-
#
|
154
|
-
# == Response and Output
|
155
|
-
#
|
156
|
-
# Halcyon responds in purely JSON format (except perhaps on sever server
|
157
|
-
# malfunctions that aren't caught or intended; read: bugs).
|
158
|
-
#
|
159
|
-
# The standard response is simply a JSON-encoded hash following this
|
160
|
-
# format:
|
161
|
-
#
|
162
|
-
# {:status => http_status_code, :body => response_body}
|
163
|
-
#
|
164
|
-
# Response body can be any object desired (as long as there is a
|
165
|
-
# +to_json+ method for it, which includes most core classes), usually
|
166
|
-
# containing a nested hash with appropriate data.
|
167
|
-
#
|
168
|
-
# DO NOT try to call +to_json+ on the +body+ contents as this will cause
|
169
|
-
# errors when trying to parse JSON.
|
170
|
-
#
|
171
|
-
# == Request and Response
|
172
|
-
#
|
173
|
-
# If you need access to the Request and Response, the instance variables
|
174
|
-
# +@req+ and +@res+ will be sufficient for you.
|
175
|
-
#
|
176
|
-
# If you need specific documentation for these objects, check the
|
177
|
-
# corresponding docs in the Rack documentation.
|
178
|
-
#
|
179
|
-
# == Requests and POST Data
|
180
|
-
#
|
181
|
-
# Most of your requests will have all the data it needs inside of the
|
182
|
-
# +params+ you receive for your action, but for POST and PUT requests
|
183
|
-
# (you are being RESTful, right?) you will need to retrieve your data
|
184
|
-
# from the method +post+. Here's how:
|
185
|
-
#
|
186
|
-
# post[:key] => "value"
|
187
|
-
#
|
188
|
-
# As you can see, keys specifically are symbols and values as well. What
|
189
|
-
# this means is that your POST data that you send to the server needs to
|
190
|
-
# be careful to provide a flat Hash (if anything other than a Hash is
|
191
|
-
# passed, it is packed up into a hash similar to +{:body=>data}+) or at
|
192
|
-
# least send a complicated structure as a JSON object so that transport
|
193
|
-
# is clean. Resurrecting the object is still on your end for POST data
|
194
|
-
# (though this could change). Here's how you would reconstruct your
|
195
|
-
# special hash:
|
196
|
-
#
|
197
|
-
# value = JSON.parse(post[:key])
|
198
|
-
#
|
199
|
-
# That will take care of reconstructing your Hash.
|
200
|
-
#
|
201
|
-
# And that is essentially all you need to worry about for retreiving your
|
202
|
-
# POST contents. Sending POST contents should be documented well enough
|
203
|
-
# in Halcyon::Client::Base.
|
204
|
-
#
|
205
|
-
# == Logging
|
206
|
-
#
|
207
|
-
# Logging can be done by logging to +@logger+ when inside the scope of
|
208
|
-
# application instance (inside of your instance methods and modules).
|
209
|
-
#
|
210
|
-
# The +@env+ instance variable has been modified to include a
|
211
|
-
# +halcyon.logger+ property including the given logger. Use this for
|
212
|
-
# logging if you need to step outside of the scope of the current
|
213
|
-
# application instance (just be sure to pass @env along with you).
|
214
|
-
def call(env)
|
215
|
-
@time_started = Time.now
|
216
|
-
|
217
|
-
# collect env information, create request and response objects, prep for dispatch
|
218
|
-
# puts env.inspect if $debug # request information (huge)
|
219
|
-
@env = env
|
220
|
-
@res = Rack::Response.new
|
221
|
-
@req = Rack::Request.new(env)
|
222
|
-
|
223
|
-
# add the logger to the @env instance variable for global access if for
|
224
|
-
# some reason the environment needs to be passed outside of the
|
225
|
-
# instance
|
226
|
-
@env['halcyon.logger'] = @logger
|
227
|
-
|
228
|
-
# pre run hook
|
229
|
-
before_run(Time.now - @time_started) if respond_to? :before_run
|
230
|
-
|
231
|
-
# prepare route and provide it for callers
|
232
|
-
route = Router.route(@env)
|
233
|
-
@env['halcyon.route'] = route
|
234
|
-
|
235
|
-
# dispatch
|
236
|
-
@res.write(run(route).to_json)
|
237
|
-
|
238
|
-
# post run hook
|
239
|
-
after_run(Time.now - @time_started) if respond_to? :after_run
|
240
|
-
|
241
|
-
@time_finished = Time.now - @time_started
|
242
|
-
|
243
|
-
# logs access in the following format: [200] / => index (0.0029s;343.79req/s)
|
244
|
-
req_time, req_per_sec = ((@time_finished*1e4).round.to_f/1e4), (((1.0/@time_finished)*1e2).round.to_f/1e2)
|
245
|
-
@logger.info "[#{@res.status}] #{@env['REQUEST_URI']} => #{route[:module].to_s}#{((route[:module].nil?) ? "" : "::")}#{route[:action]} (#{req_time}s;#{req_per_sec}req/s)"
|
246
|
-
|
247
|
-
# finish request
|
248
|
-
@res.finish
|
249
|
-
end
|
250
|
-
|
251
|
-
# = Dispatching Requests
|
252
|
-
#
|
253
|
-
# Dispatches the routed request, handling module resolution and pulling
|
254
|
-
# all of the param values together for the action. This action is called
|
255
|
-
# by +call+ and should be transparent to your server app.
|
256
|
-
#
|
257
|
-
# One of the design elements of this method is that it rescues all
|
258
|
-
# Halcon-specific exceptions (defined innside of ::Base::Exceptions) so
|
259
|
-
# that a proper JSON response may be rendered by +call+.
|
260
|
-
#
|
261
|
-
# With this in mind, it is preferred that, for any errors that should
|
262
|
-
# result in a given HTTP Response code other than 2xx, an appropriate
|
263
|
-
# exception should be thrown which is then handled by this method's
|
264
|
-
# rescue clause.
|
265
|
-
#
|
266
|
-
# Refer to the Exceptions module to see a list of available Exceptions.
|
267
|
-
#
|
268
|
-
# == Acceptable Requests
|
269
|
-
#
|
270
|
-
# Halcyon is a very picky server when dealing with requests, requiring
|
271
|
-
# that clients match a given remote location, accepting JSON responses,
|
272
|
-
# and matching a certain User-Agent profile. Unless running in debug
|
273
|
-
# mode, Halcyon will reject all requests with a 403 Forbidden response
|
274
|
-
# if these requirements are not met.
|
275
|
-
#
|
276
|
-
# This means, while in development and testing, the debug flag must be
|
277
|
-
# enabled if you intend to perform initial tests through the browser.
|
278
|
-
#
|
279
|
-
# These restrictions may appear to be arbitrary, but it is simply a
|
280
|
-
# measure to prevent a live server running in production mode from being
|
281
|
-
# assaulted by unacceptable clients which keeps the server performing
|
282
|
-
# actual functions without concerning itself with non-acceptable clients.
|
283
|
-
#
|
284
|
-
# The requirements are defined by the Halcyon::Server constants:
|
285
|
-
# * +ACCEPTABLE_REQUESTS+: defines the necessary User-Agent and Accept
|
286
|
-
# headers the client must provide.
|
287
|
-
# * ACCEPTABLE_REMOTES: defines the acceptable remote origins of
|
288
|
-
# any request. This is primarily limited to
|
289
|
-
# only local requests, but can be changed.
|
290
|
-
#
|
291
|
-
# Halcyon servers are intended to be run behind other applications and
|
292
|
-
# primarily only speaking with other apps on the same machine, though
|
293
|
-
# your specific requirements may differ and change that.
|
294
|
-
#
|
295
|
-
# When in debug mode or in testing mode, the request filtering test is
|
296
|
-
# not fired, so all requests from all User Agents and locations will
|
297
|
-
# succeed. This is important to know if you plan on testing this specific
|
298
|
-
# feature while in debugging or testing modes.
|
299
|
-
#
|
300
|
-
# == Hooks, Callbacks, and Authentication
|
301
|
-
#
|
302
|
-
# There is no Authentication mechanism built in to Halcyon (for the time
|
303
|
-
# being), but there are hooks and callbacks for you to be able to ensure
|
304
|
-
# that requests are authenticated, etc.
|
305
|
-
#
|
306
|
-
# In order to set up a callback, simply define one of the following
|
307
|
-
# methods in your app's base class:
|
308
|
-
# * before_run
|
309
|
-
# * before_action
|
310
|
-
# * after_action
|
311
|
-
# * after_run
|
312
|
-
#
|
313
|
-
# This is the exact order in which the callbacks are performed if
|
314
|
-
# defined. Make use of these methods to monitor incoming and outgoing
|
315
|
-
# requests.
|
316
|
-
#
|
317
|
-
# It is preferred for these methods to throw Exceptions::Base exceptions
|
318
|
-
# (or one of its inheriters) instead of handling them manually. This
|
319
|
-
# ensures that the actual action is not run when in fact it shouldn't,
|
320
|
-
# otherwise you could be allowing unauthenticated users privileged
|
321
|
-
# information or allowing them to perform destructive actions.
|
322
|
-
def run(route)
|
323
|
-
# make sure the request meets our expectations
|
324
|
-
acceptable_request! unless $debug || $test
|
325
|
-
|
326
|
-
# pull params
|
327
|
-
@params = route.reject{|key, val| [:action, :module].include? key}
|
328
|
-
@params.merge!(query_params)
|
329
|
-
|
330
|
-
# pre call hook
|
331
|
-
before_call if respond_to? :before_call
|
332
|
-
|
333
|
-
# handle module actions differently than non-module actions
|
334
|
-
if route[:module].nil?
|
335
|
-
# call action
|
336
|
-
res = send(route[:action])
|
337
|
-
else
|
338
|
-
# call module action
|
339
|
-
mod = self.dup
|
340
|
-
mod.instance_eval(&(@@modules[route[:module].to_sym]))
|
341
|
-
res = mod.send(route[:action])
|
342
|
-
end
|
343
|
-
|
344
|
-
# after call hook
|
345
|
-
after_call if respond_to? :after_call
|
346
|
-
|
347
|
-
@params = {}
|
348
|
-
|
349
|
-
res
|
350
|
-
rescue Halcyon::Exceptions::Base => e
|
351
|
-
@logger.warn "#{uri} => #{e.error}"
|
352
|
-
# handles all content error exceptions
|
353
|
-
@res.status = e.status
|
354
|
-
{:status => e.status, :body => e.error}
|
355
|
-
end
|
356
|
-
|
357
|
-
# Tests for acceptable requests if +$debug+ and +$test+ are not set.
|
358
|
-
def acceptable_request!
|
359
|
-
@config[:acceptable_requests].each do |req|
|
360
|
-
raise Halcyon::Exceptions::Base.new(req[2], req[3]) unless @env[req[0]] =~ req[1]
|
361
|
-
end
|
362
|
-
raise Exceptions::Forbidden.new unless @config[:acceptable_remotes].member? @env["REMOTE_ADDR"]
|
363
|
-
end
|
364
|
-
|
365
|
-
#--
|
366
|
-
# Initialization and setup
|
367
|
-
#++
|
368
|
-
|
369
|
-
# Called when the Handler gets started and stores the configuration
|
370
|
-
# options used to start the server.
|
371
|
-
#
|
372
|
-
# Feel free to define initialize for your app (which is only called once
|
373
|
-
# per server instance), just be sure to call +super+.
|
374
|
-
#
|
375
|
-
# == PID File
|
376
|
-
#
|
377
|
-
# A PID file is created when the server is first initialized with the
|
378
|
-
# current process ID. Where it is located depends on the default option,
|
379
|
-
# the config file, the commandline option, and the debug status,
|
380
|
-
# increasing in precedence in that order.
|
381
|
-
#
|
382
|
-
# By default, the PID file is placed in +/var/run/+ and is named
|
383
|
-
# +halcyon.{server}.{app}.{port}.pid+ where +{server}+ is replaced by the
|
384
|
-
# running server, +{app}+ is the app name (suffixed with +#debug+ if
|
385
|
-
# running in debug mode), and +{port}+ being the server port (if there
|
386
|
-
# are multiple servers running, this helps clarify).
|
387
|
-
#
|
388
|
-
# There is an option to numerically label your server via the +{n}+
|
389
|
-
# value, but this is deprecated and will be removed soon. Using the
|
390
|
-
# +{port}+ option makes much more sense and creates much more meaning.
|
391
|
-
def initialize(options = {})
|
392
|
-
# save configuration options
|
393
|
-
@config = DEFAULT_OPTIONS.merge(options)
|
394
|
-
@config[:app] ||= self.class.to_s.downcase
|
395
|
-
|
396
|
-
# apply name options to log_file and pid_file configs
|
397
|
-
apply_log_and_pid_file_name_options
|
398
|
-
|
399
|
-
# debug and test mode handling
|
400
|
-
enable_debugging if $debug
|
401
|
-
enable_testing if $test
|
402
|
-
|
403
|
-
# setup logging
|
404
|
-
setup_logging unless $debug || $test
|
405
|
-
|
406
|
-
# setup request filtering
|
407
|
-
setup_request_filters unless $debug || $test
|
408
|
-
|
409
|
-
# create PID file
|
410
|
-
@pid = File.new(@config[:pid_file].gsub('{n}', server_cluster_number), "w", 0644)
|
411
|
-
@pid << "#{$$}\n"; @pid.close
|
412
|
-
|
413
|
-
# log existence
|
414
|
-
@logger.info "PID file created. PID is #{$$}."
|
415
|
-
|
416
|
-
# call startup callback if defined
|
417
|
-
startup if respond_to? :startup
|
418
|
-
|
419
|
-
# log ready state
|
420
|
-
@logger.info "Started. Awaiting connectivity. Listening on #{@config[:port]}..."
|
421
|
-
|
422
|
-
# trap signals to die (when killed by the user) gracefully
|
423
|
-
finalize = Proc.new do
|
424
|
-
@logger.info "Shutting down #{$$}."
|
425
|
-
clean_up
|
426
|
-
exit
|
427
|
-
end
|
428
|
-
# http://en.wikipedia.org/wiki/Signal_%28computing%29
|
429
|
-
%w(INT KILL TERM QUIT HUP).each{|sig|trap(sig, finalize)}
|
430
|
-
|
431
|
-
# listen for USR1 signals and toggle debugging accordingly
|
432
|
-
trap("USR1") do
|
433
|
-
if $debug
|
434
|
-
disable_debugging
|
435
|
-
else
|
436
|
-
enable_debugging
|
437
|
-
end
|
438
|
-
end
|
439
|
-
end
|
440
|
-
|
441
|
-
# Closes the logger and deletes the PID file.
|
442
|
-
def clean_up
|
443
|
-
# don't try to clean up what's cleaned up already
|
444
|
-
return if defined? @cleaned_up
|
445
|
-
|
446
|
-
# run shutdown hook if defined
|
447
|
-
shutdown if respond_to? :shutdown
|
448
|
-
|
449
|
-
# close logger, delete PID file, flag clean state
|
450
|
-
@logger.close
|
451
|
-
File.delete(@pid.path) if File.exist?(@pid.path)
|
452
|
-
@cleaned_up = true
|
453
|
-
end
|
454
|
-
|
455
|
-
# Retreives the server cluster sequence number for the PID file.
|
456
|
-
#
|
457
|
-
# This is deprecated and will be removed soon, probably for the 0.4.0
|
458
|
-
# release. Use of the +{port}+ value is much more appropriate and
|
459
|
-
# meaningful.
|
460
|
-
def server_cluster_number
|
461
|
-
# if there are no +{n}+ references in the PID file name, then simply
|
462
|
-
# return 0 as the cluster number. (This is the preferred behavior and
|
463
|
-
# this test allows the method to fail fast. +{n}+ is deprecated and
|
464
|
-
# will be removed before 0.4.0 is released.)
|
465
|
-
return 0.to_s if @config[:pid_file]['{n}'].nil?
|
466
|
-
|
467
|
-
# warn users that they're using a deprecated convention.
|
468
|
-
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."
|
469
|
-
|
470
|
-
# counts the number of PID files already existing.
|
471
|
-
server_count = Dir[@config[:pid_file].gsub('{n}','*')].length
|
472
|
-
# since the counting starts at 0, if the file with the count exists,
|
473
|
-
# then one of the lesser number servers isn't running, so check each
|
474
|
-
# PID file until the one not running is found.
|
475
|
-
# if no files exist, then 0 will be the count, which won't exist, so
|
476
|
-
# it will be the default number.
|
477
|
-
while File.exist?(@config[:pid_file].gsub('{n}',server_count.to_s))
|
478
|
-
server_count -= 1
|
479
|
-
end
|
480
|
-
# return that number.
|
481
|
-
server_count.to_s
|
482
|
-
end
|
483
|
-
|
484
|
-
# If the server receives a SIGUSR1 signal it will toggle debugging. This
|
485
|
-
# method is used to setup logging and the request handling methods for
|
486
|
-
# debugging.
|
487
|
-
def enable_debugging
|
488
|
-
$debug = true
|
489
|
-
|
490
|
-
# set the PID file name to /tmp/ unless PID file already exists
|
491
|
-
@config[:pid_file] = '/tmp/halcyon.{server}.{app}.{port}.pid' unless defined? @pid
|
492
|
-
apply_log_and_pid_file_name_options # reapply for {server}, {app}, and {port} to be set
|
493
|
-
|
494
|
-
# setup logger to STDOUT and log entering debugging mode
|
495
|
-
@logger = Logger.new(STDOUT)
|
496
|
-
@logger.progname = "#{self.class}#debug"
|
497
|
-
@logger.level = Logger::DEBUG
|
498
|
-
@logger.formatter = @config[:log_format]
|
499
|
-
@logger.info "Entering debugging mode..."
|
500
|
-
rescue Errno::EACCES
|
501
|
-
abort "Can't access #{@config[:pid_file]}, try 'sudo #{$0}'"
|
502
|
-
end
|
503
|
-
|
504
|
-
# This method is used to setup logging and the request handling methods
|
505
|
-
# for debugging.
|
506
|
-
def enable_testing
|
507
|
-
# set the PID file name to /tmp/ unless PID file already exists
|
508
|
-
@config[:pid_file] = '/tmp/halcyon.testing.{app}.{port}.pid' unless defined? @pid
|
509
|
-
@config[:log_file] = '/tmp/halcyon.testing.{app}.log'
|
510
|
-
apply_log_and_pid_file_name_options # reapply for {server}, {app}, and {port} to be set
|
511
|
-
|
512
|
-
# setup logger and log entering testing mode
|
513
|
-
@logger = Logger.new(@config[:log_file])
|
514
|
-
@logger.progname = "#{self.class}#test"
|
515
|
-
@logger.level = Logger::DEBUG
|
516
|
-
@logger.formatter = @config[:log_format]
|
517
|
-
@logger.info "Entering testing mode..."
|
518
|
-
|
519
|
-
# make sure we clean up after ourselves since we're in testing mode
|
520
|
-
at_exit {
|
521
|
-
clean_up
|
522
|
-
File.delete(@config[:log_file]) if File.exist?(@config[:log_file])
|
523
|
-
}
|
524
|
-
rescue Errno::EACCES
|
525
|
-
abort "Can't access #{@config[:pid_file]}, try 'sudo #{$0}'"
|
526
|
-
end
|
527
|
-
|
528
|
-
# Disables all of the affects of debugging mode and returns logging and
|
529
|
-
# request filtering back to normal.
|
530
|
-
#
|
531
|
-
# Refer to +enable_debugging+ for more information.
|
532
|
-
def disable_debugging
|
533
|
-
# disable logging and log leaving debugging mode
|
534
|
-
$debug = false
|
535
|
-
@logger.info "Leaving debugging mode."
|
536
|
-
|
537
|
-
# setup normal logging
|
538
|
-
setup_logging
|
539
|
-
|
540
|
-
# reenable request filtering
|
541
|
-
setup_request_filters
|
542
|
-
end
|
543
|
-
|
544
|
-
# Sets up logging based on the configuration options in +@config+, which
|
545
|
-
# is set (in order of lowest to highest precedence) in the default
|
546
|
-
# options, in the configuration file provided, on the commandline, and
|
547
|
-
# debug mode options.
|
548
|
-
#
|
549
|
-
# == Levels
|
550
|
-
#
|
551
|
-
# The accepted level values are as follows:
|
552
|
-
#
|
553
|
-
# * debug
|
554
|
-
# * info
|
555
|
-
# * warn
|
556
|
-
# * error
|
557
|
-
# * fatal
|
558
|
-
# * unknown
|
559
|
-
#
|
560
|
-
# These are the exact way you can refer to the logger level you'd like to
|
561
|
-
# log at from all points of option specification (listed above in order
|
562
|
-
# of ascending precedence).
|
563
|
-
#
|
564
|
-
# If a bogus value is entered, a warning will be issued and the value
|
565
|
-
# will be defaulted to 'debug'. (So don't mess up.)
|
566
|
-
def setup_logging
|
567
|
-
# get the logging level based on the name supplied
|
568
|
-
level = {
|
569
|
-
'debug' => Logger::DEBUG,
|
570
|
-
'info' => Logger::INFO,
|
571
|
-
'warn' => Logger::WARN,
|
572
|
-
'error' => Logger::ERROR,
|
573
|
-
'fatal' => Logger::FATAL,
|
574
|
-
'unknown' => Logger::UNKNOWN # wtf?
|
575
|
-
}[@config[:log_level]]
|
576
|
-
if level.nil?
|
577
|
-
warn "Logging level specified not acceptable. Defaulting to 'debug'. Check the documentation for the acceptable values."
|
578
|
-
@config[:log_level] = 'debug'
|
579
|
-
level = Logger::DEBUG
|
580
|
-
end
|
581
|
-
|
582
|
-
# setup the logger
|
583
|
-
@logger = Logger.new(@config[:log_file])
|
584
|
-
@logger.progname = self.class
|
585
|
-
@logger.level = level
|
586
|
-
@logger.formatter = @config[:log_format]
|
587
|
-
rescue Errno::EACCES
|
588
|
-
abort "Can't access #{@config[:log_file]}, try 'sudo #{$0}'"
|
589
|
-
end
|
590
|
-
|
591
|
-
# Sets up request filters based on User-Agent, Content-Type, and Remote
|
592
|
-
# IP/address values.
|
593
|
-
#
|
594
|
-
# Extracted from +initialize+ to reduce repetition.
|
595
|
-
def setup_request_filters
|
596
|
-
@config[:acceptable_requests] = ACCEPTABLE_REQUESTS
|
597
|
-
@config[:acceptable_remotes] = ACCEPTABLE_REMOTES
|
598
|
-
end
|
599
|
-
|
600
|
-
# Searches through the PID file name and the Log file name stored in the
|
601
|
-
# +@config+ variable for +{server}+, +{app}+, and +{port}+ values and
|
602
|
-
# sets them accordingly.
|
603
|
-
def apply_log_and_pid_file_name_options
|
604
|
-
# DEFAULT :pid_file => '/var/run/halcyon.{server}.{app}.{port}.pid',
|
605
|
-
@config[:pid_file].gsub!('{server}', @config[:server])
|
606
|
-
@config[:pid_file].gsub!('{port}', @config[:port].to_s)
|
607
|
-
@config[:pid_file].gsub!('{app}', File.basename(@config[:app]))
|
608
|
-
# DEFAULT :log_file => '/var/log/halcyon.{app}.log',
|
609
|
-
@config[:log_file].gsub!('{server}', @config[:server])
|
610
|
-
@config[:log_file].gsub!('{port}', @config[:port].to_s)
|
611
|
-
@config[:log_file].gsub!('{app}', File.basename(@config[:app]))
|
612
|
-
end
|
613
|
-
|
614
|
-
# = Routing
|
615
|
-
#
|
616
|
-
# Halcyon expects its apps to have routes set up inside of the base class
|
617
|
-
# (the class that inherits from Halcyon::Server::Base). Routes are
|
618
|
-
# defined identically to Merb's routes (since Halcyon Router inherits all
|
619
|
-
# its functionality directly from the Merb Router).
|
620
|
-
#
|
621
|
-
# == Usage
|
622
|
-
#
|
623
|
-
# A sample Halcyon application defining and handling Routes follows:
|
624
|
-
#
|
625
|
-
# class Simple < Halcyon::Server::Base
|
626
|
-
# route do |r|
|
627
|
-
# r.match('/user/show/:id').to(:module => 'user', :action => 'show')
|
628
|
-
# r.match('/hello/:name').to(:action => 'greet')
|
629
|
-
# r.match('/').to(:action => 'index')
|
630
|
-
# {:action => 'not_found'} # default route
|
631
|
-
# end
|
632
|
-
# user do
|
633
|
-
# def show(p); ok(p[:id]); end
|
634
|
-
# end
|
635
|
-
# def greet(p); ok("Hi #{p[:name]}"); end
|
636
|
-
# def index(p); ok("..."); end
|
637
|
-
# def not_found(p); super; end
|
638
|
-
# end
|
639
|
-
#
|
640
|
-
# In this example we define numerous routes for actions and even an
|
641
|
-
# action in the 'user' module as well as handling the event that no route
|
642
|
-
# was matched (thereby passing to not_found).
|
643
|
-
#
|
644
|
-
# == Modules
|
645
|
-
#
|
646
|
-
# A module is simply a named block that whose methods get executed as if
|
647
|
-
# they were in Base but without conflicting any methods with them, very
|
648
|
-
# similar to module in Ruby. All that is required to define a module is
|
649
|
-
# something like this:
|
650
|
-
#
|
651
|
-
# admin do
|
652
|
-
# def users; ok(...); end
|
653
|
-
# end
|
654
|
-
#
|
655
|
-
# This just needs to add one directive when defining what a given route
|
656
|
-
# maps to, such as:
|
657
|
-
#
|
658
|
-
# route do |r|
|
659
|
-
# r.map('/admin/users').to(:module => 'admin', :action => 'users')
|
660
|
-
# end
|
661
|
-
#
|
662
|
-
# or, alternatively, you can just map to:
|
663
|
-
#
|
664
|
-
# r.map('/:module/:action').to()
|
665
|
-
#
|
666
|
-
# though it may be better to just explicitly state the module (for
|
667
|
-
# resolving cleanly when someone starts entering garbage that matches
|
668
|
-
# incorrectly).
|
669
|
-
#
|
670
|
-
# == More Help
|
671
|
-
#
|
672
|
-
# In addition to this, you may also find some of the documentation for
|
673
|
-
# the Router class helpful. However, since the Router is pulled directly
|
674
|
-
# from Merb, you really should look at the documentation for Merb. You
|
675
|
-
# can find the documentation on Merb's website at: http://merbivore.com/
|
676
|
-
def self.route
|
677
|
-
if block_given?
|
678
|
-
Router.prepare do |router|
|
679
|
-
Router.default_to yield(router) || {:action => 'not_found'}
|
680
|
-
end
|
681
|
-
else
|
682
|
-
abort "Halcyon::Server::Base.route expects a block to define routes."
|
683
|
-
end
|
684
|
-
end
|
685
|
-
|
686
|
-
# Registers modules internally. (This is designed in a way to prevent
|
687
|
-
# method naming collisions inside and outside of modules.)
|
688
|
-
def self.method_missing(name, *params, &proc)
|
689
|
-
@@modules ||= {}
|
690
|
-
@@modules[name] = proc
|
691
|
-
end
|
692
|
-
|
693
|
-
#--
|
694
|
-
# Properties and shortcuts
|
695
|
-
#++
|
696
|
-
|
697
|
-
# Takes +msg+ as parameter and formats it into the standard response type
|
698
|
-
# expected by an action's caller. This format is as follows:
|
699
|
-
#
|
700
|
-
# {:status => http_status_code, :body => json_encoded_body}
|
701
|
-
#
|
702
|
-
# The methods +standard_response+, +success+, and +ok+ all handle any
|
703
|
-
# textual message and puts it in the body field, defaulting to the 200
|
704
|
-
# response class status code.
|
705
|
-
def standard_response(body = 'OK')
|
706
|
-
{:status => 200, :body => body}
|
707
|
-
end
|
708
|
-
alias_method :success, :standard_response
|
709
|
-
alias_method :ok, :standard_response
|
710
|
-
|
711
|
-
# Similar to the +standard_response+ method, takes input and responds
|
712
|
-
# accordingly, which is by raising an exception (which handles formatting
|
713
|
-
# the response in the normal response hash).
|
714
|
-
def not_found(body = 'Not Found')
|
715
|
-
body = 'Not Found' if body.is_a?(Hash) && body.empty?
|
716
|
-
raise Exceptions::NotFound.new(404, body)
|
717
|
-
end
|
718
|
-
|
719
|
-
# Returns the params of the current request, set in the +run+ method.
|
720
|
-
def params
|
721
|
-
@params
|
722
|
-
end
|
723
|
-
|
724
|
-
# Returns the params following the ? in a given URL as a hash
|
725
|
-
def query_params
|
726
|
-
@env['QUERY_STRING'].split(/&/).inject({}){|h,kp| k,v = kp.split(/=/); h[k] = v; h}.symbolize_keys!
|
727
|
-
end
|
728
|
-
|
729
|
-
# Returns the URI requested
|
730
|
-
def uri
|
731
|
-
# special parsing is done to remove the protocol, host, and port that
|
732
|
-
# some Handlers leave in there. (Fixes inconsistencies.)
|
733
|
-
URI.parse(@env['REQUEST_URI'] || @env['PATH_INFO']).path
|
734
|
-
end
|
735
|
-
|
736
|
-
# Returns the Request Method as a lowercase symbol.
|
737
|
-
#
|
738
|
-
# One useful situation for this method would be similar to this:
|
739
|
-
#
|
740
|
-
# case method
|
741
|
-
# when :get
|
742
|
-
# # perform reading operations
|
743
|
-
# when :post
|
744
|
-
# # perform updating operations
|
745
|
-
# when :put
|
746
|
-
# # perform creating operations
|
747
|
-
# when :delete
|
748
|
-
# # perform deleting options
|
749
|
-
# end
|
750
|
-
#
|
751
|
-
# It can also be used in many other cases, like throwing an exception if
|
752
|
-
# an action is called with an unexpected method.
|
753
|
-
def method
|
754
|
-
@env['REQUEST_METHOD'].downcase.to_sym
|
755
|
-
end
|
756
|
-
|
757
|
-
# Returns the POST data hash, making the keys symbols first.
|
758
|
-
#
|
759
|
-
# Use like <tt>post[:post_param]</tt>.
|
760
|
-
def post
|
761
|
-
@req.POST.symbolize_keys!
|
762
|
-
end
|
763
|
-
|
764
|
-
# Returns the GET data hash, making the keys symbols first.
|
765
|
-
#
|
766
|
-
# Use like <tt>get[:get_param]</tt>.
|
767
|
-
def get
|
768
|
-
@req.GET.symbolize_keys!
|
769
|
-
end
|
770
|
-
|
771
|
-
end
|
772
|
-
|
773
|
-
end
|
774
|
-
end
|