Sutto-perennial 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,97 @@
1
+ require 'singleton'
2
+
3
+ module Perennial
4
+ class Loader
5
+ include Singleton
6
+ include Perennial::Hookable
7
+
8
+ cattr_accessor :controllers, :current_type, :default_type
9
+ @@controllers = []
10
+
11
+ define_hook :before_run, :after_stop
12
+
13
+ def self.register_controller(name, controller)
14
+ return if name.blank? || controller.blank?
15
+ name = name.to_sym
16
+ @@controller[name] = controller
17
+ metaclass.class_eval(<<-RUBY, __FILE__, __LINE__ + 1)
18
+ def #{name}? # def client?
19
+ @@current_type == :#{name} # @@current_type == :client
20
+ end # end
21
+ RUBY
22
+ end
23
+
24
+ def self.run!(type = self.default_type)
25
+ @@current_type = type.to_sym
26
+ self.instance.run!
27
+ end
28
+
29
+ def self.stop!(force = false)
30
+ self.instance.stop!(force)
31
+ end
32
+
33
+ def run!
34
+ self.register_signals
35
+ OptionParser.setup
36
+ self.class.invoke_hooks! :before_setup
37
+ Daemon.daemonize! if Settings.daemon?
38
+ Logger.log_name = "#{@@current_type.to_s}.log"
39
+ Logger.setup
40
+ Settings.setup
41
+ self.load_custom_code
42
+ self.class.invoke_hooks! :before_run
43
+ self.attempt_controller_action! :run
44
+ end
45
+
46
+ def stop!(force = false)
47
+ if force || !@attempted_stop
48
+ self.class.invoke_hooks! :before_stop
49
+ self.attempt_controller_action! :stop
50
+ self.class.invoke_hooks! :after_stop
51
+ @attempted_stop = true
52
+ end
53
+ Daemon.cleanup! if Settings.daemon?
54
+ end
55
+
56
+ def current_controller
57
+ @current_controller ||= @@controllers[@@current_type.to_sym]
58
+ end
59
+
60
+ protected
61
+
62
+ def load_custom_code
63
+ # Attempt to load a setup file given it exists.
64
+ begin
65
+ config_dir = Settings.root / "config"
66
+ setup_file = config_dir / "setup.rb"
67
+ require(setup_file) if File.directory?(handler_directory) && File.exist?(setup_file)
68
+ rescue LoadError
69
+ end
70
+ # Load any existing handlers assuming we can find the folder
71
+ handler_directory = Settings.root / "handlers"
72
+ if File.directory?(handler_directory)
73
+ Dir[handler_directory / "**" / "*.rb"].each do |handler|
74
+ require handler
75
+ end
76
+ end
77
+ end
78
+
79
+ def register_signals
80
+ loader = self.class
81
+ %w(INT TERM).each do |signal|
82
+ trap(signal) do
83
+ loader.stop!
84
+ exit
85
+ end
86
+ end
87
+ end
88
+
89
+ def attempt_controller_action!(action)
90
+ action = action.to_sym
91
+ unless current_controller.blank? || !current_controller.respond_to?(action)
92
+ current_controller.send(action)
93
+ end
94
+ end
95
+
96
+ end
97
+ end
@@ -0,0 +1,15 @@
1
+ module Perennial
2
+ # A mixin that provides logger instance and
3
+ # class methods
4
+ module Loggable
5
+
6
+ def self.included(parent)
7
+ parent.extend self
8
+ end
9
+
10
+ def logger
11
+ Logger
12
+ end
13
+
14
+ end
15
+ end
@@ -0,0 +1,106 @@
1
+ module Perennial
2
+ class Logger
3
+
4
+ cattr_accessor :logger, :log_name
5
+
6
+ @@log_name = "perennial.log"
7
+ @@setup = false
8
+
9
+ class << self
10
+
11
+ def setup?
12
+ !!@@setup
13
+ end
14
+
15
+ def setup
16
+ return if setup?
17
+ setup!
18
+ end
19
+
20
+ def setup!
21
+ log_path = Settings.root / "log" / @@log_name.to_str
22
+ @@logger = new(log_path, Settings.log_level, Settings.verbose?)
23
+ @@setup = true
24
+ end
25
+
26
+ def method_missing(name, *args, &blk)
27
+ self.setup # Ensure the logger is setup
28
+ @@logger.send(name, *args, &blk)
29
+ end
30
+
31
+ def respond_to?(symbol, include_private = false)
32
+ self.setup
33
+ super(symbol, include_private) || @@logger.respond_to?(symbol, include_private)
34
+ end
35
+
36
+ end
37
+
38
+ LEVELS = {
39
+ :fatal => 7,
40
+ :error => 6,
41
+ :warn => 4,
42
+ :info => 3,
43
+ :debug => 0
44
+ }
45
+
46
+ PREFIXES = {}
47
+
48
+ LEVELS.each { |k,v| PREFIXES[k] = "[#{k.to_s.upcase}]".ljust 7 }
49
+
50
+ COLOURS = {
51
+ :fatal => 31, # red
52
+ :error => 33, # yellow
53
+ :warn => 35, # magenta
54
+ :info => 32, # green
55
+ :debug => 34 # white
56
+ }
57
+
58
+ attr_accessor :level, :file, :verbose
59
+
60
+ def initialize(path, level = :info, verbose = false)
61
+ @level = level.to_sym
62
+ @verbose = verbose
63
+ @file = File.open(path, "a+")
64
+ end
65
+
66
+ def close!
67
+ @file.close
68
+ end
69
+
70
+ LEVELS.each do |name, value|
71
+ define_method(name) do |message|
72
+ write("#{PREFIXES[name]} #{message}", name) if LEVELS[@level] <= value
73
+ end
74
+
75
+ define_method(:"#{name}?") do
76
+ LEVELS[@level] <= value
77
+ end
78
+
79
+ end
80
+
81
+ def log_exception(exception)
82
+ error "Exception: #{exception}"
83
+ exception.backtrace.each do |l|
84
+ error ">> #{l}"
85
+ end
86
+ end
87
+
88
+ def verbose?
89
+ !!@vebose
90
+ end
91
+
92
+ private
93
+
94
+ def write(message, level = self.level)
95
+ @file.puts message
96
+ @file.flush
97
+ $stdout.puts colourize(message, level) if verbose?
98
+ end
99
+
100
+ def colourize(message, level)
101
+ "\033[1;#{COLOURS[level]}m#{message}\033[0m"
102
+ end
103
+
104
+
105
+ end
106
+ end
@@ -0,0 +1,32 @@
1
+ module Perennial
2
+ class Manifest
3
+
4
+ class_inheritable_accessor :app_name, :namespace
5
+ self.app_name = :perennial
6
+ self.namespace = Perennial
7
+
8
+ def self.inspect
9
+ "#<#{self.name} app_name: #{self.app_name.inspect}, namespace: #{self.namespace.inspect}>"
10
+ end
11
+
12
+ module Mixin
13
+ # Called in your application to set the default
14
+ # namespace and app_name. Also, if a block is
15
+ # provided it yields first with Manifest and then
16
+ # with the Loader class, making it simpler to setup.
17
+ def manifest(&blk)
18
+ Manifest.namespace = self
19
+ Manifest.app_name = self.name.to_s.underscore
20
+ parent_folder = __DIR__(1)
21
+ attempt_require parent_folder / 'core_ext', parent_folder / 'exceptions'
22
+ unless blk.nil?
23
+ args = []
24
+ args << Manifest if blk.arity != 0
25
+ args << Loader if blk.arity > 1 || blk.arity < 0
26
+ blk.call(*args)
27
+ end
28
+ end
29
+ end
30
+
31
+ end
32
+ end
@@ -0,0 +1,108 @@
1
+ require 'singleton'
2
+ module Perennial
3
+ class OptionParser
4
+ include Singleton
5
+
6
+ attr_reader :arguments
7
+
8
+ def initialize
9
+ @parsed_values = {}
10
+ @arguments = []
11
+ @callbacks = {}
12
+ @descriptions = {}
13
+ @shortcuts = {}
14
+ end
15
+
16
+ def add(name, description, opts = {}, &blk)
17
+ name = name.to_sym
18
+ @callbacks[name] = blk
19
+ @descriptions[name] = description
20
+ shortcut = opts.has_key?(:shortcut) ? opts[:shortcut] : generate_default_shortcut(name)
21
+ @shortcuts[shortcut] = name unless shortcut.blank?
22
+ end
23
+
24
+ def summary
25
+ output = []
26
+ max_length = 0
27
+ @callbacks.each_key do |name|
28
+ shortcuts = []
29
+ @shortcuts.each_pair { |k,v| shortcuts << k if v == name }
30
+ text = "--#{name.to_s.gsub("_", "-")}"
31
+ text << ", #{shortcuts.map { |sc| "-#{sc}" }.join(", ")}" unless shortcuts.empty?
32
+ max_length = [text.size, max_length].max
33
+ output << [text, @descriptions[name]]
34
+ end
35
+ output.map { |text, description| "#{text}: ".ljust(max_length + 2) + description }.join("\n")
36
+ end
37
+
38
+ def parse(arguments = ARGV)
39
+ arguments, options = ArgumentParser.parse(arguments)
40
+ @arguments = arguments
41
+ options.each_pair do |name, value|
42
+ name = name.gsub("-", "_")
43
+ expanded_name = @shortcuts[name] || name.to_sym
44
+ callback = @callbacks[expanded_name]
45
+ callback.call(value) if callback.present?
46
+ end
47
+ return nil
48
+ end
49
+
50
+ def self.method_missing(name, *args, &blk)
51
+ self.instance.send(name, *args, &blk)
52
+ end
53
+
54
+ # Over ride with your apps custom banner
55
+ def self.print_banner
56
+ end
57
+
58
+ def add_defaults!
59
+ return if defined?(@defaults_added) && @defaults_added
60
+ logger_levels = Logger::LEVELS.keys.map { |k| k.to_s }
61
+ add(:daemon, 'Runs this application as a daemon') { Settings.daemon = true }
62
+ add(:verbose, 'Runs this application verbosely, writing to STDOUT') { Settings.verbose = true }
63
+ add(:log_level, "Sets this applications log level, one of: #{logger_levels.join(", ")}") do |level|
64
+ if logger_levels.include?(level)
65
+ Settings.log_level = level.to_sym
66
+ else
67
+ puts "The provided log level must be one of #{logger_levels.join(", ")} (Given #{level})"
68
+ exit!
69
+ end
70
+ end
71
+ add(:help, "Shows this help message") do
72
+ self.print_banner
73
+ $stdout.puts "Usage: #{$0} [options]"
74
+ $stdout.puts "\nOptions:"
75
+ $stdout.puts self.summary
76
+ exit!
77
+ end
78
+ @defaults_added = true
79
+ end
80
+
81
+ def setup
82
+ return if defined?(@setup) && @setup
83
+ setup!
84
+ @setup = true
85
+ end
86
+
87
+ def self.setup!
88
+ opts = self.instance
89
+ opts.add_defaults!
90
+ opts.parse
91
+ ARGV.replace opts.arguments
92
+ end
93
+
94
+ protected
95
+
96
+ def generate_default_shortcut(name)
97
+ raw = name.to_s[0, 1]
98
+ if !@shortcuts.has_key?(raw)
99
+ return raw
100
+ elsif !@shortcuts.has_key?(raw.upcase)
101
+ return raw.upcase
102
+ else
103
+ raise "No shortcut option could generate for '#{name}' - Please specify :short (possibly as nil) to override"
104
+ end
105
+ end
106
+
107
+ end
108
+ end
@@ -0,0 +1,87 @@
1
+ require 'yaml'
2
+
3
+ module Perennial
4
+ class Settings
5
+
6
+ cattr_accessor :configuration, :log_level, :verbose, :daemon
7
+
8
+ @@verbose = false
9
+ @@log_level = :info
10
+ @@daemon = false
11
+
12
+ class << self
13
+
14
+ def daemon?
15
+ !!@@daemon
16
+ end
17
+
18
+ def verbose?
19
+ !!@@verbose
20
+ end
21
+
22
+ def root=(path)
23
+ @@root = path.to_str
24
+ end
25
+
26
+ def root
27
+ @@root ||= File.expand_path(File.dirname(__FILE__) / ".." / "..")
28
+ end
29
+
30
+ def setup?
31
+ @@setup ||= false
32
+ end
33
+
34
+ def setup(options = {})
35
+ self.setup!(options) unless setup?
36
+ end
37
+
38
+ def setup!(options = {})
39
+ @@configuration = {}
40
+ settings_file = root / "config" / "settings.yml"
41
+ if File.exist?(settings_file)
42
+ loaded_yaml = YAML.load(File.read(settings_file))
43
+ @@configuration.merge! loaded_yaml["default"]
44
+ end
45
+ @@configuration.merge! options
46
+ @@configuration.symbolize_keys!
47
+ # Generate a module
48
+ mod = generate_settings_accessor_mixin
49
+ extend mod
50
+ include mod
51
+ @@setup = true
52
+ end
53
+
54
+ def [](key)
55
+ self.setup
56
+ return self.configuration[key.to_sym]
57
+ end
58
+
59
+ def []=(key, value)
60
+ self.setup
61
+ self.configuration[key.to_sym] = value
62
+ return value
63
+ end
64
+
65
+ def to_hash
66
+ self.configuration.dup
67
+ end
68
+
69
+ protected
70
+
71
+ def generate_settings_accessor_mixin
72
+ Module.new do
73
+ Settings.configuration.keys.each do |k|
74
+ define_method(k) do
75
+ return Settings.configuration[k]
76
+ end
77
+ define_method("#{k}=") do |val|
78
+ Settings.configuration[k] = val
79
+ end
80
+ end
81
+ end
82
+ end
83
+
84
+ end
85
+
86
+ end
87
+ end
data/lib/perennial.rb ADDED
@@ -0,0 +1,21 @@
1
+ # Append the perennial lib folder onto the load path to make it
2
+ # nicer to require perennial-related libraries.
3
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
4
+
5
+ require 'pathname'
6
+ require 'perennial/core_ext'
7
+ require 'perennial/exceptions'
8
+
9
+ module Perennial
10
+
11
+ VERSION = "0.1.0"
12
+
13
+ has_libary :dispatchable, :hookable, :loader, :logger,
14
+ :loggable, :manifest, :settings, :argument_parser,
15
+ :option_parser
16
+
17
+ def self.included(parent)
18
+ parent.extend(Manifest::Mixin)
19
+ end
20
+
21
+ end
@@ -0,0 +1,129 @@
1
+ require File.join(File.dirname(__FILE__), "test_helper")
2
+
3
+ class DispatchableTest < Test::Unit::TestCase
4
+
5
+ class ExampleDispatcher
6
+ include Perennial::Dispatchable
7
+ end
8
+
9
+ class ExampleHandlerA
10
+
11
+ attr_accessor :messages
12
+
13
+ def initialize
14
+ @messages = []
15
+ end
16
+
17
+ def handle(name, opts)
18
+ @messages << [name, opts]
19
+ end
20
+
21
+ end
22
+
23
+ class ExampleHandlerB < ExampleHandlerA; end
24
+
25
+ context 'marking a class as dispatchable' do
26
+
27
+ setup do
28
+ @dispatcher = ExampleDispatcher.new
29
+ end
30
+
31
+ should 'define a dispatch method' do
32
+ assert @dispatcher.respond_to?(:dispatch)
33
+ end
34
+
35
+ should 'require atleast a name for dispatch' do
36
+ assert_equal -2, @dispatcher.method(:dispatch).arity
37
+ end
38
+
39
+ end
40
+
41
+ context 'when registering handlers' do
42
+
43
+ setup do
44
+ @dispatcher = test_class_for(Perennial::Dispatchable)
45
+ end
46
+
47
+ should 'append a handler using register_handler' do
48
+ assert_equal [], @dispatcher.handlers
49
+ @dispatcher.register_handler(handler = ExampleHandlerA.new)
50
+ assert_equal [handler], @dispatcher.handlers
51
+ end
52
+
53
+ should 'batch assign handlers on handlers= using register_handler' do
54
+ handlers = [ExampleHandlerA.new, ExampleHandlerB.new]
55
+ assert_equal [], @dispatcher.handlers
56
+ @dispatcher.handlers = handlers
57
+ assert_equal handlers, @dispatcher.handlers
58
+ end
59
+
60
+ should 'return all handlers via the handlers class method' do
61
+ handlers = [ExampleHandlerA.new, ExampleHandlerB.new]
62
+ @dispatcher.handlers = handlers
63
+ assert_equal handlers, @dispatcher.handlers
64
+ end
65
+
66
+ should 'make handlers available to myself and all subclasses' do
67
+ # Set A
68
+ dispatcher_a = class_via(@dispatcher)
69
+ dispatcher_a.register_handler(handler_a = ExampleHandlerA.new)
70
+ # Set B
71
+ dispatcher_b = class_via(dispatcher_a)
72
+ dispatcher_b.register_handler(handler_b = ExampleHandlerA.new)
73
+ # Set C
74
+ dispatcher_c = class_via(dispatcher_b)
75
+ dispatcher_c.register_handler(handler_c = ExampleHandlerA.new)
76
+ # Set D
77
+ dispatcher_d = class_via(dispatcher_a)
78
+ dispatcher_d.register_handler(handler_d = ExampleHandlerB.new)
79
+ # Actual Assertions
80
+ assert_equal [], @dispatcher.handlers
81
+ assert_equal [handler_a], dispatcher_a.handlers
82
+ assert_equal [handler_a, handler_b], dispatcher_b.handlers
83
+ assert_equal [handler_a, handler_b, handler_c], dispatcher_c.handlers
84
+ assert_equal [handler_a, handler_d], dispatcher_d.handlers
85
+ end
86
+
87
+ end
88
+
89
+ context 'dispatching events' do
90
+
91
+ setup do
92
+ @dispatcher = class_via(ExampleDispatcher).new
93
+ @handler = ExampleHandlerA.new
94
+ @dispatcher.class.register_handler @handler
95
+ end
96
+
97
+ should 'attempt to call handle_[event_name] on itself' do
98
+ mock(@dispatcher).respond_to?(:handle_sample_event) { true }
99
+ mock(@dispatcher).handle_sample_event(:awesome => true, :sauce => 2)
100
+ @dispatcher.dispatch :sample_event, :awesome => true, :sauce => 2
101
+ end
102
+
103
+ should 'attempt to call handle_[event_name] on each handler' do
104
+ mock(@handler).respond_to?(:handle_sample_event) { true }
105
+ mock(@handler).handle_sample_event(:awesome => true, :sauce => 2)
106
+ @dispatcher.dispatch :sample_event, :awesome => true, :sauce => 2
107
+ end
108
+
109
+ should 'call handle on each handler if handle_[event_name] isn\'t defined' do
110
+ mock(@handler).respond_to?(:handle_sample_event) { false }
111
+ mock(@handler).handle(:sample_event, :awesome => true, :sauce => 2)
112
+ @dispatcher.dispatch :sample_event, :awesome => true, :sauce => 2
113
+ end
114
+
115
+ should 'let you halt handler processing if you raise HaltHandlerProcessing' do
116
+ handler_two = ExampleHandlerB.new
117
+ @dispatcher.class.register_handler handler_two
118
+ mock(@handler).handle(:sample_event, :awesome => true, :sauce => 2) do
119
+ raise Perennial::HaltHandlerProcessing
120
+ end
121
+ dont_allow(handler_two).handle(:sample_event, :awesome => true, :sauce => 2)
122
+ @dispatcher.dispatch :sample_event, :awesome => true, :sauce => 2
123
+ end
124
+
125
+ should 'log exceptions when encountered and not crash'
126
+
127
+ end
128
+
129
+ end
@@ -0,0 +1,61 @@
1
+ require File.join(File.dirname(__FILE__), "test_helper")
2
+
3
+ class HookableTest < Test::Unit::TestCase
4
+
5
+ context 'Hookable Classes' do
6
+
7
+ setup do
8
+ @hookable_class = test_class_for(Perennial::Hookable)
9
+ end
10
+
11
+ should 'let you append hooks via append_hook' do
12
+ assert_equal [], @hookable_class.hooks_for(:awesome)
13
+ @hookable_class.append_hook(:awesome) { puts "Hello!" }
14
+ assert_equal 1, @hookable_class.hooks_for(:awesome).size
15
+ end
16
+
17
+ should 'only append hooks if they aren\'t blank' do
18
+ @hookable_class.append_hook(:awesome)
19
+ assert_equal [], @hookable_class.hooks_for(:awesome)
20
+ end
21
+
22
+ should 'let you get an array of hooks' do
23
+ @hookable_class.append_hook(:awesome) { puts "A" }
24
+ @hookable_class.append_hook(:awesome) { puts "B" }
25
+ assert_equal 2, @hookable_class.hooks_for(:awesome).size
26
+ end
27
+
28
+ should 'let you invoke hooks' do
29
+ items = []
30
+ @hookable_class.append_hook(:awesome) { items << :a }
31
+ @hookable_class.append_hook(:awesome) { items << :b }
32
+ @hookable_class.append_hook(:awesome) { items << :c }
33
+ @hookable_class.invoke_hooks!(:awesome)
34
+ assert_equal [:a, :b, :c], items
35
+ end
36
+
37
+ should 'call them in the order they are appended' do
38
+ items = []
39
+ @hookable_class.append_hook(:awesome) { items << :a }
40
+ @hookable_class.append_hook(:awesome) { items << :b }
41
+ @hookable_class.append_hook(:awesome) { items << :c }
42
+ @hookable_class.invoke_hooks!(:awesome)
43
+ [:a, :b, :c].each_with_index do |value, index|
44
+ assert_equal value, items[index]
45
+ end
46
+ end
47
+
48
+ should 'let you define hook accessors' do
49
+ assert_equal [], @hookable_class.hooks_for(:awesome)
50
+ assert !@hookable_class.respond_to?(:awesome)
51
+ assert !@hookable_class.respond_to?(:sauce)
52
+ @hookable_class.define_hook :awesome, :sauce
53
+ assert @hookable_class.respond_to?(:awesome)
54
+ assert @hookable_class.respond_to?(:sauce)
55
+ @hookable_class.awesome { puts "A" }
56
+ assert_equal 1, @hookable_class.hooks_for(:awesome).size
57
+ end
58
+
59
+ end
60
+
61
+ end
@@ -0,0 +1 @@
1
+ require File.join(File.dirname(__FILE__), "test_helper")
@@ -0,0 +1,38 @@
1
+ require File.join(File.dirname(__FILE__), "test_helper")
2
+
3
+ class LoggableTest < Test::Unit::TestCase
4
+
5
+ class ExampleLoggable
6
+ include Perennial::Loggable
7
+ end
8
+
9
+ context "Defining a class as loggable" do
10
+
11
+ setup do
12
+ @example = ExampleLoggable.new
13
+ end
14
+
15
+ should 'define a logger instance method' do
16
+ assert @example.respond_to?(:logger)
17
+ end
18
+
19
+ should 'define a logger class method' do
20
+ assert ExampleLoggable.respond_to?(:logger)
21
+ end
22
+
23
+ should 'not define a logger= instance method' do
24
+ assert !@example.respond_to?(:logger=)
25
+ end
26
+
27
+ should 'not define a logger= class method' do
28
+ assert !ExampleLoggable.respond_to?(:logger=)
29
+ end
30
+
31
+ should 'define logger to be an instance of Perennial::Logger' do
32
+ assert_equal Perennial::Logger, ExampleLoggable.logger
33
+ assert_equal Perennial::Logger, @example.logger
34
+ end
35
+
36
+ end
37
+
38
+ end