halcyon 0.4.0 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
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