ball_gag 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,2 @@
1
+ .rvmrc
2
+
data/Gemfile ADDED
@@ -0,0 +1,18 @@
1
+ source 'http://rubygems.org'
2
+
3
+ gemspec
4
+
5
+ group :development do
6
+ gem 'rake', '>= 0.9.2'
7
+ gem 'rspec', '>=2.6.0'
8
+ gem 'spork', '~> 0.9.0.rc9'
9
+ gem 'guard'
10
+ gem 'rb-fsevent'
11
+ gem 'growl_notify'
12
+ gem 'guard-rspec'
13
+ gem 'guard-spork'
14
+ gem 'pry'
15
+ gem 'pry-remote'
16
+ gem 'activemodel'
17
+ end
18
+
@@ -0,0 +1,72 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ ball_gag (0.0.1)
5
+
6
+ GEM
7
+ remote: http://rubygems.org/
8
+ specs:
9
+ activemodel (3.1.1)
10
+ activesupport (= 3.1.1)
11
+ builder (~> 3.0.0)
12
+ i18n (~> 0.6)
13
+ activesupport (3.1.1)
14
+ multi_json (~> 1.0)
15
+ builder (3.0.0)
16
+ coderay (0.9.8)
17
+ diff-lcs (1.1.3)
18
+ growl_notify (0.0.3)
19
+ rb-appscript
20
+ guard (0.8.4)
21
+ thor (~> 0.14.6)
22
+ guard-rspec (0.5.0)
23
+ guard (>= 0.8.4)
24
+ guard-spork (0.3.1)
25
+ guard (>= 0.8.4)
26
+ spork (>= 0.8.4)
27
+ i18n (0.6.0)
28
+ method_source (0.6.6)
29
+ ruby_parser (~> 2.0.5)
30
+ multi_json (1.0.3)
31
+ pry (0.9.6.2)
32
+ coderay (~> 0.9.8)
33
+ method_source (~> 0.6.5)
34
+ ruby_parser (~> 2.0.5)
35
+ slop (~> 2.1.0)
36
+ pry-remote (0.1.0)
37
+ pry (~> 0.9.6)
38
+ slop (~> 2.1)
39
+ rake (0.9.2)
40
+ rb-appscript (0.6.1)
41
+ rb-fsevent (0.4.3.1)
42
+ rspec (2.6.0)
43
+ rspec-core (~> 2.6.0)
44
+ rspec-expectations (~> 2.6.0)
45
+ rspec-mocks (~> 2.6.0)
46
+ rspec-core (2.6.4)
47
+ rspec-expectations (2.6.0)
48
+ diff-lcs (~> 1.1.2)
49
+ rspec-mocks (2.6.0)
50
+ ruby_parser (2.0.6)
51
+ sexp_processor (~> 3.0)
52
+ sexp_processor (3.0.7)
53
+ slop (2.1.0)
54
+ spork (0.9.0.rc9)
55
+ thor (0.14.6)
56
+
57
+ PLATFORMS
58
+ ruby
59
+
60
+ DEPENDENCIES
61
+ activemodel
62
+ ball_gag!
63
+ growl_notify
64
+ guard
65
+ guard-rspec
66
+ guard-spork
67
+ pry
68
+ pry-remote
69
+ rake (>= 0.9.2)
70
+ rb-fsevent
71
+ rspec (>= 2.6.0)
72
+ spork (~> 0.9.0.rc9)
@@ -0,0 +1,12 @@
1
+ guard 'spork' do
2
+ watch('spec/spec_helper.rb')
3
+ end
4
+
5
+ guard 'rspec', version: 2, cli: '--drb --color --format nested', all_after_pass: false do
6
+ watch(/^spec\/.+_spec\.rb$/)
7
+ watch(/^lib\/ball_gag\/(.+)\.rb$/) { |m| "spec/#{m[1]}_spec.rb" }
8
+ watch(/^lib\/ball_gag\.rb$/) { 'spec' }
9
+ watch('spec/spec_helper.rb') { 'spec' }
10
+ watch(/spec\/support\/.+\.rb$/) { 'spec' }
11
+ end
12
+
data/License ADDED
@@ -0,0 +1,8 @@
1
+ Copyright (c) 2011 Duncan Beevers - Dweebd LLC
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4
+
5
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6
+
7
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
8
+
@@ -0,0 +1,8 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'rspec/core/rake_task'
3
+
4
+ task default: :spec
5
+
6
+ desc 'Run specs'
7
+ RSpec::Core::RakeTask.new
8
+
@@ -0,0 +1,122 @@
1
+ # BallGag Pluggable User-Content Validation
2
+
3
+ Validate user-generated content against acceptable content rules. BallGag provides a convenient interface for describing the acceptability of the attributes of Ruby objects.
4
+
5
+ When an attribute is gagged, <tt>#{method}_gagged?</tt> and <tt>#{method}_not_gagged?</tt> methods are added to the class.
6
+
7
+ ## Example
8
+ Here's a simple example demonstrating gagging of a single attribute.
9
+
10
+ ````ruby
11
+ class Post
12
+ include BallGag
13
+ attr_accessor :body
14
+
15
+ gag(:body) { |body| !/damn/.match body }
16
+
17
+ def initialize body
18
+ self.body = body
19
+ end
20
+ end
21
+ ````
22
+
23
+ ````ruby
24
+ Post.new('That was some damn good watermelon').body_gagged?
25
+ # true
26
+
27
+ Post.new('That was some fine watermelon.').body_gagged?
28
+ # false
29
+ ````
30
+
31
+ ## Using it with Rails
32
+ BallGag integrates easily into your Rails app, allowing you validate that <tt>ActiveModel</tt> attributes are acceptable. First, add the gem to your <tt>Gemfile</tt>
33
+
34
+ ````ruby
35
+ gem 'ball_gag'
36
+ ````
37
+
38
+ Now a <tt>NotGaggedValidator</tt> is available which you can use like this.
39
+
40
+ ````ruby
41
+ class Post < ActiveRecord::Base
42
+ include BallGag
43
+ gag :body { |body| !/damn/.match body }
44
+
45
+ validates :body, :not_gagged => true
46
+ end
47
+ ````
48
+
49
+ ## Engines / Global configuration
50
+ Each gagged attribute can be provided its own block to call when checking acceptability, or a global engine can be provided. Set a global engine like this.
51
+
52
+ ````ruby
53
+ BallGag.engine = lambda { |content| !/damn/.match content }
54
+ ````
55
+
56
+ Any object that responds to <tt>call</tt> can be used as an engine.
57
+
58
+ ## Signature
59
+
60
+ The first argument to <tt>call</tt> is the value, or hash of values to be checked for acceptability.
61
+
62
+ If a single attribute is gagged, the <tt>call</tt> method is invoked with the value of the attribute. If multiple attributes are gagged, the <tt>call</tt> method is invoked with a hash whose keys are the gagged attribute names and whose values are the attributes' values.
63
+
64
+ The second argument to <tt>call</tt> is a hash of options. This hash has the following entries:
65
+ <table>
66
+ <tbody>
67
+ <tr>
68
+ <td><tt><strong>:instance</strong></tt></td>
69
+ <td>The instance on which the attribute checked was invoked.</td>
70
+ </tr>
71
+ <tr>
72
+ <td><tt><strong>:options</strong></tt></td>
73
+ <td>The options supplied to the specific <tt>gag</tt> call.</td>
74
+ </tr>
75
+ <tr>
76
+ <td><tt><strong>:single</strong></tt></td>
77
+ <td>A boolean indicating whether the <tt>gag</tt> method was invoked with only one attribute.</td>
78
+ </tr>
79
+ <tr>
80
+ <td><tt><strong>:attr</strong></tt></td>
81
+ <td>The name of the attribute being checked. This is only useful for single-attribute gags.</td>
82
+ </tr>
83
+ </tbody>
84
+ </table>
85
+
86
+ Since options provided to the <tt>gag</tt> method are passed through to your back-end, you can use them when building up a request to send to a 3rd-party.
87
+
88
+ ## Integration with 3rd-party tools
89
+ The real power of BallGag is in how it integrates with 3rd-party moderation services. BallGag helps you thread model-specific information through to a remote back-end.
90
+
91
+ An advanced engine may need to handle accepability of single and multiple attributes. The handler can differentiate between the two types of calls by inspecting the <tt>:single</tt> key of the options it's called with.
92
+
93
+ Here's an example engine that accepts single and multi-attribute calls and validates against a made-up RESTful moderation service.
94
+
95
+ ````ruby
96
+ class ModerationServiceEngine
97
+ include HTTParty
98
+ base_uri MyApp::Application.config.moderation_service_uri
99
+
100
+ def self.call fields, options
101
+ if options.delete(:single)
102
+ return call(options[:attr] => fields, options)
103
+ end
104
+
105
+ gag_options = options[:options]
106
+
107
+ sender = gag_options[:sender].
108
+ bind(instance).call
109
+
110
+ response = new(*MyApp::ApplicationConfig.moderation_service_credentials).
111
+ get('/moderate', query: { fields: fields, user: sender })
112
+
113
+ response['fields']
114
+ end
115
+ end
116
+
117
+ BallGag.engine = ModerationServiceEngine
118
+ ````
119
+
120
+ ## License
121
+
122
+ MIT License
@@ -0,0 +1,23 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path('../lib', __FILE__)
3
+ require 'ball_gag/version'
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = 'ball_gag'
7
+ s.version = BallGag::VERSION
8
+ s.authors = ['Duncan Beevers']
9
+ s.email = ['duncan@dweebd.com']
10
+ s.homepage = ''
11
+ s.summary = 'Pluggable User-Content Validation'
12
+ s.description = 'Validate user input using pluggable back-ends'
13
+
14
+ s.rubyforge_project = 'ball_gag'
15
+
16
+ s.files = `git ls-files`.split("\n")
17
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
18
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
19
+ s.require_paths = ['lib']
20
+
21
+ # specify any dependencies here; for example:
22
+ s.add_development_dependency 'rspec'
23
+ end
@@ -0,0 +1,10 @@
1
+ require 'ball_gag/version'
2
+ require 'ball_gag/gag'
3
+ require 'ball_gag/engine'
4
+ require 'ball_gag/errors'
5
+
6
+ begin
7
+ require 'ball_gag/validations'
8
+ rescue NameError # No dice
9
+ end
10
+
@@ -0,0 +1,16 @@
1
+ module BallGag
2
+ module Engine
3
+ module ClassMethods
4
+ def engine
5
+ @engine
6
+ end
7
+
8
+ def engine= new_engine
9
+ @engine = new_engine
10
+ end
11
+ end
12
+ end
13
+
14
+ extend Engine::ClassMethods
15
+ end
16
+
@@ -0,0 +1,8 @@
1
+ module BallGag
2
+ class NoEngineConfiguredError < StandardError
3
+ end
4
+
5
+ class BadResultsMappingError < StandardError
6
+ end
7
+ end
8
+
@@ -0,0 +1,139 @@
1
+ module BallGag
2
+ def self.included base
3
+ @gagged_inclusions ||= {}
4
+ if !@gagged_inclusions[base]
5
+ @gagged_inclusions[base] = true
6
+ base.extend ClassMethods
7
+ end
8
+ end
9
+
10
+ module ClassMethods
11
+ def gagged_attributes
12
+ clear_gagged_attributes unless @gagged_attributes
13
+ @gagged_attributes.keys
14
+ end
15
+
16
+ def gag *arguments, &block
17
+ define_and_mixin_gagged_attributes_methods
18
+
19
+ callable = arguments.pop if arguments.last.respond_to? :call
20
+ options = arguments.pop if arguments.last.kind_of? Hash
21
+
22
+ to_call = callable || block || BallGag.engine ||
23
+ lambda { |*| raise NoEngineConfiguredError }
24
+
25
+ define_not_gagged_interpellations(arguments, to_call, options)
26
+
27
+ arguments.each do |attribute|
28
+ @gagged_attributes[attribute] = to_call
29
+ define_gagged_interpellation attribute
30
+ end
31
+
32
+ arguments
33
+ end
34
+
35
+ def clear_gagged_attributes
36
+ undefine_gagged_attributes_methods
37
+ end
38
+
39
+ private
40
+ def define_and_mixin_gagged_attributes_methods
41
+ return if @gagged_attributes_methods
42
+
43
+ undefine_gagged_attributes_methods
44
+ end
45
+
46
+ def define_not_gagged_interpellations attributes, callable, gag_options = nil
47
+ one_attribute = 1 == attributes.length
48
+ unsanitized_values = lambda { |it|
49
+ attributes.inject({}) do |a, attribute|
50
+ a[attribute] = it.send(attribute)
51
+ a
52
+ end
53
+ }
54
+
55
+ output = one_attribute ?
56
+ lambda { |it, attr| unsanitized_values.call(it)[attr] } :
57
+ lambda { |it, attr| unsanitized_values.call(it) }
58
+
59
+ arity = callable.respond_to?(:arity) ?
60
+ callable.arity :
61
+ callable.method(:call).arity.abs
62
+
63
+ fn = nil
64
+ if 1 == arity
65
+ fn = lambda { |it, attr| callable.call(output.call(it, attr)) }
66
+ else
67
+ options = {}
68
+ fn = lambda do |it, attr|
69
+ callable.call(output.call(it, attr),
70
+ { options: gag_options,
71
+ instance: it,
72
+ attr: attr,
73
+ single: one_attribute
74
+ }
75
+ )
76
+ end
77
+ end
78
+
79
+
80
+ attributes.each do |attr|
81
+ @gagged_attributes_methods.send(:define_method,
82
+ gagged_attribute_negative_interpellation_name(attr)) do
83
+ @gagged_attribute_results ||= {}
84
+
85
+ # Have we performed this call already?
86
+ unless @gagged_attribute_results.has_key?(attributes)
87
+ @gagged_attribute_results[attributes] = fn.call(self, attr)
88
+ end
89
+
90
+ call_result = @gagged_attribute_results[attributes]
91
+
92
+ if one_attribute
93
+ # If only one attribute was supplied, a simple
94
+ # boolean response is sufficient
95
+ return false unless call_result
96
+ return true unless call_result.respond_to?(:[])
97
+ else
98
+ raise BadResultsMappingError unless call_result.respond_to?(:[])
99
+ end
100
+
101
+ @gagged_attribute_results[attributes][attr]
102
+ end
103
+ end
104
+ end
105
+
106
+ def define_gagged_interpellation attribute
107
+ neg_method = gagged_attribute_negative_interpellation_name(attribute)
108
+ @gagged_attributes_methods.send(:define_method,
109
+ gagged_attribute_interpellation_name(attribute)) do
110
+ !method(neg_method).call
111
+ end
112
+ end
113
+
114
+ def gagged_attribute_interpellation_name attribute
115
+ "#{attribute}_gagged?"
116
+ end
117
+
118
+ def gagged_attribute_negative_interpellation_name attribute
119
+ "#{attribute}_not_gagged?"
120
+ end
121
+
122
+ def undefine_gagged_attributes_methods
123
+ @gagged_attributes.keys.each do |attribute|
124
+
125
+ @gagged_attributes_methods.send(:remove_method,
126
+ gagged_attribute_interpellation_name(attribute))
127
+
128
+ @gagged_attributes_methods.send(:remove_method,
129
+ gagged_attribute_negative_interpellation_name(attribute))
130
+
131
+ end if @gagged_attributes
132
+
133
+ @gagged_attributes_methods = Module.new
134
+ include @gagged_attributes_methods
135
+ @gagged_attributes = {}
136
+ end
137
+ end
138
+ end
139
+
@@ -0,0 +1,8 @@
1
+ class NotGaggedValidator < ActiveModel::EachValidator
2
+ def validate_each object, attribute, value
3
+ if object.method("#{attribute}_gagged?").call
4
+ object.errors[attribute] << (options[:message] || 'is gagged')
5
+ end
6
+ end
7
+ end
8
+
@@ -0,0 +1,4 @@
1
+ module BallGag
2
+ VERSION = '0.0.1'
3
+ end
4
+
@@ -0,0 +1,22 @@
1
+ require 'spec_helper'
2
+
3
+ describe BallGag do
4
+ before { BallGag.engine = nil }
5
+
6
+ it 'should have no engine' do
7
+ BallGag.engine.should be_nil
8
+ end
9
+
10
+ it 'should have configurable engine' do
11
+ mock_engine = mock('engine')
12
+ BallGag.engine = mock_engine
13
+ BallGag.engine.should eq mock_engine
14
+ end
15
+
16
+ it 'should use engine' do
17
+ BallGag.engine = ExampleEngine
18
+ ExampleModel.gag :words, { strict: true }
19
+ ExampleModel.new.words_gagged?.should be_false
20
+ end
21
+ end
22
+
@@ -0,0 +1,267 @@
1
+ require 'spec_helper'
2
+
3
+ describe ExampleModel do
4
+ before do
5
+ BallGag.engine = nil
6
+ ExampleModel.clear_gagged_attributes
7
+ end
8
+
9
+ describe '#gag' do
10
+ it 'should add #{attribute}_gagged? method' do
11
+ ExampleModel.gag :words
12
+ ExampleModel.new.should respond_to :words_gagged?
13
+ end
14
+
15
+ it 'should add #{attribute}_not_gagged? method' do
16
+ ExampleModel.gag :words
17
+ ExampleModel.new.should respond_to :words_not_gagged?
18
+ end
19
+ end
20
+
21
+ describe '#gagged_attributes' do
22
+ it 'should have no gagged attributes' do
23
+ ExampleModel.gagged_attributes.should be_empty
24
+ end
25
+
26
+ it 'should gag attribute' do
27
+ ExampleModel.gag :words
28
+ ExampleModel.gagged_attributes.should include :words
29
+ end
30
+
31
+ it 'should gag multiple attributes' do
32
+ ExampleModel.gag :words, :email
33
+ ExampleModel.gagged_attributes.should include :words
34
+ ExampleModel.gagged_attributes.should include :email
35
+ end
36
+ end
37
+
38
+ describe '#clear_gagged_attributes' do
39
+ it 'should clear gagged attributes' do
40
+ ExampleModel.gag :words
41
+ ExampleModel.clear_gagged_attributes
42
+ ExampleModel.gagged_attributes.should be_empty
43
+ end
44
+
45
+ it 'should remove #{attribute}_not_gagged? method' do
46
+ ExampleModel.gag :words
47
+ ExampleModel.new.should respond_to(:words_not_gagged?)
48
+ ExampleModel.clear_gagged_attributes
49
+ ExampleModel.new.should_not respond_to(:words_not_gagged?)
50
+ end
51
+
52
+ it 'should remove #{attribute}_gagged? method' do
53
+ ExampleModel.gag :words
54
+ ExampleModel.new.should respond_to(:words_gagged?)
55
+ ExampleModel.clear_gagged_attributes
56
+ ExampleModel.new.should_not respond_to(:words_gagged?)
57
+ end
58
+ end
59
+
60
+ describe 'single attribute gagged' do
61
+ describe 'return values' do
62
+ context 'when callable returns' do
63
+ context 'true' do
64
+ before do
65
+ ExampleModel.gag :words, lambda { |*| true }
66
+ end
67
+
68
+ it '#{attribute}_gagged? should be false' do
69
+ ExampleModel.new.words_gagged?.should be_false
70
+ end
71
+ it '#{attribute}_not_gagged? should be true' do
72
+ ExampleModel.new.words_not_gagged?.should be_true
73
+ end
74
+ end
75
+
76
+ context 'false' do
77
+ before do
78
+ ExampleModel.gag :words, lambda { |*| false }
79
+ end
80
+
81
+ it '#{attribute}_gagged? should be true' do
82
+ ExampleModel.new.words_gagged?.should be_true
83
+ end
84
+ it '#{attribute}_not_gagged? should be false' do
85
+ ExampleModel.new.words_not_gagged?.should be_false
86
+ end
87
+ end
88
+ end
89
+ end
90
+
91
+ specify 'callable is called with attribute value' do
92
+ callable = lambda { |words| }
93
+ ExampleModel.gag :words, callable
94
+
95
+ mock_words = mock('words')
96
+
97
+ callable.should_receive(:call).
98
+ with(mock_words)
99
+
100
+ instance = ExampleModel.new
101
+ instance.stub!(words: mock_words)
102
+
103
+ instance.words_gagged?
104
+ end
105
+
106
+ specify 'callable is called with single: true in options' do
107
+ callable = lambda { |words, options| }
108
+ ExampleModel.gag :words, callable
109
+ callable.should_receive(:call).
110
+ with(anything, hash_including(single: true))
111
+ ExampleModel.new.words_gagged?
112
+ end
113
+ end
114
+
115
+ describe 'multiple attributes gagged' do
116
+ describe 'return values' do
117
+ context 'when results map returns true for attribute' do
118
+ before { ExampleModel.gag :words, :email do |*| { words: true } end }
119
+
120
+ it 'should return false for #{attribute}_gagged?' do
121
+ ExampleModel.new.words_gagged?.should be_false
122
+ end
123
+
124
+ it 'should return true for #{attribute}_not_gagged?' do
125
+ ExampleModel.new.words_not_gagged?.should be_true
126
+ end
127
+ end
128
+ end
129
+
130
+ specify 'callable is called with map of attributes' do
131
+ callable = lambda { |map, options| }
132
+ mock_words = mock('words')
133
+ mock_email = mock('email')
134
+
135
+ ExampleModel.gag :words, :email, callable
136
+ callable.should_receive(:call).
137
+ with(
138
+ hash_including(words: mock_words, email: mock_email), anything).
139
+ and_return({})
140
+
141
+ instance = ExampleModel.new
142
+ instance.stub!(words: mock_words, email: mock_email)
143
+ instance.words_gagged?
144
+ end
145
+
146
+ specify 'callable is called with attr in options' do
147
+ callable = lambda { |map, options| }
148
+ ExampleModel.gag :words, :email, callable
149
+
150
+ callable.should_receive(:call).
151
+ with(anything, hash_including(attr: :words)).
152
+ and_return({})
153
+ ExampleModel.new.words_gagged?
154
+
155
+ callable.should_receive(:call).
156
+ with(anything, hash_including(attr: :email)).
157
+ and_return({})
158
+ ExampleModel.new.email_gagged?
159
+ end
160
+
161
+ specify 'callable is called with single: false in options' do
162
+ callable = lambda { |map, options| }
163
+ ExampleModel.gag :words, :email, callable
164
+ callable.should_receive(:call).
165
+ with(anything, hash_including(single: false)).
166
+ and_return({})
167
+ ExampleModel.new.words_gagged?
168
+ end
169
+ end
170
+
171
+ describe 'callable options' do
172
+ specify 'instance is in options' do
173
+ callable = lambda { |words, options| }
174
+ ExampleModel.gag :words, callable
175
+
176
+ instance = ExampleModel.new
177
+ callable.should_receive(:call).
178
+ with(anything, hash_including(instance: instance))
179
+
180
+ instance.words_gagged?
181
+ end
182
+
183
+ specify 'attr is in options' do
184
+ callable = lambda { |words, options| }
185
+ ExampleModel.gag :words, callable
186
+ callable.should_receive(:call).
187
+ with(anything, hash_including(attr: :words))
188
+
189
+ ExampleModel.new.words_gagged?
190
+ end
191
+
192
+ specify 'gag options are in options' do
193
+ callable = lambda { |words, options| }
194
+ mock_options = mock('options')
195
+ mock_options.stub(:kind_of?).
196
+ with(Hash).and_return(true)
197
+
198
+ ExampleModel.gag :words, mock_options, callable
199
+ callable.should_receive(:call).
200
+ with(anything, hash_including(options: mock_options))
201
+
202
+ ExampleModel.new.words_gagged?
203
+ end
204
+ end
205
+
206
+ describe 'callable result caching' do
207
+ it 'should cache the result of the callable' do
208
+ callable = lambda {}
209
+ ExampleModel.gag :words, callable
210
+
211
+ callable.should_receive(:call).once
212
+
213
+ instance = ExampleModel.new
214
+ 2.times { instance.words_gagged? }
215
+ end
216
+
217
+ it 'should separate invocations and cache results' do
218
+ callable = lambda { |words, instance| }
219
+ mock_words = mock('words')
220
+ mock_email = mock('email')
221
+
222
+ ExampleModel.gag :words, callable
223
+ ExampleModel.gag :email, callable
224
+
225
+ instance = ExampleModel.new
226
+ instance.stub!(words: mock_words, email: mock_email)
227
+
228
+ callable.should_receive(:call).
229
+ with(mock_words, anything).
230
+ once
231
+
232
+ callable.should_receive(:call).
233
+ with(mock_email, anything).
234
+ once
235
+
236
+ 2.times { instance.words_gagged? }
237
+ 2.times { instance.email_gagged? }
238
+ end
239
+ end
240
+
241
+ describe 'error cases' do
242
+ context 'when gagged with no callable and no engine is configured' do
243
+ it 'should raise when gag is checekd' do
244
+ ExampleModel.gag :words
245
+
246
+ -> { ExampleModel.new.words_gagged? }.
247
+ should raise_error(BallGag::NoEngineConfiguredError)
248
+ end
249
+ end
250
+
251
+ context 'when multiple attributes are gagged' do
252
+ it 'should raise if callable returns non-map result' do
253
+ mock_result = mock('result')
254
+
255
+ mock_result.should_receive(:respond_to?).
256
+ with(:[]).and_return(false)
257
+
258
+ callable = lambda { |*| mock_result }
259
+ ExampleModel.gag :words, :email, callable
260
+
261
+ -> { ExampleModel.new.words_gagged? }.
262
+ should raise_error(BallGag::BadResultsMappingError)
263
+ end
264
+ end
265
+ end
266
+ end
267
+
@@ -0,0 +1,13 @@
1
+ require 'spork'
2
+
3
+ Spork.prefork do
4
+ require 'rspec'
5
+ require 'active_model'
6
+ require 'pry-remote'
7
+ end
8
+
9
+ Spork.each_run do
10
+ require 'ball_gag'
11
+ require File.join(File.dirname(__FILE__), 'support/models')
12
+ end
13
+
@@ -0,0 +1,29 @@
1
+ class ExampleModel
2
+ include BallGag
3
+
4
+ def words
5
+ 'Never has one man rocked so many.'
6
+ end
7
+
8
+ def email
9
+ 'theodore@example.com'
10
+ end
11
+ end
12
+
13
+ class ExampleActiveModel
14
+ include ActiveModel::Validations
15
+ include BallGag
16
+
17
+ def words
18
+ 'Welcome to the place where all the creatures meet.'
19
+ end
20
+ end
21
+
22
+ class ExampleEngine
23
+ class << self
24
+ def call value, options = {}
25
+ true
26
+ end
27
+ end
28
+ end
29
+
@@ -0,0 +1,33 @@
1
+ require 'spec_helper'
2
+
3
+ describe 'ActiveModel::Validations integration' do
4
+ before do
5
+ ExampleActiveModel.reset_callbacks :validate
6
+ end
7
+
8
+ it 'should call #{attribute}_gagged?' do
9
+ ExampleActiveModel.validates :words, not_gagged: true
10
+
11
+ instance = ExampleActiveModel.new
12
+ instance.should_receive(:words_gagged?)
13
+ instance.valid?
14
+ end
15
+
16
+ it 'should add default error message' do
17
+ ExampleActiveModel.validates :words, not_gagged: true
18
+ instance = ExampleActiveModel.new
19
+ instance.stub!(words_gagged?: true)
20
+ instance.valid?
21
+ instance.errors[:words].should include 'is gagged'
22
+ end
23
+
24
+ it 'should respect custom error message' do
25
+ ExampleActiveModel.validates :words,
26
+ not_gagged: { message: 'is not acceptable' }
27
+ instance = ExampleActiveModel.new
28
+ instance.stub!(words_gagged?: true)
29
+ instance.valid?
30
+ instance.errors[:words].should include 'is not acceptable'
31
+ end
32
+ end
33
+
metadata ADDED
@@ -0,0 +1,80 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ball_gag
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Duncan Beevers
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2011-11-07 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: rspec
16
+ requirement: &70285274103000 !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :development
23
+ prerelease: false
24
+ version_requirements: *70285274103000
25
+ description: Validate user input using pluggable back-ends
26
+ email:
27
+ - duncan@dweebd.com
28
+ executables: []
29
+ extensions: []
30
+ extra_rdoc_files: []
31
+ files:
32
+ - .gitignore
33
+ - Gemfile
34
+ - Gemfile.lock
35
+ - Guardfile
36
+ - License
37
+ - Rakefile
38
+ - Readme.md
39
+ - ball_gag.gemspec
40
+ - lib/ball_gag.rb
41
+ - lib/ball_gag/engine.rb
42
+ - lib/ball_gag/errors.rb
43
+ - lib/ball_gag/gag.rb
44
+ - lib/ball_gag/validations.rb
45
+ - lib/ball_gag/version.rb
46
+ - spec/engine_spec.rb
47
+ - spec/gag_spec.rb
48
+ - spec/spec_helper.rb
49
+ - spec/support/models.rb
50
+ - spec/validations_spec.rb
51
+ homepage: ''
52
+ licenses: []
53
+ post_install_message:
54
+ rdoc_options: []
55
+ require_paths:
56
+ - lib
57
+ required_ruby_version: !ruby/object:Gem::Requirement
58
+ none: false
59
+ requirements:
60
+ - - ! '>='
61
+ - !ruby/object:Gem::Version
62
+ version: '0'
63
+ required_rubygems_version: !ruby/object:Gem::Requirement
64
+ none: false
65
+ requirements:
66
+ - - ! '>='
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ requirements: []
70
+ rubyforge_project: ball_gag
71
+ rubygems_version: 1.8.10
72
+ signing_key:
73
+ specification_version: 3
74
+ summary: Pluggable User-Content Validation
75
+ test_files:
76
+ - spec/engine_spec.rb
77
+ - spec/gag_spec.rb
78
+ - spec/spec_helper.rb
79
+ - spec/support/models.rb
80
+ - spec/validations_spec.rb