defender 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/.document ADDED
@@ -0,0 +1,5 @@
1
+ README.rdoc
2
+ lib/**/*.rb
3
+ bin/*
4
+ features/**/*.feature
5
+ LICENSE
data/.gitignore ADDED
@@ -0,0 +1,23 @@
1
+ ## MAC OS
2
+ .DS_Store
3
+
4
+ ## TEXTMATE
5
+ *.tmproj
6
+ tmtags
7
+
8
+ ## EMACS
9
+ *~
10
+ \#*
11
+ .\#*
12
+
13
+ ## VIM
14
+ *.swp
15
+
16
+ ## PROJECT::GENERAL
17
+ coverage
18
+ rdoc
19
+ pkg
20
+ doc
21
+ .yardoc
22
+
23
+ ## PROJECT::SPECIFIC
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2009 Henrik Hodne
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,24 @@
1
+ = defender
2
+
3
+ This is a Ruby wrapper of the Defensio[http://defensio.com] spam filtering API. To use this library, you need an API key from Defensio. Go ahead and {get one}[http://defensio.com/signup/].
4
+
5
+ == Testing
6
+
7
+ To run the tests, you need to pass the api key and owner url with the environment keys API_KEY and API_OWNER_URL, like this:
8
+ rake spec API_KEY="key1234" API_OWNER_URL="http://myblog.com"
9
+ The tests will fail with invalid keys and urls.
10
+
11
+ == Note on Patches/Pull Requests
12
+
13
+ * Fork the project.
14
+ * Make your feature addition or bug fix.
15
+ * Add tests for it. This is important so I don't break it in a
16
+ future version unintentionally.
17
+ * Commit, do not mess with rakefile, version, or history.
18
+ (if you want to have your own version, that is fine but
19
+ bump version in a commit by itself I can ignore when I pull)
20
+ * Send me a pull request. Bonus points for topic branches.
21
+
22
+ == Copyright
23
+
24
+ Copyright (c) 2009 Henrik Hodne. See LICENSE for details.
data/Rakefile ADDED
@@ -0,0 +1,69 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+
4
+ begin
5
+ require 'jeweler'
6
+ Jeweler::Tasks.new do |gem|
7
+ gem.name = "defender"
8
+ gem.summary = %Q{Ruby API wrapper for Defensio}
9
+ gem.description = %Q{A wrapper of the Defensio spam filtering service.}
10
+ gem.email = "henrik.hodne@binaryhex.com"
11
+ gem.homepage = "http://github.com/dvyjones/defender"
12
+ gem.authors = ["Henrik Hodne"]
13
+ gem.add_development_dependency "rspec", ">= 1.2.9"
14
+ gem.add_development_dependency "yard", ">= 0"
15
+ # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
16
+ end
17
+ Jeweler::GemcutterTasks.new
18
+ rescue LoadError
19
+ puts "Jeweler (or a dependency) not available. Install it with: sudo gem install jeweler"
20
+ end
21
+
22
+ require 'spec/rake/spectask'
23
+ Spec::Rake::SpecTask.new(:spec) do |spec|
24
+ spec.libs << 'lib' << 'spec'
25
+ spec.spec_files = FileList['spec/**/*_spec.rb']
26
+ end
27
+
28
+ Spec::Rake::SpecTask.new(:rcov) do |spec|
29
+ spec.libs << 'lib' << 'spec'
30
+ spec.pattern = 'spec/**/*_spec.rb'
31
+ spec.rcov = true
32
+ end
33
+
34
+ task :spec => :check_dependencies
35
+
36
+ begin
37
+ require 'reek/adapters/rake_task'
38
+ Reek::RakeTask.new do |t|
39
+ t.verbose = false
40
+ t.source_files = 'lib/**/*.rb'
41
+ end
42
+ rescue LoadError
43
+ task :reek do
44
+ abort "Reek is not available. In order to run reek, you must: sudo gem install reek"
45
+ end
46
+ end
47
+
48
+ begin
49
+ require 'roodi'
50
+ require 'roodi_task'
51
+ RoodiTask.new do |t|
52
+ t.verbose = false
53
+ end
54
+ rescue LoadError
55
+ task :roodi do
56
+ abort "Roodi is not available. In order to run roodi, you must: sudo gem install roodi"
57
+ end
58
+ end
59
+
60
+ task :default => :spec
61
+
62
+ begin
63
+ require 'yard'
64
+ YARD::Rake::YardocTask.new
65
+ rescue LoadError
66
+ task :yardoc do
67
+ abort "YARD is not available. In order to run yardoc, you must: sudo gem install yard"
68
+ end
69
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.1.0
data/lib/defender.rb ADDED
@@ -0,0 +1,328 @@
1
+ require 'yaml'
2
+ require 'net/http'
3
+
4
+ class Defender
5
+ # The Defensio API version currently supported by Defender
6
+ API_VERSION = "1.2"
7
+
8
+ DEFAULT_OPTIONS = {
9
+ service_type: "blog",
10
+ api_key: "",
11
+ owner_url: ""
12
+ }
13
+
14
+ ##
15
+ # Raised if an invalid or no API key is given
16
+ class APIKeyError < StandardError; end
17
+
18
+ ##
19
+ # The response from the {Defender#audit_comment} method. Should only be
20
+ # initialized by the library.
21
+ class CommentResponse
22
+ ##
23
+ # A message signature that uniquely identifies the comment in the Defensio
24
+ # system. This signature should be stored by the client for retraining
25
+ # purposes.
26
+ attr_reader :signature
27
+
28
+ ##
29
+ # A value indicating the relative likelihood of the comment being spam.
30
+ # This value should be stored by the client for use in building convenient
31
+ # spam sorting user-interfaces.
32
+ #
33
+ # @return [Float] A value between 0 and 1.
34
+ attr_reader :spaminess
35
+
36
+ ##
37
+ # Initialize a CommentResponse. Should only be called by the library.
38
+ #
39
+ # @param [Hash] response The response from the audit-comment call.
40
+ def initialize(response)
41
+ @signature = response["signature"]
42
+ @spam = response["spam"]
43
+ @spaminess = response["spaminess"].to_f
44
+ end
45
+
46
+ ##
47
+ # Returns true if Defensio marked the comment as spam, returns false
48
+ # otherwise.
49
+ #
50
+ # @return [Boolean]
51
+ def spam?
52
+ @spam
53
+ end
54
+
55
+ def to_s
56
+ @signature
57
+ end
58
+ end
59
+
60
+ ##
61
+ # The response from the {Defender#statistics} method. Should only be
62
+ # initialized by the library.
63
+ class Statistics
64
+ ##
65
+ # Describes the percentage of comments correctly identified as spam/ham by
66
+ # Defensio on this blog.
67
+ #
68
+ # @return [Float<0..1>]
69
+ attr_reader :accuracy
70
+
71
+ ##
72
+ # The number of spam comments caught by the filter.
73
+ attr_reader :spam
74
+
75
+ ##
76
+ # The number of ham (legitimate) comments accepted by the filter.
77
+ attr_reader :ham
78
+
79
+ ##
80
+ # The number of times a legitimate message was retrained from the spambox
81
+ # (i.e. "de-spammed" by the user)
82
+ attr_reader :false_positives
83
+
84
+ ##
85
+ # The number of times a spam message was retrained from comments box (i.e.
86
+ # "de-legitimized" by the user)
87
+ attr_reader :false_negatives
88
+
89
+ ##
90
+ # A boolean value indicating whether Defensio is still in its initial
91
+ # learning phase.
92
+ #
93
+ # @return [Boolean]
94
+ attr_reader :learning
95
+
96
+ ##
97
+ # More details on the reason(s) why Defensio is still in its initial
98
+ # learning phase.
99
+ attr_reader :learning_status
100
+
101
+ def initialize(response)
102
+ @accuracy = response["accuracy"]
103
+ @spam = response["spam"]
104
+ @ham = response["ham"]
105
+ @false_positives = response["false-positives"]
106
+ @false_negatives = response["false-negatives"]
107
+ @learning = response["learning"]
108
+ @learning_status = response["learning-status"]
109
+ end
110
+ end
111
+
112
+ attr_accessor :service_type, :api_key, :owner_url
113
+
114
+ ##
115
+ # Raises a StandardError with the error message from Defensio if the
116
+ # response is a "failed" one.
117
+ #
118
+ # @param [Hash] response The return value from {#call_action}.
119
+ def self.raise_if_error(response)
120
+ if response["status"] == "fail"
121
+ raise StandardError, response["message"]
122
+ end
123
+ response
124
+ end
125
+
126
+ ##
127
+ # Converts a hash with symbol keys and underscores to a hash with string
128
+ # keys and hyphens. Calls #strftime or #to_s on the values.
129
+ #
130
+ # @param [Hash] options Input options.
131
+ # @return [Hash]
132
+ def self.options_to_parameters(options)
133
+ opts = {}
134
+ options.each do |key, value|
135
+ if value.respond_to?(:strftime)
136
+ value = value.strftime("%Y/%m/%d")
137
+ end
138
+ opts[key.to_s.gsub("_", "-").downcase] = value.to_s
139
+ end
140
+ opts
141
+ end
142
+
143
+ ##
144
+ # Initialize Defender
145
+ #
146
+ # @param [Hash] opts The options hash.
147
+ # @option opts ["blog","app"] :service_type ("blog") The service type. May be
148
+ # "app" (use of Defender within an application) or "blog" (use of Defender
149
+ # to support a blogging platform).
150
+ # @option opts [String] :api_key Your API key. This option is required, the
151
+ # method calls will fail without it.
152
+ def initialize(opts={})
153
+ opts = DEFAULT_OPTIONS.merge(opts)
154
+ @service_type = opts[:service_type]
155
+ @api_key = opts[:api_key]
156
+ @owner_url = opts[:owner_url]
157
+ end
158
+
159
+ ##
160
+ # Checks if the given key is valid.
161
+ #
162
+ # @return [Boolean]
163
+ # @see http://defensio.com/api/#validate-key
164
+ def valid_key?
165
+ begin
166
+ response = call_action("validate-key")
167
+ if response["status"] == "success"
168
+ return true
169
+ else
170
+ return false
171
+ end
172
+ rescue StandardError
173
+ return false
174
+ end
175
+ end
176
+
177
+ ##
178
+ # Announce an article existence. This should (if feasible) be called when an
179
+ # article or blogpost is created so Defensio can analyse it.
180
+ #
181
+ # @param [#to_s] title The title of the article
182
+ # @param [#to_s] author The name of the author of the article
183
+ # @param [#to_s] author_email The email address of the person posting the
184
+ # article.
185
+ # @param [#to_s] content The content of the article itself.
186
+ # @param [#to_s] permalink The permalink of the article just posted.
187
+ # @raise [StandardError] If the call fails, a StandardError is raised with
188
+ # the error message given from Defensio.
189
+ # @return [Boolean] Returns true if the article was successfully announced,
190
+ # raises StandardError otherwise.
191
+ # @see http://defensio.com/api/#announce-article
192
+ def announce_article(title, author, author_email, content, permalink)
193
+ response = call_action("announce-article",
194
+ "article-title" => title.to_s,
195
+ "article-author" => author.to_s,
196
+ "article-author-email" => author_email.to_s,
197
+ "article-content" => content,
198
+ "permalink" => permalink)
199
+ true
200
+ end
201
+
202
+ ##
203
+ # Check if a comment is spam. This is the central action of Defensio.
204
+ #
205
+ # @param [Hash] opts All options are recommended, but only required if noted.
206
+ # @option opts [#to_s] :user_ip The IP address of whomever is posting the
207
+ # comment. This option is required.
208
+ # @option opts [#to_s, #strftime] :article_date The date the original blog
209
+ # article was posted. If a string is given, it must be in the format
210
+ # "yyyy/mm/dd". This option is required.
211
+ # @option opts [#to_s] :comment_author The name of the author of the comment.
212
+ # This option is required.
213
+ # @option opts ["comment", "trackback", "pingback", "other"] :comment_type
214
+ # The type of the comment being posted to the blog. This option is required
215
+ # @option opts [#to_s] :comment_content The actual content of the comment
216
+ # (strongly recommended to be included where ever possible).
217
+ # @option opts [#to_s] :comment_author_email The email address of the person
218
+ # posting the comment.
219
+ # @option opts [#to_s] :comment_author_url The URL of the person posting the
220
+ # comment.
221
+ # @option opts [#to_s] :permalink The permalink of the blog post to which
222
+ # the comment is being posted.
223
+ # @option opts [#to_s] :referrer The URL of the site that brought commenter
224
+ # to this page.
225
+ # @option opts [Boolean] :user_logged_in Whether or not the user posting
226
+ # the comment is logged-into the blogging platform
227
+ # @option opts [Boolean] :trusted_user Whether or not the user is an
228
+ # administrator, moderator or editor of this blog; the client should pass
229
+ # true only if blogging platform can guarantee that the user has been
230
+ # authenticated and has a role of responsibility on this blog.
231
+ # @option opts [#to_s] :openid The OpenID URL of the currently logged in
232
+ # user. Must be used in conjunction with :user_logged_in => true. OpenID
233
+ # authentication must be taken care of by your application.
234
+ # @option opts [#to_s] :test_force For testing purposes only: Use this
235
+ # parameter to force the outcome of audit_comment. Optionally affix (with
236
+ # a comma) a desired spaminess return value (in the range 0 to 1).
237
+ # Example: "spam,0.5000" or "ham,0.0010".
238
+ # @raise [StandardError] If the call fails, a StandardError is raised with
239
+ # the error message given from Defensio.
240
+ # @return [Defender::CommentResponse]
241
+ # @see http://defensio.com/api/#audit-comment
242
+ def audit_comment(opts={})
243
+ response = call_action("audit-comment", Defender.options_to_parameters(opts))
244
+ return CommentResponse.new(response)
245
+ end
246
+
247
+ ##
248
+ # This action is used to retrain false negatives. False negatives are
249
+ # comments that were originally tagged as "ham" (i.e. legitimate) but were
250
+ # in fact spam.
251
+ #
252
+ # @param [Array<#to_s, CommentResponse>] signatures List of signatures (may
253
+ # contain a single entry) of the comments to be submitted for retraining.
254
+ # Note that a signature for each comment was originally provided by the
255
+ # {#audit_comment} method.
256
+ # @raise [StandardError] If the call fails, a StandardError is raised with
257
+ # the error message given from Defensio.
258
+ # @return [Boolean] Returns true if the comments were successfully marked,
259
+ # raises StandardError otherwise.
260
+ def report_false_negatives(signatures)
261
+ response = call_action("report-false-negatives",
262
+ "signatures" => signatures.map(&:to_s).join(","))
263
+ true
264
+ end
265
+
266
+ ##
267
+ # This action is used to retrain false negatives. False negatives are
268
+ # comments that were originally tagged as spam but were in fact "ham" (i.e.
269
+ # legitimate).
270
+ #
271
+ # @param [Array<#to_s, CommentResponse>] signatures List of signatures (may
272
+ # contain a single entry) of the comments to be submitted for retraining.
273
+ # Note that a signature for each comment was originally provided by the
274
+ # {#audit_comment} method.
275
+ # @raise [StandardError] If the call fails, a StandardError is raised with
276
+ # the error message given from Defensio.
277
+ # @return [Boolean] Returns true if the comments were successfully marked,
278
+ # raises StandardError otherwise.
279
+ def report_false_positives(signatures)
280
+ response = call_action("report-false-positives",
281
+ "signatures" => signatures.map(&:to_s).join(","))
282
+ true
283
+ end
284
+
285
+ ##
286
+ # This action returns basic statistics regarding the performance of Defensio
287
+ # since activation.
288
+ #
289
+ # @return [Defender::Statistics]
290
+ def statistics
291
+ response = call_action("get-stats")
292
+ return Statistics.new(response)
293
+ end
294
+
295
+ private
296
+ ##
297
+ # Returns the url for the given action.
298
+ #
299
+ # @param [#to_s] action The action to generate the URL for.
300
+ # @return [String] The URL for the action.
301
+ # @raise [APIKeyError] Raises this if no API key is given.
302
+ def url(action)
303
+ raise APIKeyError unless @api_key.length > 0
304
+ "http://api.defensio.com/" \
305
+ "#{@service_type}/" \
306
+ "#{Defender::API_VERSION}/" \
307
+ "#{action}/" \
308
+ "#{@api_key}.yaml"
309
+ end
310
+
311
+ ##
312
+ # Backend function for calling an action.
313
+ #
314
+ # @param [#to_s] action The action to call.
315
+ # @param [Hash] params The parameters for the action.
316
+ # @return [Hash] The raw response, only parsed from YAML.
317
+ # @raise [APIKeyError] If an invalid (or no) API key is given, this is
318
+ # raised
319
+ def call_action(action, params={})
320
+ params = {"owner-url" => @owner_url}.merge(params)
321
+ response = Net::HTTP.post_form(URI.parse(url(action)), params)
322
+ if response.code == 400
323
+ raise APIKeyError
324
+ else
325
+ Defender.raise_if_error(YAML.load(response.body)["defensio-result"])
326
+ end
327
+ end
328
+ end
@@ -0,0 +1,79 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
2
+
3
+ describe "Defender" do
4
+ it "should raise a StandardError if a method fails" do
5
+ lambda do
6
+ Defender.raise_if_error({
7
+ "status" => "fail",
8
+ "message" => "Failed!"
9
+ })
10
+ end.should raise_error(StandardError, "Failed!")
11
+ end
12
+
13
+ it "should return the correct URL for any given action" do
14
+ d = Defender.new(:api_key => "key1234")
15
+ d.instance_eval do
16
+ url("foobar")
17
+ end.should == "http://api.defensio.com/blog/#{Defender::API_VERSION}/foobar/key1234.yaml"
18
+ end
19
+
20
+ it "should correctly identify a valid API key" do
21
+ d = Defender.new(:api_key => ENV["API_KEY"], :owner_url => ENV["API_OWNER_URL"])
22
+ d.valid_key?.should be_true
23
+ end
24
+
25
+ it "should correctly identify an invalid API key" do
26
+ d = Defender.new(:api_key => "key1234", :owner_url => ENV["API_OWNER_URL"])
27
+ d.valid_key?.should be_false
28
+ end
29
+
30
+ it "should correctly identify a spammy comment" do
31
+ d = Defender.new(:api_key => ENV["API_KEY"], :owner_url => ENV["API_OWNER_URL"])
32
+
33
+ d.audit_comment(
34
+ :user_ip => "127.0.0.1",
35
+ :article_date => Time.now,
36
+ :comment_author => "Henrik Hodne",
37
+ :comment_type => "comment",
38
+ :test_force => "spam,0.5000"
39
+ ).spam?.should be_true
40
+ end
41
+
42
+ it "should correctly identify a meaty comment" do
43
+ d = Defender.new(:api_key => ENV["API_KEY"], :owner_url => ENV["API_OWNER_URL"])
44
+
45
+ d.audit_comment(
46
+ :user_ip => "127.0.0.1",
47
+ :article_date => Time.now,
48
+ :comment_author => "Henrik Hodne",
49
+ :comment_type => "comment",
50
+ :test_force => "ham,0.1000"
51
+ ).spam?.should be_false
52
+ end
53
+
54
+ it "should correctly set the spaminess" do
55
+ d = Defender.new(:api_key => ENV["API_KEY"], :owner_url => ENV["API_OWNER_URL"])
56
+
57
+ d.audit_comment(
58
+ :user_ip => "127.0.0.1",
59
+ :article_date => Time.now,
60
+ :comment_author => "Henrik Hodne",
61
+ :comment_type => "comment",
62
+ :test_force => "spam,0.5000"
63
+ ).spaminess.should == 0.5
64
+ end
65
+
66
+ it "should fail without valid API credentials" do
67
+ d = Defender.new(:api_key => "key1234", :owner_url => "http://google.com")
68
+
69
+ lambda {
70
+ d.audit_comment(
71
+ :user_ip => "127.0.0.1",
72
+ :article_date => Time.now,
73
+ :comment_author => "Henrik Hodne",
74
+ :comment_type => "comment",
75
+ :test_force => "ham,0.1000"
76
+ )
77
+ }.should raise_error(StandardError, "Authentication failed. Please verify your key/owner-url combination.")
78
+ end
79
+ end
data/spec/spec.opts ADDED
@@ -0,0 +1 @@
1
+ --color
@@ -0,0 +1,9 @@
1
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
2
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
3
+ require 'defender'
4
+ require 'spec'
5
+ require 'spec/autorun'
6
+
7
+ Spec::Runner.configure do |config|
8
+
9
+ end
metadata ADDED
@@ -0,0 +1,85 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: defender
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Henrik Hodne
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-11-08 00:00:00 +01:00
13
+ default_executable:
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: rspec
17
+ type: :development
18
+ version_requirement:
19
+ version_requirements: !ruby/object:Gem::Requirement
20
+ requirements:
21
+ - - ">="
22
+ - !ruby/object:Gem::Version
23
+ version: 1.2.9
24
+ version:
25
+ - !ruby/object:Gem::Dependency
26
+ name: yard
27
+ type: :development
28
+ version_requirement:
29
+ version_requirements: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: "0"
34
+ version:
35
+ description: A wrapper of the Defensio spam filtering service.
36
+ email: henrik.hodne@binaryhex.com
37
+ executables: []
38
+
39
+ extensions: []
40
+
41
+ extra_rdoc_files:
42
+ - LICENSE
43
+ - README.rdoc
44
+ files:
45
+ - .document
46
+ - .gitignore
47
+ - LICENSE
48
+ - README.rdoc
49
+ - Rakefile
50
+ - VERSION
51
+ - lib/defender.rb
52
+ - spec/defender_spec.rb
53
+ - spec/spec.opts
54
+ - spec/spec_helper.rb
55
+ has_rdoc: true
56
+ homepage: http://github.com/dvyjones/defender
57
+ licenses: []
58
+
59
+ post_install_message:
60
+ rdoc_options:
61
+ - --charset=UTF-8
62
+ require_paths:
63
+ - lib
64
+ required_ruby_version: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: "0"
69
+ version:
70
+ required_rubygems_version: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - ">="
73
+ - !ruby/object:Gem::Version
74
+ version: "0"
75
+ version:
76
+ requirements: []
77
+
78
+ rubyforge_project:
79
+ rubygems_version: 1.3.5
80
+ signing_key:
81
+ specification_version: 3
82
+ summary: Ruby API wrapper for Defensio
83
+ test_files:
84
+ - spec/spec_helper.rb
85
+ - spec/defender_spec.rb