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.
Files changed (60) hide show
  1. data/AUTHORS +1 -0
  2. data/LICENSE +20 -0
  3. data/README +107 -0
  4. data/Rakefile +8 -6
  5. data/bin/halcyon +3 -204
  6. data/lib/halcyon.rb +55 -42
  7. data/lib/halcyon/application.rb +247 -0
  8. data/lib/halcyon/application/router.rb +86 -0
  9. data/lib/halcyon/client.rb +187 -35
  10. data/lib/halcyon/client/ssl.rb +38 -0
  11. data/lib/halcyon/controller.rb +154 -0
  12. data/lib/halcyon/exceptions.rb +67 -59
  13. data/lib/halcyon/logging.rb +31 -0
  14. data/lib/halcyon/logging/analogger.rb +31 -0
  15. data/lib/halcyon/logging/helpers.rb +37 -0
  16. data/lib/halcyon/logging/log4r.rb +25 -0
  17. data/lib/halcyon/logging/logger.rb +20 -0
  18. data/lib/halcyon/logging/logging.rb +19 -0
  19. data/lib/halcyon/runner.rb +141 -0
  20. data/lib/halcyon/runner/commands.rb +141 -0
  21. data/lib/halcyon/runner/helpers.rb +9 -0
  22. data/lib/halcyon/runner/helpers/command_helper.rb +71 -0
  23. data/spec/halcyon/application_spec.rb +70 -0
  24. data/spec/halcyon/client_spec.rb +63 -0
  25. data/spec/halcyon/controller_spec.rb +68 -0
  26. data/spec/halcyon/halcyon_spec.rb +63 -0
  27. data/spec/halcyon/logging_spec.rb +31 -0
  28. data/spec/halcyon/router_spec.rb +37 -12
  29. data/spec/halcyon/runner_spec.rb +54 -0
  30. data/spec/spec_helper.rb +75 -9
  31. data/support/generators/halcyon/USAGE +0 -0
  32. data/support/generators/halcyon/halcyon_generator.rb +52 -0
  33. data/support/generators/halcyon/templates/README +26 -0
  34. data/support/generators/halcyon/templates/Rakefile +32 -0
  35. data/support/generators/halcyon/templates/app/application.rb +43 -0
  36. data/support/generators/halcyon/templates/config/config.yml +36 -0
  37. data/support/generators/halcyon/templates/config/init/environment.rb +11 -0
  38. data/support/generators/halcyon/templates/config/init/hooks.rb +39 -0
  39. data/support/generators/halcyon/templates/config/init/requires.rb +10 -0
  40. data/support/generators/halcyon/templates/config/init/routes.rb +50 -0
  41. data/support/generators/halcyon/templates/lib/client.rb +77 -0
  42. data/support/generators/halcyon/templates/runner.ru +8 -0
  43. data/support/generators/halcyon_flat/USAGE +0 -0
  44. data/support/generators/halcyon_flat/halcyon_flat_generator.rb +52 -0
  45. data/support/generators/halcyon_flat/templates/README +26 -0
  46. data/support/generators/halcyon_flat/templates/Rakefile +32 -0
  47. data/support/generators/halcyon_flat/templates/app.rb +49 -0
  48. data/support/generators/halcyon_flat/templates/lib/client.rb +17 -0
  49. data/support/generators/halcyon_flat/templates/runner.ru +8 -0
  50. metadata +73 -20
  51. data/lib/halcyon/client/base.rb +0 -261
  52. data/lib/halcyon/client/exceptions.rb +0 -41
  53. data/lib/halcyon/client/router.rb +0 -106
  54. data/lib/halcyon/server.rb +0 -62
  55. data/lib/halcyon/server/auth/basic.rb +0 -107
  56. data/lib/halcyon/server/base.rb +0 -774
  57. data/lib/halcyon/server/exceptions.rb +0 -41
  58. data/lib/halcyon/server/router.rb +0 -103
  59. data/spec/halcyon/error_spec.rb +0 -55
  60. data/spec/halcyon/server_spec.rb +0 -105
