harbinger 0.0.1.pre → 0.1.0

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