harbinger 0.0.1.pre → 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.
Files changed (63) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +3 -0
  3. data/.hound.yml +818 -0
  4. data/.travis.yml +20 -0
  5. data/Gemfile +23 -3
  6. data/README.md +8 -27
  7. data/Rakefile +47 -1
  8. data/app/controllers/harbinger/messages_controller.rb +24 -0
  9. data/app/models/harbinger/database_channel_message.rb +51 -0
  10. data/app/models/harbinger/database_channel_message_element.rb +19 -0
  11. data/app/views/harbinger/messages/index.html.erb +43 -0
  12. data/app/views/harbinger/messages/show.html.erb +24 -0
  13. data/config/routes.rb +3 -0
  14. data/db/migrate/20140310185338_create_harbinger_database_channel_message.rb +11 -0
  15. data/db/migrate/20140310185339_create_harbinger_database_channel_message_elements.rb +14 -0
  16. data/gemfiles/rails4.1.gemfile +12 -0
  17. data/gemfiles/rails4.gemfile +13 -0
  18. data/harbinger.gemspec +22 -7
  19. data/lib/generators/harbinger/install/install_generator.rb +23 -0
  20. data/lib/generators/harbinger/install/templates/harbinger_initializer.rb.erb +6 -0
  21. data/lib/harbinger.rb +100 -1
  22. data/lib/harbinger/channels.rb +25 -0
  23. data/lib/harbinger/channels/database_channel.rb +15 -0
  24. data/lib/harbinger/channels/logger_channel.rb +31 -0
  25. data/lib/harbinger/channels/null_channel.rb +7 -0
  26. data/lib/harbinger/configuration.rb +58 -0
  27. data/lib/harbinger/engine.rb +8 -1
  28. data/lib/harbinger/exceptions.rb +4 -0
  29. data/lib/harbinger/message.rb +20 -0
  30. data/lib/harbinger/reporters.rb +34 -0
  31. data/lib/harbinger/reporters/exception_reporter.rb +26 -0
  32. data/lib/harbinger/reporters/null_reporter.rb +14 -0
  33. data/lib/harbinger/reporters/request_reporter.rb +20 -0
  34. data/lib/harbinger/reporters/user_reporter.rb +20 -0
  35. data/lib/harbinger/version.rb +1 -1
  36. data/run_each_spec_in_isolation +9 -0
  37. data/script/fast_specs +20 -0
  38. data/spec/controllers/harbinger/messages_controller_spec.rb +26 -0
  39. data/spec/features/end_to_end_exception_handling_spec.rb +39 -0
  40. data/spec/lib/harbinger/channels/database_channel_spec.rb +18 -0
  41. data/spec/lib/harbinger/channels/logger_channel_spec.rb +21 -0
  42. data/spec/lib/harbinger/channels/null_channel_spec.rb +8 -0
  43. data/spec/lib/harbinger/channels_spec.rb +40 -0
  44. data/spec/lib/harbinger/configuration_spec.rb +53 -0
  45. data/spec/lib/harbinger/message_spec.rb +15 -0
  46. data/spec/lib/harbinger/reporters/exception_reporter_spec.rb +24 -0
  47. data/spec/lib/harbinger/reporters/null_reporter_spec.rb +21 -0
  48. data/spec/lib/harbinger/reporters/request_reporter_spec.rb +23 -0
  49. data/spec/lib/harbinger/reporters/user_reporter_spec.rb +17 -0
  50. data/spec/lib/harbinger/reporters_spec.rb +46 -0
  51. data/spec/lib/harbinger_spec.rb +60 -0
  52. data/spec/models/harbinger/database_channel_message_element_spec.rb +16 -0
  53. data/spec/models/harbinger/database_channel_message_spec.rb +68 -0
  54. data/spec/routing/harbinger/messages_routing_spec.rb +16 -0
  55. data/spec/spec_active_record_helper.rb +41 -0
  56. data/spec/spec_fast_helper.rb +70 -0
  57. data/spec/spec_slow_helper.rb +57 -0
  58. data/spec/spec_view_helper.rb +38 -0
  59. data/spec/test_app_templates/lib/generators/test_app_generator.rb +13 -0
  60. data/spec/views/harbinger/messages/index.html.erb_spec.rb +31 -0
  61. data/spec/views/harbinger/messages/show.html.erb_spec.rb +36 -0
  62. metadata +205 -20
  63. data/MIT-LICENSE +0 -20
@@ -1,5 +1,104 @@
1
- require "harbinger/engine"
1
+ require "harbinger/engine" if defined?(Rails)
2
2
  require "harbinger/version"