@@ -0,0 +1,25 @@
1
+ require 'log4r'
2
+ include Log4r
3
+ module Halcyon
4
+ module Logging
5
+ class Log4r < Log4r::Logger
6
+
7
+ class << self
8
+
9
+ def setup(config)
10
+ logger = self.new(config[:label] || Halcyon.app)
11
+ if config[:file]
12
+ logger.outputters = Log4r::FileOutputter.new(:filename => config[:file])
13
+ else
14
+ logger.outputters = Log4r::Outputter.stdout
15
+ end
16
+ logger.level = Object.const_get((config[:level] || 'debug').upcase.to_sym)
17
+ logger.outputters[0].formatter = Log4r::PatternFormatter.new(:pattern => "%5l [%d] (#{$$}) #{Halcyon.app} :: %m\n", :date_pattern => "%Y-%m-%d %H:%M:%S")
18
+ logger
19
+ end
20
+
21
+ end
22
+
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,20 @@
1
+ require 'logger'
2
+ module Halcyon
3
+ module Logging
4
+ class Logger < ::Logger
5
+
6
+ class << self
7
+
8
+ def setup(config)
9
+ logger = config[:logger] || self.new(config[:file] || STDOUT)
10
+ logger.formatter = proc{|s,t,p,m|"%5s [%s] (%s) %s :: %s\n" % [s, t.strftime("%Y-%m-%d %H:%M:%S"), $$, p, m]}
11
+ logger.progname = Halcyon.app
12
+ logger.level = Logger.const_get((config[:level] || 'info').upcase)
13
+ logger
14
+ end
15
+
16
+ end
17
+
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,19 @@
1
+ require 'logging'
2
+ module Halcyon
3
+ module Logging
4
+ class Logging < Logging::Logger
5
+
6
+ class << self
7
+
8
+ def setup(config)
9
+ logger = config[:logger] || ::Logging.logger(config[:file] || STDOUT)
10
+ logger.level = config[:level].downcase.to_sym
11
+ logger.instance_variable_get("@appenders")[0].instance_variable_set("@layout", ::Logging::Layouts::Pattern.new(:pattern => "%5l [%d] (%p) #{Halcyon.app} :: %m\n", :date_pattern => "%Y-%m-%d %H:%M:%S"))
12
+ logger
13
+ end
14
+
15
+ end
16
+
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,141 @@
1
+ module Halcyon
2
+
3
+ # Handles initializing and running the application, including:
4
+ # * setting up the logger
5
+ # * loading initializers
6
+ # * loading controllers
7
+ #
8
+ # The Runner is a full-fledged Rack application, and accepts calls to #call.
9
+ #
10
+ # Also handles running commands form the command line.
11
+ #
12
+ # Examples
13
+ # # start serving the current app (in .)
14
+ # Halcyon::Runner.run!(['start', '-p', '4647'])
15
+ #
16
+ # # load the config file and initialize the app
17
+ # Halcyon::Runner.load_config Halcyon.root/'config'/'config.yml'
18
+ # Halcyon::Runner.new
19
+ class Runner
20
+
21
+ autoload :Commands, 'halcyon/runner/commands'
22
+
23
+ class << self
24
+
25
+ # Runs commands from the CLI.
26
+ # +argv+ the arguments to pass to the commands
27
+ #
28
+ # Returns nothing
29
+ def run!(argv=ARGV)
30
+ Commands.send(argv.shift, argv)
31
+ end
32
+
33
+ # Returns the path to the configuration file specified, defaulting
34
+ # to the path for the <tt>config.yml</tt> file.
35
+ # +file+ the name of the config file path (without the <tt>.yml</tt>
36
+ # extension)
37
+ def config_path(file = "config")
38
+ Halcyon.paths[:config]/"#{file}.yml"
39
+ end
40
+
41
+ end
42
+
43
+ # Initializes the application and application resources.
44
+ def initialize
45
+ Halcyon::Runner.load_paths if Halcyon.paths.nil?
46
+
47
+ # Load the configuration if none is set already
48
+ if Halcyon.config.nil?
49
+ if File.exist?(Halcyon::Runner.config_path)
50
+ Halcyon.config = Halcyon::Runner.load_config
51
+ else
52
+ Halcon.config = Halcyon::Application::DEFAULT_OPTIONS
53
+ end
54
+ end
55
+
56
+ # Set application name
57
+ Halcyon.app = Halcyon.config[:app] || Halcyon.root.split('/').last.camel_case
58
+
59
+ # Setup logger
60
+ if Halcyon.config[:logger]
61
+ Halcyon.config[:logging] = (Halcyon.config[:logging] || Halcyon::Application::DEFAULT_OPTIONS[:logging]).merge({
62
+ :type => Halcyon.config[:logger].class.to_s,
63
+ :logger => Halcyon.config[:logger]
64
+ })
65
+ end
66
+ Halcyon::Logging.set((Halcyon.config[:logging][:type] rescue nil))
67
+ Halcyon.logger = Halcyon::Logger.setup(Halcyon.config[:logging])
68
+
69
+ # Run initializers
70
+ Dir.glob(Halcyon.paths[:init]/'{requires,hooks,routes,environment,*}.rb').each do |initializer|
71
+ self.logger.debug "Init: #{File.basename(initializer).chomp('.rb').camel_case}" if
72
+ require initializer.chomp('.rb')
73
+ end
74
+
75
+ # Setup autoloads for Controllers found in Halcyon.root/'app'
76
+ Dir.glob(Halcyon.paths[:controller]/'{application,*}.rb').each do |controller|
77
+ self.logger.debug "Load: #{File.basename(controller).chomp('.rb').camel_case} Controller" if
78
+ require controller.chomp('.rb')
79
+ end
80
+
81
+ @app = Halcyon::Application.new
82
+ end
83
+
84
+ # Calls the application, which gets proxied to the dispatcher.
85
+ # +env+ the request environment details
86
+ #
87
+ # Returns [Fixnum:status, {String:header => String:value}, [String:body]]
88
+ def call(env)
89
+ @app.call(env)
90
+ end
91
+
92
+ class << self
93
+
94
+ # Loads the configuration file specified into <tt>Halcyon.config</tt>.
95
+ # +file+ the configuration file to load
96
+ #
97
+ # Examples
98
+ # Halcyon::Runner.load_config Halcyon.root/'config'/'config.yml'
99
+ # Halcyon.config #=> {:allow_from => :all, :logging => {...}, ...}.to_mash
100
+ #
101
+ # Returns {Symbol:key => String:value}.to_mash
102
+ def load_config(file=Halcyon::Runner.config_path)
103
+ if File.exist?(file)
104
+ require 'yaml'
105
+
106
+ # load the config file
107
+ begin
108
+ config = YAML.load_file(file).to_mash
109
+ rescue Errno::EACCES
110
+ raise LoadError.new("Can't access #{file}, try 'sudo #{$0}'")
111
+ end
112
+ else
113
+ warn "#{file} not found, ensure the path to this file is correct. Ignoring."
114
+ nil
115
+ end
116
+ end
117
+
118
+ # Set the paths for resources to be located.
119
+ #
120
+ # Used internally for setting the load paths if not manually overridden
121
+ # and needed to be set before normal application initialization.
122
+ #
123
+ # TODO: Move this to the planned <tt>Halcyon::Config</tt> object.
124
+ #
125
+ # Returns nothing.
126
+ def load_paths
127
+ # Set the default application paths, not overwriting manually set paths
128
+ Halcyon.paths = {
129
+ :controller => Halcyon.root/'app',
130
+ :model => Halcyon.root/'app'/'models',
131
+ :lib => Halcyon.root/'lib',
132
+ :config => Halcyon.root/'config',
133
+ :init => Halcyon.root/'config'/'{init,initialize}',
134
+ :log => Halcyon.root/'log'
135
+ }.to_mash.merge(Halcyon.paths || {})
136
+ end
137
+
138
+ end
139
+
140
+ end
141
+ end
@@ -0,0 +1,141 @@
1
+ require 'optparse'
2
+
3
+ module Halcyon
4
+ class Runner
5
+
6
+ autoload :Helpers, 'halcyon/runner/helpers'
7
+
8
+ class Commands
9
+ class << self
10
+
11
+ # Run the Halcyon application
12
+ def start(argv)
13
+ options = {
14
+ :port => 4647,
15
+ :server => (Gem.searcher.find('thin').nil? ? 'mongrel' : 'thin')
16
+ }
17
+
18
+ OptionParser.new do |opts|
19
+ opts.banner = "Usage: halcyon start [options]"
20
+
21
+ opts.separator ""
22
+ opts.separator "Start options:"
23
+ opts.on("-s", "--server SERVER", "") { |server| options[:server] = server }
24
+
25
+ begin
26
+ opts.parse! argv
27
+ rescue OptionParser::InvalidOption => e
28
+ # the other options can be used elsewhere, like in RubiGen
29
+ argv = e.recover(argv)
30
+ end
31
+ end
32
+
33
+ if options[:server] == 'thin'
34
+ # Thin is installed
35
+ command = "thin start -R runner.ru #{argv.join(' ')}"
36
+ else
37
+ # Thin is not installed
38
+ command = "rackup runner.ru -s #{options[:server]} #{argv.join(' ')}"
39
+ end
40
+
41
+ # run command
42
+ exec command
43
+ end
44
+
45
+ # Start the Halcyon server up in interactive mode
46
+ def console(argv)
47
+ # Notify user of environment
48
+ puts "(Starting Halcyon app in console...)"
49
+
50
+ # Add ./lib to load path
51
+ $:.unshift(Halcyon.root/'lib')
52
+
53
+ # prepare environment for IRB
54
+ ARGV.clear
55
+ require 'rack/mock'
56
+ require 'logger'
57
+ require 'irb'
58
+ require 'irb/completion'
59
+ if File.exists? '.irbrc'
60
+ ENV['IRBRC'] = '.irbrc'
61
+ end
62
+
63
+ # Set up the application
64
+ Object.instance_eval do
65
+ $log = ''
66
+ Halcyon::Runner.load_paths if Halcyon.paths.nil?
67
+ (Halcyon.config = Halcyon::Runner.load_config) || require(Halcyon.root/'app')
68
+ Halcyon.config[:logger] = Logger.new(StringIO.new($log))
69
+ $app = Halcyon::Runner.new
70
+ $response = nil
71
+ end
72
+
73
+ # Setup helper methods
74
+ Object.send(:include, Halcyon::Runner::Helpers::CommandHelper)
75
+
76
+ # Let users know what methods and values are available
77
+ puts "Call #usage for usage details."
78
+
79
+ # Start IRB session
80
+ IRB.start
81
+
82
+ exit
83
+ end
84
+ alias_method :interactive, :console
85
+ alias_method :irb, :console
86
+ alias_method :"-i", :console
87
+
88
+ # Generate a new Halcyon application
89
+ def init(argv)
90
+ app_name = argv.last
91
+
92
+ options = {
93
+ :generator => 'halcyon',
94
+ :git => false
95
+ }
96
+
97
+ OptionParser.new do |opts|
98
+ opts.banner = "Usage: halcyon init [options]"
99
+
100
+ opts.separator ""
101
+ opts.separator "Generator options:"
102
+ opts.on("-f", "--flat", "") { options[:generator] = 'halcyon_flat' }
103
+
104
+ opts.separator ""
105
+ opts.separator "Additional options:"
106
+ opts.on("-g", "--git", "Initialize a Git repository when finished generating") { options[:git] = true }
107
+ opts.on("-G", "--git-commit", "Initialize a Git repo and commit") { options[:git] = options[:git_commit] = true }
108
+
109
+ begin
110
+ opts.parse! argv
111
+ rescue OptionParser::InvalidOption => e
112
+ # the other options can be used elsewhere, like in RubiGen
113
+ end
114
+ end
115
+
116
+ require 'rubigen'
117
+ require 'rubigen/scripts/generate'
118
+ RubiGen::Base.use_application_sources!
119
+ RubiGen::Base.sources << RubiGen::PathSource.new(:custom, File.expand_path(File.join(File.dirname(__FILE__), '..', '..', '..', "support/generators")))
120
+ RubiGen::Scripts::Generate.new.run(argv, :generator => options[:generator])
121
+
122
+ # Create a Git repository in the new app dir
123
+ if options[:git]
124
+ system("cd #{app_name} && git init -q && cd #{Dir.pwd}")
125
+ puts "Initialized Git repository in #{app_name}/"
126
+ File.open(File.join("#{app_name}",'.gitignore'),"w") {|f| f << "log/*.log" }
127
+ File.open(File.join("#{app_name}",'log','.gitignore'),"w") {|f| f << "" }
128
+ end
129
+
130
+ # commit to the git repo
131
+ if options[:git_commit]
132
+ system("cd #{app_name} && git add . && git commit -m 'Initial import.' -q && cd #{Dir.pwd}")
133
+ puts "Committed empty application in #{app_name}/"
134
+ puts "Run `git commit --amend` to change the commit message."
135
+ end
136
+ end
137
+
138
+ end
139
+ end
140
+ end
141
+ end
@@ -0,0 +1,9 @@
1
+ module Halcyon
2
+ class Runner
3
+ class Helpers
4
+
5
+ autoload :CommandHelper, 'halcyon/runner/helpers/command_helper'
6
+
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,71 @@
1
+ module Halcyon
2
+ class Runner
3
+ class Helpers
4
+ module CommandHelper
5
+
6
+ def usage
7
+ msg = <<-"end;"
8
+
9
+ These methods will provide you with most of the
10
+ functionality you will need to test your app.
11
+
12
+ #app The loaded application
13
+ #log The contents of the log (Ex: puts log)
14
+ #tail The tail end of the log (Ex: tail)
15
+ #clear Clears the log (Ex: clear)
16
+ #get Sends a GET request to the app
17
+ Ex: get '/controller/action'
18
+ #post Sends a POST request to #app
19
+ Ex: post '/controller/action', :key => value
20
+ #put See #post
21
+ #delete See #get
22
+ #response Response of the last request
23
+
24
+ end;
25
+ puts msg.gsub(/^[ ]{12}/, '')
26
+ end
27
+
28
+ def app
29
+ $app
30
+ end
31
+
32
+ def log
33
+ $log
34
+ end
35
+
36
+ def tail
37
+ puts $log.split("\n").reverse[0..5].reverse.join("\n")
38
+ end
39
+
40
+ def clear
41
+ $log = ''
42
+ end
43
+
44
+ def get(path)
45
+ $response = Rack::MockRequest.new($app).get(path)
46
+ JSON.parse($response.body)
47
+ end
48
+
49
+ def post(path, params = {})
50
+ $response = Rack::MockRequest.new($app).post(path, :input => params.to_params)
51
+ JSON.parse($response.body)
52
+ end
53
+
54
+ def put(path, params = {})
55
+ $response = Rack::MockRequest.new($app).put(path, :input => params.to_params)
56
+ JSON.parse($response.body)
57
+ end
58
+
59
+ def delete(path)
60
+ $response = Rack::MockRequest.new($app).delete(path)
61
+ JSON.parse($response.body)
62
+ end
63
+
64
+ def response
65
+ $response
66
+ end
67
+
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,70 @@
1
+ describe "Halcyon::Application" do
2
+
3
+ before do
4
+ @log = ''
5
+ @logger = Logger.new(StringIO.new(@log))
6
+ @config = $config.dup
7
+ @config[:logger] = @logger
8
+ @config[:app] = 'Specs'
9
+ Halcyon.config = @config
10
+ @app = Halcyon::Runner.new
11
+ end
12
+
13
+ it "should run startup hook if defined" do
14
+ # $started is set by the startup hook
15
+ $started.should.be.true?
16
+ end
17
+
18
+ it "should dispatch methods according to their respective routes" do
19
+ Rack::MockRequest.new(@app).get("/hello/Matt")
20
+ @log.should =~ / INFO \[\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\] \(\d+\) Specs :: \[200\] \/hello\/Matt \(.+\)\n/
21
+ end
22
+
23
+ it "should handle requests and respond with JSON" do
24
+ body = JSON.parse(Rack::MockRequest.new(@app).get("/").body)
25
+ body['status'].should == 200
26
+ body['body'].should == "Found"
27
+ end
28
+
29
+ it "should handle requests with param values in the URL" do
30
+ body = JSON.parse(Rack::MockRequest.new(@app).get("/hello/Matt?test=value").body)
31
+ body['status'].should == 200
32
+ body['body'].should == "Hello Matt"
33
+ @log.split("\n").last.should =~ /"test"=>"value"/
34
+ end
35
+
36
+ it "should not dispatch private methods" do
37
+ body = JSON.parse(Rack::MockRequest.new(@app).get("/specs/undispatchable_private_method").body)
38
+ body['status'].should == 404
39
+ body['body'].should == "Not Found"
40
+ end
41
+
42
+ it "should route unmatchable requests to the default route and return JSON with appropriate status" do
43
+ body = JSON.parse(Rack::MockRequest.new(@app).get("/garbage/request/url").body)
44
+ body['status'].should == 404
45
+ body['body'].should == "Not Found"
46
+ end
47
+
48
+ it "should log activity" do
49
+ Halcyon.logger.is_a?(Logger).should.be.true?
50
+ Rack::MockRequest.new(@app).get("/lolcats/r/cute")
51
+ @log.should =~ / INFO \[\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\] \(\d+\) Specs :: \[404\] \/lolcats\/r\/cute \(.+\)\n/
52
+ end
53
+
54
+ it "should allow all requests by default" do
55
+ Halcyon.config[:allow_from].should == :all
56
+ end
57
+
58
+ it "should handle exceptions gracefully" do
59
+ body = JSON.parse(Rack::MockRequest.new(@app).get("/specs/cause_exception").body)
60
+ body['status'].should == 500
61
+ body['body'].should == "Internal Server Error"
62
+ end
63
+
64
+ it "should not confuse a NoMethodFound error in an action as a missing route" do
65
+ body = JSON.parse(Rack::MockRequest.new(@app).get("/specs/call_nonexistent_method").body)
66
+ body['status'].should.not == 404
67
+ body['body'].should == "Internal Server Error"
68
+ end
69
+
70
+ end