diff --git a/.rspec b/.rspec new file mode 100644 index 0000000..c99d2e7 --- /dev/null +++ b/.rspec @@ -0,0 +1 @@ +--require spec_helper diff --git a/.travis.yml b/.travis.yml index b41822f..6daf74b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,7 +8,6 @@ rvm: - 2.1 - 2.2 - 2.3.0 - - rbx-2 - ruby-head matrix: allow_failures: diff --git a/Gemfile b/Gemfile index 7eb2841..1a214f0 100644 --- a/Gemfile +++ b/Gemfile @@ -7,3 +7,5 @@ gem 'rspec' gem 'guard' gem 'guard-rspec' gem 'coveralls', :require => false +gem 'pry-byebug' +gem 'rubocop' diff --git a/README.md b/README.md index d575f0e..bde27d0 100644 --- a/README.md +++ b/README.md @@ -7,8 +7,8 @@ ErrbitPlugins are Ruby gems that extend the functionality of Errbit. To get started, create a Ruby gem and add 'errbit_plugin' as a dependency in your gem's gemspec. -Now you can start adding plugins. At the moment, there is only one kind of -plugin you can create, and that is the issue tracker. +Now you can start adding plugins. Keep reading to learn about what you can do +with Errbit plugins. ### Issue Trackers An issue tracker plugin is a Ruby class that enables you to link errors within @@ -122,7 +122,86 @@ class MyIssueTracker < ErrbitPlugin::IssueTracker end ``` +### Notifiers +A Notifier is a Ruby class that can notify an external system when Errbit +receives notices. When configuring an app within the Errbit UI, you can choose +to enable a notifier which will delegate the business of sending notifications +to the selected notifier. +Your notifier plugin is responsible for implementing the interface defined by +ErrbitPlugin::Notifier. All of the required methods must be implemented and the +class must extend ErrbitPlugin::Notifier. Here's an example: +```ruby +class MyNotifier < ErrbitPlugin::Notifier + + # A unique label for your notifier plugin used internally by errbit + def self.label + 'my-notifier' + end + + def self.note + 'a note about this notifier that users will see in the UI' + end + + # Form fields that will be presented to the administrator when setting up + # or editing the errbit app. The values we collect will be availble for use + # later when its time to notify. + def self.fields + { + username: { + placeholder: "Some placeholder text" + }, + password: { + placeholder: "Some more placeholder text" + } + } + end + + # Icons to display during user interactions with this notifier. This method + # should return a hash of two-tuples, the key names being 'create', 'goto', + # and 'inactive'. The two-tuples should contain the icon media type and the + # binary icon data. + def self.icons + @icons ||= { + create: [ 'image/png', File.read('./path/to/create.png') ], + goto: [ 'image/png', File.read('./path/to/goto.png') ], + inactive: [ 'image/png', File.read('./path/to/inactive.png') ], + } + end + + # If this notifier can be in a configured or non-configured state, you can let + # errbit know by returning a Boolean here + def configured? + # In this case, we'll say this notifier is configured when username + # is set + !!params['username'] + end + + # Called to validate user input. Just return a hash of errors if there are + # any + def errors + if @params['field_one'] + {} + else + { :field_one, 'Field One must be present' } + end + end + + # notify is called when Errbit decides its time to notify external systems + # about a new error notice. notify receives an instance of the notice's problem + # which you can interrogate for any information you'd like to include in the + # notification message. + def notify(problem) + # Send a notification! Use HTTP, SMTP, FTP, RabbitMQ or whatever you want + # to notify your external system. + end + + # The URL for your remote issue tracker + def url + 'http://some-remote-tracker.com' + end +end +``` ## Contributing diff --git a/lib/errbit_plugin.rb b/lib/errbit_plugin.rb index fb7699b..4dee67a 100644 --- a/lib/errbit_plugin.rb +++ b/lib/errbit_plugin.rb @@ -1,5 +1,7 @@ -require "errbit_plugin/version" -require "errbit_plugin/registry" -require "errbit_plugin/issue_tracker" -require "errbit_plugin/validate_issue_tracker" -require "errbit_plugin/issue_trackers/none" +require 'errbit_plugin/version' +require 'errbit_plugin/registry' +require 'errbit_plugin/issue_tracker' +require 'errbit_plugin/validate_issue_tracker' +require 'errbit_plugin/notifier' +require 'errbit_plugin/validate_notifier' +require 'errbit_plugin/issue_trackers/none' diff --git a/lib/errbit_plugin/notifier.rb b/lib/errbit_plugin/notifier.rb new file mode 100644 index 0000000..fa352ee --- /dev/null +++ b/lib/errbit_plugin/notifier.rb @@ -0,0 +1,10 @@ +module ErrbitPlugin + # abstract class for issue trackers + class Notifier + attr_reader :options + + def initialize(options) + @options = options + end + end +end diff --git a/lib/errbit_plugin/notifiers/none.rb b/lib/errbit_plugin/notifiers/none.rb new file mode 100644 index 0000000..8c8e5a3 --- /dev/null +++ b/lib/errbit_plugin/notifiers/none.rb @@ -0,0 +1,27 @@ +module ErrbitPlugin + class NoneNotifier < Notifier + LABEL = 'none'.freeze + NOTE = 'No notifications'.freeze + + def self.label; LABEL; end + def self.note; NOTE; end + def self.fields; {}; end + def self.icons + @icons ||= { + create: ['image/png', read_static_file('none_create.png')], + goto: ['image/png', read_static_file('none_create.png')], + inactive: ['image/png', read_static_file('none_inactive.png')], + } + end + def self.read_static_file(file) + File.read(File.expand_path(File.join( + File.dirname(__FILE__), '..', '..', '..', 'static', file))) + end + def configured?; false; end + def errors; {}; end + def url; ''; end + def notify(*); true; end + end +end + +ErrbitPlugin::Registry.add_notifier(ErrbitPlugin::NoneNotifier) diff --git a/lib/errbit_plugin/registry.rb b/lib/errbit_plugin/registry.rb index 1ab9519..03b5eb0 100644 --- a/lib/errbit_plugin/registry.rb +++ b/lib/errbit_plugin/registry.rb @@ -4,8 +4,15 @@ class AlreadyRegisteredError < StandardError; end module Registry @issue_trackers = {} + @notifiers = {} def self.add_issue_tracker(klass) + validate = ValidateIssueTracker.new(klass) + + unless validate.valid? + raise IncompatibilityError.new(validate.errors.join('; ')) + end + key = klass.label if issue_trackers.has_key?(key) @@ -13,21 +20,40 @@ def self.add_issue_tracker(klass) "issue_tracker '#{key}' already registered" end - validate = ValidateIssueTracker.new(klass) - - if validate.valid? - @issue_trackers[key] = klass - else - raise IncompatibilityError.new(validate.errors.join('; ')) - end + @issue_trackers[key] = klass end def self.clear_issue_trackers - @issue_trackers = {} + @issue_trackers.clear end def self.issue_trackers @issue_trackers end + + def self.add_notifier(klass) + validate = ValidateNotifier.new(klass) + + unless validate.valid? + raise IncompatibilityError.new(validate.errors.join('; ')) + end + + key = klass.label + + if notifiers.has_key?(key) + raise AlreadyRegisteredError, + "notifier '#{key}' already registered" + end + + @notifiers[key] = klass + end + + def self.clear_notifiers + @notifiers.clear + end + + def self.notifiers + @notifiers + end end end diff --git a/lib/errbit_plugin/validate_notifier.rb b/lib/errbit_plugin/validate_notifier.rb new file mode 100644 index 0000000..4b4fb17 --- /dev/null +++ b/lib/errbit_plugin/validate_notifier.rb @@ -0,0 +1,62 @@ +module ErrbitPlugin + class ValidateNotifier + def initialize(klass) + @klass = klass + @errors = [] + end + attr_reader :errors + + def valid? + valid_inherit = good_inherit? + valid_instance_methods = implements_instance_methods? + valid_class_methods = implements_class_methods? + + valid_inherit && valid_instance_methods && valid_class_methods + end + + private + + def good_inherit? + unless @klass.ancestors.include?(ErrbitPlugin::Notifier) + add_errors(:not_inherited) + false + else + true + end + end + + def implements_instance_methods? + impl = [:configured?, :errors, :notify, :url].map do |method| + if instance.respond_to?(method) + true + else + add_errors(:instance_method_missing, method) + false + end + end + + impl.all? { |value| value == true } + end + + def implements_class_methods? + impl = [:label, :fields, :note, :icons].map do |method| + if @klass.respond_to?(method) + true + else + add_errors(:class_method_missing, method) + false + end + end + + impl.all? { |value| value == true } + end + + def instance + @instance ||= @klass.new({}) + end + + def add_errors(key, value=nil) + @errors << [key, value].compact + end + end +end diff --git a/spec/errbit_plugin/notifier_spec.rb b/spec/errbit_plugin/notifier_spec.rb new file mode 100644 index 0000000..41fdad5 --- /dev/null +++ b/spec/errbit_plugin/notifier_spec.rb @@ -0,0 +1,9 @@ +describe ErrbitPlugin::Notifier do + describe '#initialize' do + it 'stores options' do + opts = { one: 'two' } + obj = described_class.new(opts) + expect(obj.options).to eq(opts) + end + end +end diff --git a/spec/errbit_plugin/registry_spec.rb b/spec/errbit_plugin/registry_spec.rb index 982b193..211cf88 100644 --- a/spec/errbit_plugin/registry_spec.rb +++ b/spec/errbit_plugin/registry_spec.rb @@ -1,51 +1,46 @@ -require 'spec_helper' - describe ErrbitPlugin::Registry do - before do - ErrbitPlugin::Registry.clear_issue_trackers - end + describe '.add_issue_tracker' do + before { described_class.clear_issue_trackers } - let(:tracker) { - tracker = Class.new(ErrbitPlugin::IssueTracker) do - def self.label - 'something' + let(:tracker) do + Class.new(ErrbitPlugin::IssueTracker) do + def self.label + 'something' + end end end - tracker - } - describe ".add_issue_tracker" do - context "with issue_tracker class valid" do + context 'with valid issue tracker' do before do allow(ErrbitPlugin::ValidateIssueTracker) .to receive(:new) .with(tracker) - .and_return(double(:valid? => true, :message => '')) + .and_return(double(valid?: true, message: '')) end - it 'add new issue_tracker plugin' do - ErrbitPlugin::Registry.add_issue_tracker(tracker) - expect(ErrbitPlugin::Registry.issue_trackers).to eq({ + + it 'can be added' do + described_class.add_issue_tracker(tracker) + expect(described_class.issue_trackers).to eq({ 'something' => tracker }) end - context "with already issue_tracker with this key" do - it 'raise ErrbitPlugin::AlreadyRegisteredError' do - ErrbitPlugin::Registry.add_issue_tracker(tracker) - expect { - ErrbitPlugin::Registry.add_issue_tracker(tracker) - }.to raise_error(ErrbitPlugin::AlreadyRegisteredError) - end + + it 'cannot be added twice' do + described_class.add_issue_tracker(tracker) + expect { + described_class.add_issue_tracker(tracker) + }.to raise_error(ErrbitPlugin::AlreadyRegisteredError) end end - context "with an IssueTracker not valid" do - it 'raise an IncompatibilityError' do + context 'with invalid issue tracker' do + it 'raises an IncompatibilityError' do allow(ErrbitPlugin::ValidateIssueTracker) .to receive(:new) .with(tracker) - .and_return(double(:valid? => false, :message => 'foo', :errors => [])) + .and_return(double(valid?: false, message: 'foo', errors: [])) expect { - ErrbitPlugin::Registry.add_issue_tracker(tracker) + described_class.add_issue_tracker(tracker) }.to raise_error(ErrbitPlugin::IncompatibilityError) end @@ -53,10 +48,68 @@ def self.label allow(ErrbitPlugin::ValidateIssueTracker) .to receive(:new) .with(tracker) - .and_return(double(:valid? => false, :message => 'foo', :errors => ['one', 'two'])) + .and_return(double(valid?: false, message: 'foo', errors: %w(one two))) + + begin + described_class.add_issue_tracker(tracker) + rescue ErrbitPlugin::IncompatibilityError => e + expect(e.message).to eq('one; two') + end + end + end + end + + describe '.add_notifier' do + before { described_class.clear_notifiers } + + let(:notifier) do + Class.new(ErrbitPlugin::Notifier) do + def self.label + 'something' + end + end + end + + context 'with a valid notifier class' do + before do + allow(ErrbitPlugin::ValidateNotifier) + .to receive(:new) + .with(notifier) + .and_return(double(valid?: true, message: '')) + end + + it 'adds the notifier plugin' do + described_class.add_notifier(notifier) + expect(described_class.notifiers).to eq({ 'something' => notifier }) + end + + it 'does not add the same plugin twice' do + described_class.add_notifier(notifier) + expect { + described_class.add_notifier(notifier) + }.to raise_error(ErrbitPlugin::AlreadyRegisteredError) + end + end + + context 'with an invalid notifier class' do + it 'raise an IncompatibilityError' do + allow(ErrbitPlugin::ValidateNotifier) + .to receive(:new) + .with(notifier) + .and_return(double(valid?: false, message: 'foo', errors: [])) + expect { + described_class.add_notifier(notifier) + }.to raise_error(ErrbitPlugin::IncompatibilityError) + end + + it 'puts the errors in the exception message' do + allow(ErrbitPlugin::ValidateNotifier) + .to receive(:new) + .with(notifier) + .and_return(double(valid?: false, message: 'foo', errors: %w(one two))) begin - ErrbitPlugin::Registry.add_issue_tracker(tracker) + described_class.add_notifier(notifier) rescue ErrbitPlugin::IncompatibilityError => e expect(e.message).to eq('one; two') end diff --git a/spec/errbit_plugin/validate_issue_tracker_spec.rb b/spec/errbit_plugin/validate_issue_tracker_spec.rb index 1b73870..56605a4 100644 --- a/spec/errbit_plugin/validate_issue_tracker_spec.rb +++ b/spec/errbit_plugin/validate_issue_tracker_spec.rb @@ -1,5 +1,3 @@ -require 'spec_helper' - describe ErrbitPlugin::ValidateIssueTracker do describe "#valid?" do diff --git a/spec/errbit_plugin/validate_notifier_spec.rb b/spec/errbit_plugin/validate_notifier_spec.rb new file mode 100644 index 0000000..691427c --- /dev/null +++ b/spec/errbit_plugin/validate_notifier_spec.rb @@ -0,0 +1,219 @@ +describe ErrbitPlugin::ValidateNotifier do + describe '#valid?' do + context 'with a valid class' do + klass = Class.new(ErrbitPlugin::Notifier) do + def self.label; 'foo'; end + def self.note; 'foo'; end + def self.fields; ['foo']; end + def self.icons; {}; end + def configured?; true; end + def errors; true; end + def notify; 'http'; end + def url; 'http'; end + end + + it 'valid' do + expect(described_class.new(klass).valid?).to be true + end + end + + context 'with class not inherit from ErrbitPlugin::Notifier' do + klass = Class.new do + def self.label; 'foo'; end + def self.note; 'foo'; end + def self.fields; ['foo']; end + def self.icons; {}; end + def initialize(params); end + def configured?; true; end + def errors; true; end + def notify; 'http'; end + def url; 'http'; end + end + + it 'not valid' do + expect(described_class.new(klass).valid?).to be false + end + + it 'says :not_inherited' do + is = described_class.new(klass) + is.valid? + expect(is.errors).to eql [[:not_inherited]] + end + end + + context 'with no label method' do + klass = Class.new(ErrbitPlugin::Notifier) do + def self.note; 'foo'; end + def self.fields; ['foo']; end + def self.icons; {}; end + def configured?; true; end + def errors; true; end + def notify; 'http'; end + def url; 'http'; end + end + + it 'not valid' do + expect(described_class.new(klass).valid?).to be false + end + + it 'say not implement configured?' do + is = described_class.new(klass) + is.valid? + expect(is.errors).to eql [[:class_method_missing, :label]] + end + end + + context 'with no icons method' do + klass = Class.new(ErrbitPlugin::Notifier) do + def self.note; 'foo'; end + def self.fields; ['foo']; end + def self.label; 'alabel'; end + def configured?; true; end + def errors; true; end + def notify; 'http'; end + def url; 'http'; end + end + + it 'not valid' do + expect(described_class.new(klass).valid?).to be false + end + + it 'say not implement configured?' do + is = described_class.new(klass) + is.valid? + expect(is.errors).to eql [[:class_method_missing, :icons]] + end + end + + context 'without fields method' do + klass = Class.new(ErrbitPlugin::Notifier) do + def self.label; 'foo'; end + def self.note; 'foo'; end + def self.icons; {}; end + def configured?; true; end + def errors; true; end + def notify; 'http'; end + def url; 'http'; end + end + + it 'not valid' do + expect(described_class.new(klass).valid?).to be false + end + + it 'say not implement configured?' do + is = described_class.new(klass) + is.valid? + expect(is.errors).to eql [[:class_method_missing, :fields]] + end + end + + context 'without configured? method' do + klass = Class.new(ErrbitPlugin::Notifier) do + def self.label; 'foo'; end + def self.note; 'foo'; end + def self.fields; ['foo']; end + def self.icons; {}; end + def errors; true; end + def notify; 'http'; end + def url; 'http'; end + end + + it 'not valid' do + expect(described_class.new(klass).valid?).to be false + end + + it 'say not implement configured?' do + is = described_class.new(klass) + is.valid? + expect(is.errors).to eql [[:instance_method_missing, :configured?]] + end + end + + context 'without errors method' do + klass = Class.new(ErrbitPlugin::Notifier) do + def self.label; 'foo'; end + def self.note; 'foo'; end + def self.fields; ['foo']; end + def self.icons; {}; end + def configured?; true; end + def notify; 'http'; end + def url; 'http'; end + end + + it 'not valid' do + expect(described_class.new(klass).valid?).to be false + end + + it 'say not implement errors' do + is = described_class.new(klass) + is.valid? + expect(is.errors).to eql [[:instance_method_missing, :errors]] + end + end + + context 'without notify method' do + klass = Class.new(ErrbitPlugin::Notifier) do + def self.label; 'foo'; end + def self.note; 'foo'; end + def self.fields; ['foo']; end + def self.icons; {}; end + def configured?; true; end + def errors; true; end + def url; 'http'; end + end + + it 'not valid' do + expect(described_class.new(klass).valid?).to be false + end + it 'say not implement url' do + is = described_class.new(klass) + is.valid? + expect(is.errors).to eql [[:instance_method_missing, :notify]] + end + end + + context 'without url method' do + klass = Class.new(ErrbitPlugin::Notifier) do + def self.label; 'foo'; end + def self.note; 'foo'; end + def self.fields; ['foo']; end + def self.icons; {}; end + def configured?; true; end + def errors; true; end + def notify; 'http'; end + end + + it 'not valid' do + expect(described_class.new(klass).valid?).to be false + end + + it 'say not implement url' do + is = described_class.new(klass) + is.valid? + expect(is.errors).to eql [[:instance_method_missing, :url]] + end + end + + context 'without note method' do + klass = Class.new(ErrbitPlugin::Notifier) do + def self.label; 'foo'; end + def self.fields; ['foo']; end + def self.icons; {}; end + def configured?; true; end + def errors; true; end + def notify; 'http'; end + def url; 'foo'; end + end + + it 'not valid' do + expect(described_class.new(klass).valid?).to be false + end + + it 'say not implement note method' do + is = described_class.new(klass) + is.valid? + expect(is.errors).to eql [[:class_method_missing, :note]] + end + end + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 92338c7..5401d2c 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -13,6 +13,7 @@ end require 'errbit_plugin' +require 'pry' RSpec.configure do |config| config.run_all_when_everything_filtered = true