3
+ require "harbinger/reporters"
4
+ require "harbinger/channels"
5
+ require "harbinger/exceptions"
6
+ require "harbinger/configuration"
3
7
 
4
8
  module Harbinger
9
+ module_function
10
+ class << self
11
+ attr_writer :configuration
12
+
13
+ def configuration
14
+ @configuration ||= Configuration.new
15
+ end
16
+ end
17
+
18
+ module_function
19
+ def configure
20
+ yield(configuration)
21
+ end
22
+
23
+ # Responsible for building a :message from the various :contexts and then
24
+ # delivering the :message to the appropriate :channels.
25
+ #
26
+ # @see .build_message
27
+ # @see .deliver_message
28
+ #
29
+ # @param [Hash] options
30
+ # @option options [Message] :message The message you want to amend.
31
+ # If none is provided, then one is created.
32
+ # @option options [Object, Array<Object>] :contexts One or more Objects that
33
+ # Harbinger will visit and extract message elements from.
34
+ # @option options [Symbol, Array<Symbol>] :channels One or more channels that
35
+ # Harbinger will deliver the :message to
36
+ def call(options)
37
+ message = build_message(options)
38
+ deliver_message(message, options)
39
+ end
40
+
41
+ # Responsible for building a :message from the various :contexts.
42
+ #
43
+ # @see .call
44
+ #
45
+ # @param [Hash] options
46
+ # @option options [Message] :message The message you want to amend.
47
+ # If none is provided, then one is created.
48
+ # @option options [Object, Array<Object>] :contexts One or more Objects that
49
+ # Harbinger will visit and extract message elements from.
50
+ def build_message(options = {})
51
+ contexts = Array(options.fetch(:contexts)).flatten.compact
52
+ message = options.fetch(:message) { default_message }
53
+
54
+ contexts.each { |context| reporter_for(context).accept(message) }
55
+ message
56
+ end
57
+
58
+ # Responsible for delivering a :message to the appropriate :channels.
59
+ #
60
+ # @see .call
61
+ #
62
+ # @param message [Message] The message you want to amend.
63
+ # If none is provided, then one is created.
64
+ # @param [Hash] options
65
+ # @option options [Symbol, Array<Symbol>] :channels One or more channels that
66
+ # Harbinger will deliver the :message to
67
+ def deliver_message(message, options = {})
68
+ channels = options.fetch(:channels) { default_channels }
69
+ Array(channels).flatten.compact.each do |channel_name|
70
+ channel = channel_for(channel_name)
71
+ channel.deliver(message)
72
+ end
73
+ true
74
+ end
75
+
76
+ def default_message
77
+ require 'harbinger/message'
78
+ Message.new
79
+ end
80
+ private_class_method :default_message
81
+
82
+ def reporter_for(context)
83
+ Reporters.find_for(context)
84
+ end
85
+ private_class_method :reporter_for
86
+
87
+ def channel_for(name)
88
+ Channels.find_for(name)
89
+ end
90
+ private_class_method :channel_for
91
+
92
+ def default_channels
93
+ configuration.default_channels
94
+ end
95
+
96
+ def logger
97
+ configuration.logger
98
+ end
99
+
100
+ def database_storage
101
+ configuration.database_storage
102
+ end
103
+
5
104
  end
