Sutto-perennial 0.1.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.
@@ -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