omnihooks 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 06339853f7ece3c68f11ba4915da17e824ccb27d
4
+ data.tar.gz: 2f4d22b89574c3415920c62daed2fb54c432e213
5
+ SHA512:
6
+ metadata.gz: 52b3d2514746e528b4cf60b2adfa88fe2dc511b1c03eac56c15abfa224b19c88fe822daa024005d65cf020805ab3df569937413319ffb825559948bba285ac22
7
+ data.tar.gz: c51daa43fbc940543974b40c4d1568c4f62c7be1ab42ef0daa1c1201dfc8cc8e14dabdc3c242d1a1de3c66b53bd533bbbee9c970721768f20d2eae17ac602b62
data/.gitignore ADDED
@@ -0,0 +1,15 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ *.bundle
11
+ *.so
12
+ *.o
13
+ *.a
14
+ mkmf.log
15
+ /spec/examples.txt
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --require spec_helper
data/.ruby-gemsets ADDED
@@ -0,0 +1 @@
1
+ omnihooks
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ ruby-2.1.6
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in omnihooks.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2015 Karl Falconer
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,31 @@
1
+ # OmniHooks
2
+
3
+ TODO: Write a gem description
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ ```ruby
10
+ gem 'omnihooks'
11
+ ```
12
+
13
+ And then execute:
14
+
15
+ $ bundle
16
+
17
+ Or install it yourself as:
18
+
19
+ $ gem install omnihooks
20
+
21
+ ## Usage
22
+
23
+ TODO: Write usage instructions here
24
+
25
+ ## Contributing
26
+
27
+ 1. Fork it ( https://github.com/[my-github-username]/omnihooks/fork )
28
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
29
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
30
+ 4. Push to the branch (`git push origin my-new-feature`)
31
+ 5. Create a new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,2 @@
1
+ require "bundler/gem_tasks"
2
+
@@ -0,0 +1,48 @@
1
+ require "active_support/inflector"
2
+
3
+ module OmniHooks
4
+ class Builder < ::Rack::Builder
5
+
6
+ def initialize(app, &block)
7
+ @options = nil
8
+ if rack14?
9
+ super
10
+ else
11
+ @app = app
12
+ super(&block)
13
+ @ins << @app
14
+ end
15
+ end
16
+
17
+ def rack14?
18
+ Rack.release.split('.')[1].to_i >= 4
19
+ end
20
+
21
+ def on_failure(&block)
22
+ OmniHooks.config.on_failure = block
23
+ end
24
+
25
+ def options(options = false)
26
+ return @options || {} if options == false
27
+ @options = options
28
+ end
29
+
30
+ def provider(klass, *args, &block)
31
+ if klass.is_a?(Class)
32
+ middleware = klass
33
+ else
34
+ begin
35
+ middleware = OmniHooks::Strategies.const_get(klass.to_s.camelize)
36
+ rescue NameError
37
+ raise(LoadError.new("Could not find matching strategy for #{klass.inspect}. You may need to install an additional gem (such as omnihooks-#{klass})."))
38
+ end
39
+ end
40
+ args.last.is_a?(Hash) ? args.push(options.merge(args.pop)) : args.push(options)
41
+ use middleware, *args, &block
42
+ end
43
+
44
+ def call(env)
45
+ to_app.call(env)
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,43 @@
1
+ module OmniHooks
2
+ module Strategies
3
+ # The Developer strategy is a very simple strategy that can be used as a
4
+ # placeholder in your application until a different authentication strategy
5
+ # is swapped in. It has zero security and should *never* be used in a
6
+ # production setting.
7
+ #
8
+ # ## Usage
9
+ #
10
+ # To use the Developer strategy, all you need to do is put it in like any
11
+ # other strategy:
12
+ #
13
+ # @example Basic Usage
14
+ #
15
+ # use OmniAuth::Builder do
16
+ # provider :developer
17
+ # end
18
+ #
19
+ # @example Custom Fields
20
+ #
21
+ # use OmniAuth::Builder do
22
+ # provider :developer,
23
+ # :fields => [:first_name, :last_name],
24
+ # :uid_field => :last_name
25
+ # end
26
+ #
27
+ # This will create a strategy that, when the user visits `/auth/developer`
28
+ # they will be presented a form that prompts for (by default) their name
29
+ # and email address. The auth hash will be populated with these fields and
30
+ # the `uid` will simply be set to the provided email.
31
+ class Developer
32
+ include OmniHooks::Strategy
33
+
34
+ event_type do
35
+ params[:type]
36
+ end
37
+
38
+ event do
39
+ params
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,303 @@
1
+ require "active_support/notifications"
2
+
3
+ module OmniHooks
4
+ # The Strategy is the base unit of OmniHooks's ability to
5
+ # wrangle multiple providers. Each strategy provided by
6
+ # OmniHooks includes this mixin to gain the default functionality
7
+ # necessary to be compatible with the OmniHooks library.
8
+ module Strategy # rubocop:disable ModuleLength
9
+ def self.included(base)
10
+ OmniHooks.strategies << base
11
+
12
+ base.extend ClassMethods
13
+ base.class_eval do
14
+ option :backend, ActiveSupport::Notifications
15
+ option :adapter, OmniHooks::Strategy::NotificationAdapter
16
+ option :namespace_delimiter, '.'
17
+ end
18
+ end
19
+
20
+ module ClassMethods
21
+ attr_accessor :namespace
22
+ # Returns an inherited set of default options set at the class-level
23
+ # for each strategy.
24
+ def default_options
25
+ return @default_options if instance_variable_defined?(:@default_options) && @default_options
26
+ existing = superclass.respond_to?(:default_options) ? superclass.default_options : {}
27
+ @default_options = OmniHooks::Strategy::Options.new(existing)
28
+ end
29
+
30
+ # Directly declare a default option for your class. This is a useful from
31
+ # a documentation perspective as it provides a simple line-by-line analysis
32
+ # of the kinds of options your strategy provides by default.
33
+ #
34
+ # @param name [Symbol] The key of the default option in your configuration hash.
35
+ # @param value [Object] The value your object defaults to. Nil if not provided.
36
+ #
37
+ # @example
38
+ #
39
+ # class MyStrategy
40
+ # include OmniAuth::Strategy
41
+ #
42
+ # option :foo, 'bar'
43
+ # option
44
+ # end
45
+ def option(name, value = nil)
46
+ default_options[name] = value
47
+ end
48
+
49
+ # Sets (and retrieves) option key names for initializer arguments to be
50
+ # recorded as. This takes care of 90% of the use cases for overriding
51
+ # the initializer in OmniAuth Strategies.
52
+ def args(args = nil)
53
+ if args
54
+ @args = Array(args)
55
+ return
56
+ end
57
+ existing = superclass.respond_to?(:args) ? superclass.args : []
58
+ (instance_variable_defined?(:@args) && @args) || existing
59
+ end
60
+ # This allows for more declarative subclassing of strategies by allowing
61
+ # default options to be set using a simple configure call.
62
+ #
63
+ # @param options [Hash] If supplied, these will be the default options (deep-merged into the superclass's default options).
64
+ # @yield [Options] The options Mash that allows you to set your defaults as you'd like.
65
+ #
66
+ # @example Using a yield to configure the default options.
67
+ #
68
+ # class MyStrategy
69
+ # include OmniHooks::Strategy
70
+ #
71
+ # configure_options do |c|
72
+ # c.foo = 'bar'
73
+ # end
74
+ # end
75
+ #
76
+ # @example Using a hash to configure the default options.
77
+ #
78
+ # class MyStrategy
79
+ # include OmniHooks::Strategy
80
+ # configure_options foo: 'bar'
81
+ # end
82
+ def configure_options(options = nil)
83
+ if block_given?
84
+ yield default_options
85
+ else
86
+ default_options.deep_merge!(options)
87
+ end
88
+ end
89
+
90
+ def configure(&block)
91
+ raise ArgumentError, "must provide a block" unless block_given?
92
+ block.arity.zero? ? instance_eval(&block) : yield(self)
93
+ end
94
+ alias :setup :configure
95
+
96
+ def subscribe(name, callable = Proc.new)
97
+ backend.subscribe(namespace.to_regexp(name), adapter.call(callable))
98
+ end
99
+
100
+ def all(callable = Proc.new)
101
+ backend.subscribe(nil, callable)
102
+ end
103
+
104
+ def listening?(name)
105
+ namespaced_name = namespace.call(name)
106
+ backend.notifier.listening?(namespaced_name)
107
+ end
108
+ #protected
109
+ %w(event_type event).each do |fetcher|
110
+ class_eval <<-RUBY
111
+ def #{fetcher}(&block)
112
+ @#{fetcher}_proc = nil unless defined?(@#{fetcher}_proc)
113
+
114
+ return @#{fetcher}_proc unless block_given?
115
+ @#{fetcher}_proc = block
116
+ end
117
+ def #{fetcher}_stack(context)
118
+ compile_stack(self.ancestors, :#{fetcher}, context)
119
+ end
120
+ RUBY
121
+ end
122
+
123
+ def instrument(event_type, event_object)
124
+ backend.instrument(namespace.call(event_type), event_object)
125
+ end
126
+
127
+ private
128
+
129
+ def compile_stack(ancestors, method, context)
130
+ stack = ancestors.inject([]) do |a, ancestor|
131
+ a << context.instance_eval(&ancestor.send(method)) if ancestor.respond_to?(method) && ancestor.send(method)
132
+ a
133
+ end
134
+ stack.reverse!
135
+ end
136
+
137
+ def adapter
138
+ default_options.adapter
139
+ end
140
+
141
+ def backend
142
+ default_options.backend
143
+ end
144
+
145
+ def namespace
146
+ @namespace ||= OmniHooks::Strategy::Namespace.new(default_options.name, default_options.namespace_delimiter)
147
+ end
148
+ end
149
+
150
+ class Namespace < Struct.new(:prefix, :delimiter)
151
+ def call(name = nil)
152
+ "#{prefix}#{delimiter}#{name}"
153
+ end
154
+
155
+ def to_regexp(name = nil)
156
+ %r{^#{Regexp.escape(call(name))}}
157
+ end
158
+ end
159
+
160
+ class NotificationAdapter < Struct.new(:subscriber)
161
+ def self.call(callable)
162
+ new(callable)
163
+ end
164
+
165
+ def call(*args)
166
+ payload = args.last
167
+ subscriber.call(payload)
168
+ end
169
+ end
170
+
171
+ class Options < Hashie::Mash; end
172
+
173
+ attr_reader :options
174
+
175
+ # Initializes the strategy by passing in the Rack endpoint,
176
+ # the unique URL segment name for this strategy, and any
177
+ # additional arguments. An `options` hash is automatically
178
+ # created from the last argument if it is a hash.
179
+ #
180
+ # @param app [Rack application] The application on which this middleware is applied.
181
+ #
182
+ # @overload new(app, options = {})
183
+ # If nothing but a hash is supplied, initialized with the supplied options
184
+ # overriding the strategy's default options via a deep merge.
185
+ # @overload new(app, *args, options = {})
186
+ # If the strategy has supplied custom arguments that it accepts, they may
187
+ # will be passed through and set to the appropriate values.
188
+ #
189
+ # @yield [Class, Options] Yields Parent class and options to block for further configuration.
190
+ def initialize(app, *args, &block) # rubocop:disable UnusedMethodArgument
191
+ @app = app
192
+ @env = nil
193
+ @options = self.class.default_options.dup
194
+
195
+ options.deep_merge!(args.pop) if args.last.is_a?(Hash)
196
+
197
+ self.class.args.each do |arg|
198
+ break if args.empty?
199
+ options[arg] = args.shift
200
+ end
201
+
202
+ # Make sure that all of the args have been dealt with, otherwise error out.
203
+ fail(ArgumentError.new("Received wrong number of arguments. #{args.inspect}")) unless args.empty?
204
+
205
+ yield self.class, options if block_given?
206
+ end
207
+
208
+ def inspect
209
+ "#<#{self.class}>"
210
+ end
211
+
212
+ # Duplicates this instance and runs #call! on it.
213
+ # @param [Hash] The Rack environment.
214
+ def call(env)
215
+ dup.call!(env)
216
+ end
217
+ # The logic for dispatching any additional actions that need
218
+ # to be taken. For instance, calling the request phase if
219
+ # the request path is recognized.
220
+ #
221
+ # @param env [Hash] The Rack environment.
222
+ def call!(env) # rubocop:disable CyclomaticComplexity, PerceivedComplexity
223
+ @env = env
224
+
225
+ return instrument if on_request_path? && OmniHooks.config.allowed_request_methods.include?(request.request_method.downcase.to_sym)
226
+
227
+ @app.call(env)
228
+ end
229
+
230
+ def request
231
+ @request ||= Rack::Request.new(@env)
232
+ end
233
+
234
+ protected
235
+ attr_reader :app, :env
236
+
237
+ # Direct access to the OmniAuth logger, automatically prefixed
238
+ # with this strategy's name.
239
+ #
240
+ # @example
241
+ # log :warn, "This is a warning."
242
+ def log(level, message)
243
+ OmniHooks.logger.send(level, "(#{name}) #{message}")
244
+ end
245
+
246
+ private
247
+
248
+ CURRENT_PATH_REGEX = %r{/$}
249
+ EMPTY_STRING = ''.freeze
250
+
251
+ def instrument
252
+ # instance needs to lookup and from the paylook the event type
253
+ begin
254
+ evt = get_event
255
+ evt_type = get_event_type
256
+ self.class.instrument(evt_type, evt) if evt
257
+ rescue => e
258
+ log(:error, e.message)
259
+ [500, {}, [nil]]
260
+ else
261
+ # Send a 200 response back to
262
+ [200, {}, [nil]]
263
+ end
264
+ end
265
+
266
+ def path_prefix
267
+ options[:path_prefix] || OmniHooks.config.path_prefix
268
+ end
269
+
270
+ def name
271
+ options.name
272
+ end
273
+
274
+ def request_path
275
+ @request_path ||= options[:request_path].is_a?(String) ? options[:request_path] : "#{path_prefix}/#{name}"
276
+ end
277
+
278
+ def on_request_path?
279
+ if options.request_path.respond_to?(:call)
280
+ options.request_path.call(env)
281
+ else
282
+ on_path?(request_path)
283
+ end
284
+ end
285
+
286
+ def on_path?(path)
287
+ current_path.casecmp(path) == 0
288
+ end
289
+
290
+ def current_path
291
+ @current_path ||= request.path_info.downcase.sub(CURRENT_PATH_REGEX, EMPTY_STRING)
292
+ end
293
+
294
+ def get_event_type
295
+ self.class.event_type_stack(self).last
296
+ end
297
+
298
+ def get_event
299
+ self.class.event_stack(self).last
300
+ end
301
+
302
+ end
303
+ end
@@ -0,0 +1,3 @@
1
+ module OmniHooks
2
+ VERSION = "0.0.1"
3
+ end
data/lib/omnihooks.rb ADDED
@@ -0,0 +1,76 @@
1
+ require 'rack'
2
+ require 'singleton'
3
+ require 'logger'
4
+ require 'hashie'
5
+ require 'omnihooks/builder'
6
+ require 'omnihooks/strategy'
7
+
8
+ module OmniHooks
9
+
10
+ module Strategies
11
+ end
12
+
13
+ #
14
+ # Collection of current strategies
15
+ #
16
+ # @return [Array<Object>] strategy collection
17
+ def self.strategies
18
+ @strategies ||= []
19
+ end
20
+
21
+ class Configuration
22
+ include Singleton
23
+
24
+ attr_accessor :logger, :path_prefix, :allowed_request_methods
25
+
26
+ def self.default_logger
27
+ logger = Logger.new(STDOUT)
28
+ logger.progname = 'omnihooks'
29
+ logger
30
+ end
31
+
32
+ def self.defaults
33
+ @defaults ||= {
34
+ logger: default_logger,
35
+ path_prefix: '/hooks',
36
+ allowed_request_methods: [:post]
37
+ }
38
+ end
39
+
40
+ def initialize
41
+ self.class.defaults.each_pair { |k, v| send("#{k}=", v) }
42
+ end
43
+ end
44
+
45
+
46
+ def self.config
47
+ Configuration.instance
48
+ end
49
+
50
+ def self.configure
51
+ yield config
52
+ end
53
+
54
+ def self.logger
55
+ config.logger
56
+ end
57
+
58
+ module Utils
59
+ module_function
60
+
61
+ def deep_merge(hash, other_hash)
62
+ target = hash.dup
63
+
64
+ other_hash.keys.each do |key|
65
+ if other_hash[key].is_a?(::Hash) && hash[key].is_a?(::Hash)
66
+ target[key] = deep_merge(target[key], other_hash[key])
67
+ next
68
+ end
69
+
70
+ target[key] = other_hash[key]
71
+ end
72
+
73
+ target
74
+ end
75
+ end
76
+ end
data/omnihooks.gemspec ADDED
@@ -0,0 +1,27 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'omnihooks/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "omnihooks"
8
+ spec.version = OmniHooks::VERSION
9
+ spec.authors = ["Karl Falconer"]
10
+ spec.email = ["karl.falconer@falconerdevelopment.com"]
11
+ spec.summary = 'A generalized framework for multiple-provider webhooks subscriptions.'
12
+ spec.homepage = ""
13
+ spec.license = "MIT"
14
+
15
+ spec.files = `git ls-files -z`.split("\x0")
16
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
17
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
18
+ spec.require_paths = ["lib"]
19
+
20
+ spec.add_dependency "activesupport", ">= 3.1"
21
+ spec.add_dependency 'rack', '~> 1.0'
22
+ spec.add_dependency 'hashie', ['>= 1.2', '< 4']
23
+
24
+ spec.add_development_dependency "bundler", "~> 1.7"
25
+ spec.add_development_dependency "rake", "~> 10.0"
26
+ spec.add_development_dependency 'rspec', '~> 3.3.0'
27
+ end
@@ -0,0 +1,48 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe OmniHooks::Builder do
4
+ describe '#provider' do
5
+ it 'translates a symbol to a constant' do
6
+ expect(OmniHooks::Strategies).to receive(:const_get).with('MyStrategy').and_return(Class.new)
7
+ OmniHooks::Builder.new(nil) do
8
+ provider :my_strategy
9
+ end
10
+ end
11
+
12
+ it 'accepts a class' do
13
+ class ExampleClass; end
14
+ expect do
15
+ OmniHooks::Builder.new(nil) do
16
+ provider ::ExampleClass
17
+ end
18
+ end.not_to raise_error
19
+ end
20
+
21
+ it "raises a helpful LoadError message if it can't find the class" do
22
+ expect do
23
+ OmniHooks::Builder.new(nil) do
24
+ provider :lorax
25
+ end
26
+ end.to raise_error(LoadError, 'Could not find matching strategy for :lorax. You may need to install an additional gem (such as omnihooks-lorax).')
27
+ end
28
+ end
29
+ describe '#options' do
30
+ it 'merges provided options in' do
31
+ k = Class.new
32
+ b = OmniHooks::Builder.new(nil)
33
+ expect(b).to receive(:use).with(k, :foo => 'bar', :baz => 'tik')
34
+
35
+ b.options :foo => 'bar'
36
+ b.provider k, :baz => 'tik'
37
+ end
38
+
39
+ it 'adds an argument if no options are provided' do
40
+ k = Class.new
41
+ b = OmniHooks::Builder.new(nil)
42
+ expect(b).to receive(:use).with(k, :foo => 'bar')
43
+
44
+ b.options :foo => 'bar'
45
+ b.provider k
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,302 @@
1
+ require 'spec_helper'
2
+
3
+ def make_env(path = '/hooks/test', props = {})
4
+ {
5
+ 'REQUEST_METHOD' => 'POST',
6
+ 'PATH_INFO' => path,
7
+ 'rack.session' => {},
8
+ 'rack.input' => StringIO.new('test=true'),
9
+ }.merge(props)
10
+ end
11
+
12
+
13
+ RSpec.describe OmniHooks::Strategy do
14
+ let(:app) do
15
+ lambda { |_env| [404, {}, ['Awesome']] }
16
+ end
17
+
18
+ let(:fresh_strategy) do
19
+ c = Class.new
20
+ c.send(:include, OmniHooks::Strategy)
21
+ end
22
+
23
+ describe '.default_options' do
24
+ it 'is inherited from a parent class' do
25
+ superklass = Class.new
26
+ superklass.send :include, OmniHooks::Strategy
27
+ superklass.configure_options do |c|
28
+ c.foo = 'bar'
29
+ end
30
+
31
+ klass = Class.new(superklass)
32
+ expect(klass.default_options.foo).to eq('bar')
33
+ end
34
+ end
35
+
36
+ describe '.configure_options' do
37
+ subject do
38
+ c = Class.new
39
+ c.send(:include, OmniHooks::Strategy)
40
+ end
41
+
42
+ context 'when block is passed' do
43
+ it 'allows for default options setting' do
44
+ subject.configure_options do |c|
45
+ c.wakka = 'doo'
46
+ end
47
+ expect(subject.new(nil).options['wakka']).to eq('doo')
48
+ end
49
+
50
+ it "works when block doesn't evaluate to true" do
51
+ environment_variable = nil
52
+ subject.configure_options do |c|
53
+ c.abc = '123'
54
+ c.hgi = environment_variable
55
+ end
56
+ expect(subject.new(nil).options['abc']).to eq('123')
57
+ end
58
+ end
59
+
60
+ it 'takes a hash and deep merge it' do
61
+ subject.configure_options :abc => {:def => 123}
62
+ subject.configure_options :abc => {:hgi => 456}
63
+ expect(subject.new(nil).options['abc']).to eq('def' => 123, 'hgi' => 456)
64
+ end
65
+ end
66
+
67
+ describe '.option' do
68
+ subject do
69
+ c = Class.new
70
+ c.send(:include, OmniHooks::Strategy)
71
+ end
72
+ it 'sets a default value' do
73
+ subject.option :abc, 123
74
+ expect(subject.new(nil).options.abc).to eq(123)
75
+ end
76
+
77
+ it 'sets the default value to nil if none is provided' do
78
+ subject.option :abc
79
+ expect(subject.new(nil).options.abc).to be_nil
80
+ end
81
+ end
82
+
83
+ describe '.args' do
84
+ subject do
85
+ c = Class.new
86
+ c.send(:include, OmniHooks::Strategy)
87
+ end
88
+
89
+ it 'sets args to the specified argument if there is one' do
90
+ subject.args [:abc, :def]
91
+ expect(subject.args).to eq([:abc, :def])
92
+ end
93
+
94
+ it 'is inheritable' do
95
+ subject.args [:abc, :def]
96
+ c = Class.new(subject)
97
+ expect(c.args).to eq([:abc, :def])
98
+ end
99
+
100
+ it 'accepts corresponding options as default arg values' do
101
+ subject.args [:a, :b]
102
+ subject.option :a, '1'
103
+ subject.option :b, '2'
104
+
105
+ expect(subject.new(nil).options.a).to eq '1'
106
+ expect(subject.new(nil).options.b).to eq '2'
107
+ expect(subject.new(nil, '3', '4').options.b).to eq '4'
108
+ expect(subject.new(nil, nil, '4').options.a).to eq nil
109
+ end
110
+ end
111
+
112
+ describe '.instrument' do
113
+ subject do
114
+ c = Class.new
115
+ c.send(:include, OmniHooks::Strategy)
116
+ c.option :name, 'class'
117
+ c
118
+ end
119
+
120
+ it 'should forward event publication to backend' do
121
+ expect(ActiveSupport::Notifications).to receive(:instrument).with('class.foo', 'bar')
122
+ subject.instrument('foo', 'bar')
123
+ end
124
+
125
+ end
126
+
127
+ context 'fetcher procs' do
128
+ subject { fresh_strategy }
129
+ %w(event event_type).each do |fetcher|
130
+ describe ".#{fetcher}" do
131
+ it 'sets and retrieve a proc' do
132
+ proc = lambda { 'Hello' }
133
+ subject.send(fetcher, &proc)
134
+ expect(subject.send(fetcher)).to eq(proc)
135
+ end
136
+ end
137
+ end
138
+ end
139
+
140
+ context 'fetcher stacks' do
141
+ subject { fresh_strategy }
142
+ %w(event event_type).each do |fetcher|
143
+ describe ".#{fetcher}_stack" do
144
+ it 'is an array of called ancestral procs' do
145
+ fetchy = proc { 'Hello' }
146
+ subject.send(fetcher, &fetchy)
147
+ expect(subject.send("#{fetcher}_stack", subject.new(app))).to eq(['Hello'])
148
+ end
149
+ end
150
+ end
151
+ end
152
+
153
+ describe '#initialize' do
154
+ context 'options extraction' do
155
+ it 'is the last argument if the last argument is a Hash' do
156
+ expect(ExampleStrategy.new(app, :abc => 123).options[:abc]).to eq(123)
157
+ end
158
+
159
+ it 'is the default options if any are provided' do
160
+ allow(ExampleStrategy).to receive(:default_options).and_return(OmniHooks::Strategy::Options.new(:abc => 123))
161
+ expect(ExampleStrategy.new(app).options.abc).to eq(123)
162
+ end
163
+ end
164
+
165
+ context 'custom args' do
166
+ subject do
167
+ c = Class.new
168
+ c.send(:include, OmniHooks::Strategy)
169
+ end
170
+
171
+ it 'sets options based on the arguments if they are supplied' do
172
+ subject.args [:abc, :def]
173
+ s = subject.new app, 123, 456
174
+ expect(s.options[:abc]).to eq(123)
175
+ expect(s.options[:def]).to eq(456)
176
+ end
177
+ end
178
+ end
179
+
180
+ describe '#call' do
181
+ before(:all) do
182
+ @options = nil
183
+ end
184
+
185
+ let(:strategy) { ExampleStrategy.new(app, @options || {}) }
186
+
187
+ it 'duplicates and calls' do
188
+ klass = Class.new
189
+ klass.send :include, OmniHooks::Strategy
190
+ instance = klass.new(app)
191
+ expect(instance).to receive(:dup).and_return(instance)
192
+ instance.call('rack.session' => {})
193
+ end
194
+
195
+ context 'without a subscriber' do
196
+ it 'should return a sucess response' do
197
+ klass = Class.new
198
+ klass.send :include, OmniHooks::Strategy
199
+ klass.option :name, 'class'
200
+ klass.event { 'Foo' }
201
+ klass.event_type { 'bar' }
202
+ instance = klass.new(app)
203
+
204
+ expect(ActiveSupport::Notifications).to receive(:instrument).with('class.bar', 'Foo')
205
+
206
+ expect(instance.call(make_env('/hooks/class'))).to eq([200, {}, [nil]])
207
+ end
208
+
209
+ context 'with exception in event callback' do
210
+ let(:klass) { Class.new }
211
+ before(:each) do
212
+
213
+ klass.send :include, OmniHooks::Strategy
214
+ klass.option :name, 'class'
215
+ klass.event { raise 'Foo' }
216
+ end
217
+
218
+ it 'should not raise an error' do
219
+ instance = klass.new(app)
220
+ expect { instance.call(make_env('/hooks/class')) }.not_to raise_error
221
+ end
222
+
223
+ it 'should return a non 200 response' do
224
+ instance = klass.new(app)
225
+ expect(instance.call(make_env('/hooks/class'))).to eq([500, {}, [nil]])
226
+ end
227
+ end
228
+ end
229
+
230
+ context 'with an explicit subscriber' do
231
+ let(:subscriber) { Proc.new { nil } }
232
+ before(:each) do
233
+
234
+ ExampleStrategy.event { 'Foo' }
235
+ ExampleStrategy.event_type { request.params['type'] }
236
+ ExampleStrategy.configure do |events|
237
+ events.subscribe('foo.bar', subscriber)
238
+ end
239
+ end
240
+
241
+ context 'with matched event type' do
242
+ it 'should return a success response' do
243
+ expect(subscriber).to receive(:call).with('Foo')
244
+
245
+ expect(strategy.call(make_env('/hooks/test', {'rack.input' => StringIO.new('type=foo.bar&payload=test')}))).to eq([200, {}, [nil]])
246
+ end
247
+ end
248
+
249
+ context 'with unmatched event' do
250
+ it 'should return a success response' do
251
+ expect(subscriber).not_to receive(:call)
252
+
253
+ expect(strategy.call(make_env('/hooks/test', {'rack.input' => StringIO.new('type=foo.sam&payload=test')}))).to eq([200, {}, [nil]])
254
+ end
255
+ end
256
+
257
+ context 'with an exception in the subscriber' do
258
+ before(:each) do
259
+ expect(subscriber).to receive(:call).and_raise(RuntimeError)
260
+ end
261
+
262
+ it 'should return an error response' do
263
+ expect(strategy.call(make_env('/hooks/test', {'rack.input' => StringIO.new('type=foo.bar&payload=test')}))).to eq([500, {}, [nil]])
264
+ end
265
+ end
266
+
267
+ after(:each) do
268
+ # reset the handlers
269
+ ExampleStrategy.event
270
+ ExampleStrategy.event_type
271
+ end
272
+ end
273
+
274
+ context 'request method restriction' do
275
+ before do
276
+ OmniHooks.config.allowed_request_methods = [:put]
277
+ end
278
+
279
+ it 'does not allow a request method of the wrong type' do
280
+ expect { strategy.call(make_env) }.not_to raise_error
281
+ end
282
+
283
+ it 'forwards request method of the wrong type to application' do
284
+ expect(strategy.call(make_env)).to eq([404, {}, ['Awesome']])
285
+ end
286
+
287
+ it 'allows a request method of the correct type' do
288
+ expect(strategy.call(make_env('/hooks/test', 'REQUEST_METHOD' => 'PUT'))).to eq([200, {}, [nil]])
289
+ end
290
+
291
+ after do
292
+ OmniHooks.config.allowed_request_methods = [:post]
293
+ end
294
+ end
295
+ end
296
+
297
+ describe '#inspect' do
298
+ it 'returns the class name' do
299
+ expect(ExampleStrategy.new(app).inspect).to eq('#<ExampleStrategy>')
300
+ end
301
+ end
302
+ end
@@ -0,0 +1,35 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe OmniHooks do
4
+ describe '.strategies' do
5
+ it 'increases when a new strategy is made' do
6
+ expect {
7
+ class ExampleStrategy
8
+ include OmniHooks::Strategy
9
+ end
10
+ }.to change(OmniHooks.strategies, :size).by(1)
11
+ expect(OmniHooks.strategies.last).to eq(ExampleStrategy)
12
+ end
13
+ end
14
+
15
+ context 'configuration' do
16
+ describe '.defaults' do
17
+ it 'is a hash of default configuration' do
18
+ expect(OmniHooks::Configuration.defaults).to be_kind_of(Hash)
19
+ end
20
+ end
21
+
22
+ it 'is callable from .configure' do
23
+ OmniHooks.configure do |c|
24
+ expect(c).to be_kind_of(OmniHooks::Configuration)
25
+ end
26
+ end
27
+ end
28
+
29
+ describe '.logger' do
30
+ it 'calls through to the configured logger' do
31
+ allow(OmniHooks).to receive(:config).and_return(double(:logger => 'foo'))
32
+ expect(OmniHooks.logger).to eq('foo')
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,115 @@
1
+ require 'rspec'
2
+ require 'omnihooks'
3
+
4
+
5
+ # This file was generated by the `rspec --init` command. Conventionally, all
6
+ # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
7
+ # The generated `.rspec` file contains `--require spec_helper` which will cause
8
+ # this file to always be loaded, without a need to explicitly require it in any
9
+ # files.
10
+ #
11
+ # Given that it is always loaded, you are encouraged to keep this file as
12
+ # light-weight as possible. Requiring heavyweight dependencies from this file
13
+ # will add to the boot time of your test suite on EVERY test run, even for an
14
+ # individual file that may not need all of that loaded. Instead, consider making
15
+ # a separate helper file that requires the additional dependencies and performs
16
+ # the additional setup, and require it from the spec files that actually need
17
+ # it.
18
+ #
19
+ # The `.rspec` file also contains a few flags that are not defaults but that
20
+ # users commonly want.
21
+ #
22
+ # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
23
+ RSpec.configure do |config|
24
+ # rspec-expectations config goes here. You can use an alternate
25
+ # assertion/expectation library such as wrong or the stdlib/minitest
26
+ # assertions if you prefer.
27
+ config.expect_with :rspec do |expectations|
28
+ # This option will default to `true` in RSpec 4. It makes the `description`
29
+ # and `failure_message` of custom matchers include text for helper methods
30
+ # defined using `chain`, e.g.:
31
+ # be_bigger_than(2).and_smaller_than(4).description
32
+ # # => "be bigger than 2 and smaller than 4"
33
+ # ...rather than:
34
+ # # => "be bigger than 2"
35
+ expectations.include_chain_clauses_in_custom_matcher_descriptions = true
36
+ end
37
+
38
+ # rspec-mocks config goes here. You can use an alternate test double
39
+ # library (such as bogus or mocha) by changing the `mock_with` option here.
40
+ config.mock_with :rspec do |mocks|
41
+ # Prevents you from mocking or stubbing a method that does not exist on
42
+ # a real object. This is generally recommended, and will default to
43
+ # `true` in RSpec 4.
44
+ mocks.verify_partial_doubles = true
45
+ end
46
+
47
+ # The settings below are suggested to provide a good initial experience
48
+ # with RSpec, but feel free to customize to your heart's content.
49
+
50
+ # These two settings work together to allow you to limit a spec run
51
+ # to individual examples or groups you care about by tagging them with
52
+ # `:focus` metadata. When nothing is tagged with `:focus`, all examples
53
+ # get run.
54
+ config.filter_run :focus
55
+ config.run_all_when_everything_filtered = true
56
+
57
+ # Allows RSpec to persist some state between runs in order to support
58
+ # the `--only-failures` and `--next-failure` CLI options. We recommend
59
+ # you configure your source control system to ignore this file.
60
+ config.example_status_persistence_file_path = "spec/examples.txt"
61
+
62
+ # Limits the available syntax to the non-monkey patched syntax that is
63
+ # recommended. For more details, see:
64
+ # - http://myronmars.to/n/dev-blog/2012/06/rspecs-new-expectation-syntax
65
+ # - http://www.teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/
66
+ # - http://myronmars.to/n/dev-blog/2014/05/notable-changes-in-rspec-3#new__config_option_to_disable_rspeccore_monkey_patching
67
+ config.disable_monkey_patching!
68
+
69
+ # This setting enables warnings. It's recommended, but in some cases may
70
+ # be too noisy due to issues in dependencies.
71
+ config.warnings = true
72
+
73
+ # Many RSpec users commonly either run the entire suite or an individual
74
+ # file, and it's useful to allow more verbose output when running an
75
+ # individual spec file.
76
+ if config.files_to_run.one?
77
+ # Use the documentation formatter for detailed output,
78
+ # unless a formatter has already been configured
79
+ # (e.g. via a command-line flag).
80
+ config.default_formatter = 'doc'
81
+ end
82
+
83
+ # Print the 10 slowest examples and example groups at the
84
+ # end of the spec run, to help surface which specs are running
85
+ # particularly slow.
86
+ config.profile_examples = 10
87
+
88
+ # Run specs in random order to surface order dependencies. If you find an
89
+ # order dependency and want to debug it, you can fix the order by providing
90
+ # the seed, which is printed after each run.
91
+ # --seed 1234
92
+ config.order = :random
93
+
94
+ # Seed global randomization in this process using the `--seed` CLI option.
95
+ # Setting this allows you to use `--seed` to deterministically reproduce
96
+ # test failures related to randomization by passing the same `--seed` value
97
+ # as the one that triggered the failure.
98
+ Kernel.srand config.seed
99
+
100
+ end
101
+
102
+ class ExampleStrategy
103
+ include OmniHooks::Strategy
104
+ attr_reader :last_env
105
+ option :name, 'test'
106
+
107
+ def call(env)
108
+ self.call!(env)
109
+ end
110
+
111
+ def initialize(*args, &block)
112
+ super
113
+ @fail = nil
114
+ end
115
+ end
metadata ADDED
@@ -0,0 +1,156 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: omnihooks
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Karl Falconer
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-09-02 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activesupport
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '3.1'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '3.1'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rack
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '1.0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '1.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: hashie
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '1.2'
48
+ - - "<"
49
+ - !ruby/object:Gem::Version
50
+ version: '4'
51
+ type: :runtime
52
+ prerelease: false
53
+ version_requirements: !ruby/object:Gem::Requirement
54
+ requirements:
55
+ - - ">="
56
+ - !ruby/object:Gem::Version
57
+ version: '1.2'
58
+ - - "<"
59
+ - !ruby/object:Gem::Version
60
+ version: '4'
61
+ - !ruby/object:Gem::Dependency
62
+ name: bundler
63
+ requirement: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - "~>"
66
+ - !ruby/object:Gem::Version
67
+ version: '1.7'
68
+ type: :development
69
+ prerelease: false
70
+ version_requirements: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - "~>"
73
+ - !ruby/object:Gem::Version
74
+ version: '1.7'
75
+ - !ruby/object:Gem::Dependency
76
+ name: rake
77
+ requirement: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - "~>"
80
+ - !ruby/object:Gem::Version
81
+ version: '10.0'
82
+ type: :development
83
+ prerelease: false
84
+ version_requirements: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - "~>"
87
+ - !ruby/object:Gem::Version
88
+ version: '10.0'
89
+ - !ruby/object:Gem::Dependency
90
+ name: rspec
91
+ requirement: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - "~>"
94
+ - !ruby/object:Gem::Version
95
+ version: 3.3.0
96
+ type: :development
97
+ prerelease: false
98
+ version_requirements: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - "~>"
101
+ - !ruby/object:Gem::Version
102
+ version: 3.3.0
103
+ description:
104
+ email:
105
+ - karl.falconer@falconerdevelopment.com
106
+ executables: []
107
+ extensions: []
108
+ extra_rdoc_files: []
109
+ files:
110
+ - ".gitignore"
111
+ - ".rspec"
112
+ - ".ruby-gemsets"
113
+ - ".ruby-version"
114
+ - Gemfile
115
+ - LICENSE.txt
116
+ - README.md
117
+ - Rakefile
118
+ - lib/omnihooks.rb
119
+ - lib/omnihooks/builder.rb
120
+ - lib/omnihooks/strategies/developer.rb
121
+ - lib/omnihooks/strategy.rb
122
+ - lib/omnihooks/version.rb
123
+ - omnihooks.gemspec
124
+ - spec/omnihooks/builder_spec.rb
125
+ - spec/omnihooks/strategy_spec.rb
126
+ - spec/omnihooks_spec.rb
127
+ - spec/spec_helper.rb
128
+ homepage: ''
129
+ licenses:
130
+ - MIT
131
+ metadata: {}
132
+ post_install_message:
133
+ rdoc_options: []
134
+ require_paths:
135
+ - lib
136
+ required_ruby_version: !ruby/object:Gem::Requirement
137
+ requirements:
138
+ - - ">="
139
+ - !ruby/object:Gem::Version
140
+ version: '0'
141
+ required_rubygems_version: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - ">="
144
+ - !ruby/object:Gem::Version
145
+ version: '0'
146
+ requirements: []
147
+ rubyforge_project:
148
+ rubygems_version: 2.4.8
149
+ signing_key:
150
+ specification_version: 4
151
+ summary: A generalized framework for multiple-provider webhooks subscriptions.
152
+ test_files:
153
+ - spec/omnihooks/builder_spec.rb
154
+ - spec/omnihooks/strategy_spec.rb
155
+ - spec/omnihooks_spec.rb
156
+ - spec/spec_helper.rb