@@ -0,0 +1,25 @@
1
+ module Harbinger
2
+ module Channels
3
+ module_function
4
+ def find_for(channel_name)
5
+ channel_class_name = channel_name_for_instance(channel_name)
6
+ if const_defined?(channel_class_name)
7
+ const_get(channel_class_name)
8
+ else
9
+ NullChannel
10
+ end
11
+ rescue StandardError
12
+ NullChannel
13
+ end
14
+
15
+ def channel_name_for_instance(channel_name)
16
+ channel_name.to_s.gsub(/(?:^|_)([a-z])/) { $1.upcase } + "Channel"
17
+ end
18
+ private_class_method :channel_name_for_instance
19
+ end
20
+ end
21
+
22
+ # Note: I am not requiring all of the channels for this Gem. Some
23
+ # implementations my want to override the defaults. Look to
24
+ # `lib/harbinger/engine.rb` for how things are required for Rails applications.
25
+ require 'harbinger/channels/null_channel'
@@ -0,0 +1,15 @@
1
+ module Harbinger::Channels
2
+ module DatabaseChannel
3
+ module_function
4
+ def deliver(message, options = {})
5
+ storage = options.fetch(:storage) { default_storage }
6
+ storage.store_message(message)
7
+ end
8
+
9
+ def default_storage
10
+ Harbinger.database_storage
11
+ end
12
+ private_class_method :default_storage
13
+
14
+ end
15
+ end
@@ -0,0 +1,31 @@
1
+ module Harbinger::Channels
2
+ module LoggerChannel
3
+ module_function
4
+ def deliver(message, options = {})
5
+ logger = options.fetch(:logger) { default_logger }
6
+ severity = options.fetch(:severity) { default_severity }
7
+ read(message) do |line|
8
+ logger.add(severity, line)
9
+ end
10
+ end
11
+
12
+ def read(message)
13
+ yield("BEGIN MESSAGE OBJECT ID=#{message.object_id}")
14
+ message.attributes.each do |key, value|
15
+ yield("#{key.inspect} => #{value.inspect}")
16
+ end
17
+ yield("END MESSAGE OBJECT ID=#{message.object_id}")
18
+ end
19
+ private_class_method :read
20
+
21
+ def default_logger
22
+ Harbinger.logger
23
+ end
24
+ private_class_method :default_logger
25
+
26
+ def default_severity
27
+ 5 # ::Logger::UNKNOWN
28
+ end
29
+ private_class_method :default_severity
30
+ end
31
+ end
@@ -0,0 +1,7 @@
1
+ module Harbinger::Channels
2
+ module NullChannel
3
+ module_function
4
+ def deliver(message, options = {})
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,58 @@
1
+ require 'harbinger/exceptions'
2
+ module Harbinger
3
+ class Configuration
4
+
5
+ def default_channels
6
+ @default_channels || __default_channels
7
+ end
8
+
9
+ def default_channels=(*channel_names)
10
+ @default_channels = Array(channel_names).flatten.compact.collect{|name|
11
+ word = name.to_s
12
+ word.gsub!(/([A-Z\d]+)([A-Z][a-z])/,'\1_\2')
13
+ word.gsub!(/([a-z\d])([A-Z])/,'\1_\2')
14
+ word.tr!("-", "_")
15
+ word.downcase!
16
+ word.to_sym
17
+ }
18
+ end
19
+
20
+ def logger
21
+ @logger || default_logger
22
+ end
23
+
24
+ def logger=(object)
25
+ raise ConfigurationError.new("Expected Harbinger.database_storage to respond_to #add. #{object.inspect} does not respond to #add") unless object.respond_to?(:add)
26
+ @logger = object
27
+ end
28
+
29
+ def database_storage
30
+ @database_storage || default_database_storage
31
+ end
32
+
33
+ def database_storage=(object)
34
+ raise ConfigurationError.new("Expected Harbinger.database_storage to respond_to #store_message. #{object.inspect} does not respond to #add") unless object.respond_to?(:store_message)
35
+ @database_storage = object
36
+ end
37
+
38
+ private
39
+
40
+ def default_logger
41
+ if defined?(Rails)
42
+ Rails.logger
43
+ else
44
+ require 'logger'
45
+ ::Logger.new(STDOUT)
46
+ end
47
+ end
48
+
49
+ def default_database_storage
50
+ require 'harbinger/database_channel_message'
51
+ DatabaseChannelMessage
52
+ end
53
+
54
+ def __default_channels
55
+ [:logger]
56
+ end
57
+ end
58
+ end
@@ -1,5 +1,12 @@
1
1
  module Harbinger
2
2
  class Engine < ::Rails::Engine
3
- isolate_namespace Harbinger
3
+ engine_name 'harbinger'
4
+
5
+ config.to_prepare do
6
+ # Because I don't want to auto-require all of the dependent channels
7
+ Harbinger.default_channels.each do |channel_name|
8
+ require "harbinger/channels/#{channel_name}_channel"
9
+ end
10
+ end
4
11
  end
5
12
  end
