halcyon 0.4.0 → 0.5.0
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/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
|