@@ -0,0 +1,4 @@
1
+ module Harbinger
2
+ class ConfigurationError < RuntimeError
3
+ end
4
+ end
@@ -0,0 +1,20 @@
1
+ module Harbinger
2
+ class Message
3
+ attr_reader :attributes
4
+ def initialize
5
+ @attributes = {}
6
+ yield(self) if block_given?
7
+ end
8
+
9
+ def append(container, key, value)
10
+ composite_key = "#{container}.#{key}"
11
+ @attributes[composite_key] ||= []
12
+ @attributes[composite_key] << value
13
+ end
14
+
15
+ def contexts
16
+ attributes.keys.collect { |key| key.split('.')[0] }.uniq
17
+ end
18
+
19
+ end
20
+ end
@@ -0,0 +1,34 @@
1
+ module Harbinger
2
+ module Reporters
3
+ module_function
4
+ def find_for(context)
5
+ if context.respond_to?(:to_harbinger_reporter)
6
+ context.to_harbinger_reporter
7
+ else
8
+ # @TODO - Handle inheritence; KeyError is not an Exception
9
+ reporter_class_name = reporter_name_for_instance(context)
10
+ if const_defined?(reporter_class_name)
11
+ const_get(reporter_class_name).new(context)
12
+ else
13
+ NullReporter.new(context)
14
+ end
15
+ end
16
+ rescue StandardError
17
+ NullReporter.new(context)
18
+ end
19
+
20
+ def reporter_name_for_instance(context)
21
+ if context.is_a?(Exception)
22
+ "ExceptionReporter"
23
+ else
24
+ context.class.to_s.gsub(/(?:^|_)([a-z])/) { $1.upcase } + "Reporter"
25
+ end
26
+ end
27
+ private_class_method :reporter_name_for_instance
28
+ end
29
+ end
30
+
31
+ require "harbinger/reporters/user_reporter"
32
+ require "harbinger/reporters/request_reporter"
33
+ require "harbinger/reporters/exception_reporter"
34
+ require "harbinger/reporters/null_reporter"
@@ -0,0 +1,26 @@
1
+ module Harbinger::Reporters
2
+ class ExceptionReporter
3
+ attr_reader :exception
4
+
5
+ def initialize(exception)
6
+ @exception = exception
7
+ end
8
+
9
+ def accept(message)
10
+
11
+ if exception.respond_to?(:class)
12
+ message.append('exception', 'class_name', exception.class.to_s)
13
+ end
14
+
15
+ if exception.respond_to?(:backtrace)
16
+ message.append('exception', 'backtrace', Array(exception.backtrace).join("\n"))
17
+ end
18
+
19
+ if exception.respond_to?(:message)
20
+ message.append('exception', 'message', exception.message)
21
+ end
22
+
23
+ end
24
+
25
+ end
26
+ end
@@ -0,0 +1,14 @@
1
+ module Harbinger::Reporters
2
+ class NullReporter
3
+ attr_reader :context
4
+
5
+ def initialize(context, config = {})
6
+ @context = context
7
+ end
8
+
9
+ def accept(message)
10
+ message.append('nil', context.class.to_s, context.inspect)
11
+ end
12
+
13
+ end
14
+ end
@@ -0,0 +1,20 @@
1
+ module Harbinger
2
+ module Reporters
3
+ class RequestReporter
4
+ attr_reader :request, :method_names
5
+ private :method_names
6
+ def initialize(request, config = {})
7
+ @request = request
8
+ @method_names = config.fetch(:method_names) { ['path', 'params', 'user_agent'] }
9
+ end
10
+
11
+ def accept(message)
12
+ method_names.each do |method_name|
13
+ if request.respond_to?(method_name)
14
+ message.append('request', method_name, request.public_send(method_name))
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,20 @@
1
+ module Harbinger::Reporters
2
+ class UserReporter
3
+ attr_reader :user, :method_names
4
+ private :method_names
5
+
6
+ def initialize(user, config = {})
7
+ @user = user
8
+ @method_names = config.fetch(:method_names) { ['username'] }
9
+ end
10
+
11
+ def accept(message)
12
+ method_names.each do |method_name|
13
+ if user.respond_to?(method_name)
14
+ message.append('user', method_name, user.public_send(method_name))
15
+ end
16
+ end
17
+ end
18
+
19
+ end
20
+ end
@@ -1,3 +1,3 @@
1
1
  module Harbinger
2
- VERSION = "0.0.1.pre"
2
+ VERSION = "0.1.0"
3
3
  end
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env ruby -w
2
+ require 'time'
3
+ require 'rake'
4
+
5
+ FileList['spec/**/*_spec.rb'].sort.select do |fn|
6
+ puts "rspec #{fn}"
7
+ system "rspec #{fn}"
8
+ end
9
+ system('rspec')
@@ -0,0 +1,20 @@
1
+ #!/usr/bin/env ruby -w
2
+
3
+ require 'rake'
4
+
5
+ module Fast
6
+ module_function
7
+ def spec?(fn)
8
+ open(fn) { |f| f.gets =~ /fast_helper/ }
9
+ end
10
+ end
11
+
12
+ fast_specs = FileList['spec/**/*_spec.rb'].select { |fn|
13
+ Fast.spec?(fn)
14
+ }
15
+ if fast_specs.any?
16
+ system "rspec #{fast_specs}"
17
+ else
18
+ puts "Unable to find any fast specs"
19
+ exit(-1)
20
+ end