has_face 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +4 -0
- data/.rvmrc +1 -0
- data/Gemfile +3 -0
- data/README.md +133 -0
- data/Rakefile +1 -0
- data/has_face.gemspec +28 -0
- data/lib/generators/has_face/install_generator.rb +15 -0
- data/lib/generators/has_face/templates/has_face.rb +5 -0
- data/lib/has_face/configuration.rb +17 -0
- data/lib/has_face/locales/en.yml +5 -0
- data/lib/has_face/test/matchers.rb +56 -0
- data/lib/has_face/validator.rb +74 -0
- data/lib/has_face/version.rb +3 -0
- data/lib/has_face.rb +30 -0
- data/spec/has_face/test/matchers_spec.rb +39 -0
- data/spec/has_face/validator_spec.rb +211 -0
- data/spec/spec_helper.rb +32 -0
- data/spec/support/assets/hit.jpg +0 -0
- data/spec/support/assets/miss.jpg +0 -0
- data/spec/support/models/avatar.rb +5 -0
- data/spec/support/models/base_user.rb +7 -0
- data/spec/support/models/user.rb +9 -0
- data/spec/support/models/user_with_allow_blank.rb +3 -0
- data/spec/support/models/user_with_allow_nil.rb +3 -0
- data/spec/support/schema_setup.rb +17 -0
- data/spec/support/vcr_cassettes/invalid_api_key.yml +38 -0
- data/spec/support/vcr_cassettes/invalid_detect_url.yml +41 -0
- data/spec/support/vcr_cassettes/invalid_image.yml +42 -0
- data/spec/support/vcr_cassettes/valid_image.yml +49 -0
- metadata +198 -0
data/.gitignore
ADDED
data/.rvmrc
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
rvm use @has_face --create
|
data/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,133 @@
|
|
1
|
+
# Has face
|
2
|
+
An Active model validator to validate that images contain faces by
|
3
|
+
using the face.com API. The validator works by uploading images to
|
4
|
+
face.com and then checking to see if any faces were tagged in the photo.
|
5
|
+
It has been tested with carrierwave attachments but will work with any
|
6
|
+
attachment type that correctly responds to a `path` method.
|
7
|
+
|
8
|
+
## Requirements
|
9
|
+
- Rails 3.0 or greater
|
10
|
+
- An account for accessing the face.com API (They are free at the moment)
|
11
|
+
|
12
|
+
## Installation
|
13
|
+
Add has_face to your Gemfile and then bundle
|
14
|
+
|
15
|
+
``` ruby
|
16
|
+
gem 'has_face'
|
17
|
+
```
|
18
|
+
|
19
|
+
Once installed run the generator to create an initializer
|
20
|
+
|
21
|
+
``` ruby
|
22
|
+
rails g has_face:install
|
23
|
+
```
|
24
|
+
|
25
|
+
Then open up `config/initializers/has_face.rb` and enter your face.com
|
26
|
+
API details.
|
27
|
+
|
28
|
+
``` ruby
|
29
|
+
# config/initializers/has_face.rb
|
30
|
+
HasFace.configure do |config|
|
31
|
+
config.api_key = 'your face.com API key'
|
32
|
+
config.api_secret = 'your face.com API secret'
|
33
|
+
config.skip_validation_on_error = false
|
34
|
+
end
|
35
|
+
```
|
36
|
+
|
37
|
+
## Usage
|
38
|
+
|
39
|
+
Simply add a validation to the image you want to ensure has faces:
|
40
|
+
|
41
|
+
``` ruby
|
42
|
+
class User < ActiveRecord::Base
|
43
|
+
validates :avatar, :has_face => true
|
44
|
+
end
|
45
|
+
```
|
46
|
+
|
47
|
+
The `allow_nil` and `allow_blank` options are supported:
|
48
|
+
|
49
|
+
``` ruby
|
50
|
+
class User < ActiveRecord::Base
|
51
|
+
validates :avatar, :has_face => true, :allow_blank => true
|
52
|
+
end
|
53
|
+
```
|
54
|
+
|
55
|
+
## i18n
|
56
|
+
|
57
|
+
Error messages generated are i18n safe. To alter the error message shown
|
58
|
+
add this to your `config/locale/en.yml`
|
59
|
+
|
60
|
+
``` ruby
|
61
|
+
en:
|
62
|
+
activerecord:
|
63
|
+
errors:
|
64
|
+
messages:
|
65
|
+
no_face: "We couldn't see a face in your photo, try taking another one."
|
66
|
+
```
|
67
|
+
|
68
|
+
|
69
|
+
## Error Handling
|
70
|
+
|
71
|
+
By default has_face will raise either a `HasFace::FaceAPIError` or
|
72
|
+
`HasFace::HTTPRequestError` on failure. You will need to catch these
|
73
|
+
errors and then take the appropriate action in your application like so:
|
74
|
+
|
75
|
+
``` ruby
|
76
|
+
begin
|
77
|
+
@user = User.create(params[:user])
|
78
|
+
rescue HasFace::FaceAPIError, HasFace::HTTPRequestError => e
|
79
|
+
# Perform some sort of action.
|
80
|
+
end
|
81
|
+
```
|
82
|
+
|
83
|
+
If you would like to skip valdiation when a HTTP or API error occurs
|
84
|
+
then simply turn on the `skip_validation_on_error` configuration option:
|
85
|
+
|
86
|
+
``` ruby
|
87
|
+
HasFace.configure do |config|
|
88
|
+
config.skip_validation_on_error = true
|
89
|
+
end
|
90
|
+
```
|
91
|
+
|
92
|
+
If an error does occur then it will be logged as a warning in the log
|
93
|
+
for your applications current environment.
|
94
|
+
|
95
|
+
## Testing has_face
|
96
|
+
|
97
|
+
To speed up your test suite, you can disable face validations by setting the
|
98
|
+
`enable_validation` config value to false, this is usally best done in
|
99
|
+
your test config.
|
100
|
+
|
101
|
+
``` ruby
|
102
|
+
HasFace.enable_validation = false
|
103
|
+
```
|
104
|
+
|
105
|
+
Has Face supplies a matcher which you can use in your tests, to
|
106
|
+
enable it, include the matchers module in your rspec config.
|
107
|
+
|
108
|
+
``` ruby
|
109
|
+
RSpec.configure do |config|
|
110
|
+
config.include HasFace::Test::Matchers
|
111
|
+
end
|
112
|
+
```
|
113
|
+
|
114
|
+
|
115
|
+
Once included he matcher can be used:
|
116
|
+
|
117
|
+
``` ruby
|
118
|
+
context 'validations' do
|
119
|
+
it { should validate_has_face_for :avatar }
|
120
|
+
end
|
121
|
+
```
|
122
|
+
|
123
|
+
The options `allow_blank` and `allow_nil` can also be passed to the matcher:
|
124
|
+
|
125
|
+
``` ruby
|
126
|
+
it { should validate_has_face_for :avatar, :allow_blank => true }
|
127
|
+
```
|
128
|
+
|
129
|
+
### Contributing
|
130
|
+
|
131
|
+
Fork on GitHub and after you’ve committed tested patches, send a pull request.
|
132
|
+
|
133
|
+
To get tests running simply run `bundle install` and then `rspec spec`
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require 'bundler/gem_tasks'
|
data/has_face.gemspec
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "has_face/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "has_face"
|
7
|
+
s.version = HasFace::VERSION
|
8
|
+
s.authors = ["Mario Visic"]
|
9
|
+
s.email = ["mario@mariovisic.com"]
|
10
|
+
s.homepage = "https://github.com/mariovisic/has_face"
|
11
|
+
s.summary = "Easily validate if an image contains faces"
|
12
|
+
s.description = "An Active Model validator that uses the face.com API to ensures an image contains a face"
|
13
|
+
|
14
|
+
s.rubyforge_project = "has_face"
|
15
|
+
|
16
|
+
s.add_dependency 'rails', '>= 3.0'
|
17
|
+
s.add_dependency 'rest-client'
|
18
|
+
|
19
|
+
s.add_development_dependency 'rspec', '>= 2.0'
|
20
|
+
s.add_development_dependency 'rr', '>= 1.0'
|
21
|
+
s.add_development_dependency 'vcr', '>= 1.0'
|
22
|
+
s.add_development_dependency 'fakeweb', '>= 1.0'
|
23
|
+
|
24
|
+
s.files = `git ls-files`.split("\n")
|
25
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
26
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
27
|
+
s.require_paths = ["lib"]
|
28
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module HasFace
|
2
|
+
module Generators
|
3
|
+
class InstallGenerator < Rails::Generators::Base
|
4
|
+
desc "Copy HasFace default files"
|
5
|
+
source_root File.expand_path('../templates', __FILE__)
|
6
|
+
class_option :template_engine
|
7
|
+
|
8
|
+
def copy_initializers
|
9
|
+
copy_file 'has_face.rb', 'config/initializers/has_face.rb'
|
10
|
+
end
|
11
|
+
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module HasFace
|
2
|
+
class Configuration
|
3
|
+
|
4
|
+
@enable_validation = true
|
5
|
+
@skip_validation_on_error = false
|
6
|
+
@detect_url = "http://api.face.com/faces/detect.json"
|
7
|
+
|
8
|
+
class << self
|
9
|
+
attr_accessor :enable_validation
|
10
|
+
attr_accessor :detect_url
|
11
|
+
attr_accessor :api_key
|
12
|
+
attr_accessor :api_secret
|
13
|
+
attr_accessor :skip_validation_on_error
|
14
|
+
end
|
15
|
+
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
module HasFace
|
2
|
+
module Test
|
3
|
+
module Matchers
|
4
|
+
|
5
|
+
class ValidateHasFaceFor
|
6
|
+
|
7
|
+
def initialize(expected, options)
|
8
|
+
@expected = expected
|
9
|
+
@options = options
|
10
|
+
end
|
11
|
+
|
12
|
+
def matches?(actual)
|
13
|
+
@actual = actual
|
14
|
+
|
15
|
+
@actual.class.validators.any? do |validator|
|
16
|
+
validator.is_a?(HasFace::Validator) && \
|
17
|
+
validator.attributes.include?(@expected) && \
|
18
|
+
has_correct_allow_blank(validator) && \
|
19
|
+
has_correct_allow_nil(validator)
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
23
|
+
|
24
|
+
def failure_message
|
25
|
+
"expected #{@actual.class.to_s} to validate has face for #{@expected.inspect} #{with_options_message}"
|
26
|
+
end
|
27
|
+
|
28
|
+
def negative_failure_message
|
29
|
+
"expected #{@actual.class.to_s} not to validate has face for #{@expected.inspect} #{with_options_message}"
|
30
|
+
end
|
31
|
+
|
32
|
+
def has_correct_allow_blank(validator)
|
33
|
+
return true if @options[:allow_blank].nil?
|
34
|
+
validator.instance_variable_get('@allow_blank') == @options[:allow_blank]
|
35
|
+
end
|
36
|
+
|
37
|
+
def has_correct_allow_nil(validator)
|
38
|
+
return true if @options[:allow_nil].nil?
|
39
|
+
validator.instance_variable_get('@allow_nil') == @options[:allow_nil]
|
40
|
+
end
|
41
|
+
|
42
|
+
def with_options_message
|
43
|
+
if @options.present?
|
44
|
+
"with options #{@options.inspect}"
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|
49
|
+
|
50
|
+
def validate_has_face_for(expected, options = {})
|
51
|
+
ValidateHasFaceFor.new(expected, options)
|
52
|
+
end
|
53
|
+
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
require 'rest_client'
|
2
|
+
|
3
|
+
module HasFace
|
4
|
+
class Validator < ActiveModel::EachValidator
|
5
|
+
|
6
|
+
def initialize(options)
|
7
|
+
@allow_nil, @allow_blank = options.delete(:allow_nil), options.delete(:allow_blank)
|
8
|
+
super
|
9
|
+
end
|
10
|
+
|
11
|
+
def validate_each(record, attr_name, value)
|
12
|
+
|
13
|
+
# Skip validation if globally turned off
|
14
|
+
return if HasFace.enable_validation == false
|
15
|
+
|
16
|
+
image = record.send(attr_name)
|
17
|
+
image_path = image.try(:path) if image.respond_to?(:path)
|
18
|
+
|
19
|
+
# Skip validation if our image is nil/blank and allow nil/blank is on
|
20
|
+
return if (@allow_nil && image.nil?) || (@allow_blank && image.blank?)
|
21
|
+
|
22
|
+
# Add an error if the url is blank
|
23
|
+
return record.errors.add(attr_name, :no_face) if image_path.blank?
|
24
|
+
|
25
|
+
# Get an parse the JSON response
|
26
|
+
params = { :api_key => HasFace.api_key, :api_secret => HasFace.api_secret, :image => File.new(image_path, 'rb') }
|
27
|
+
|
28
|
+
begin
|
29
|
+
response = RestClient.post(HasFace.detect_url, params)
|
30
|
+
rescue RestClient::Exception => error
|
31
|
+
return handle_request_error(error)
|
32
|
+
end
|
33
|
+
|
34
|
+
json_response = JSON.parse(response.body)
|
35
|
+
|
36
|
+
# Error handling for failed responses
|
37
|
+
return handle_api_error(json_response) unless json_response['status'] == 'success'
|
38
|
+
|
39
|
+
|
40
|
+
# Add errors if no tags are present
|
41
|
+
tags = json_response.try(:[], 'photos').try(:first).try(:[], 'tags') || []
|
42
|
+
record.errors.add(attr_name, :no_face) if tags.blank?
|
43
|
+
|
44
|
+
end
|
45
|
+
|
46
|
+
protected
|
47
|
+
|
48
|
+
def handle_api_error(response)
|
49
|
+
error_message = "face.com API Error: \"#{response['error_message']}\" Code: #{response['error_code']}"
|
50
|
+
|
51
|
+
if HasFace.skip_validation_on_error
|
52
|
+
Rails.logger.warn error_message if Rails.logger.present?
|
53
|
+
true
|
54
|
+
else
|
55
|
+
raise FaceAPIError.new error_message
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def handle_request_error(error)
|
60
|
+
error_message = "has_face HTTP Request Error: \"#{error.message}\""
|
61
|
+
|
62
|
+
if HasFace.skip_validation_on_error
|
63
|
+
Rails.logger.warn error_message if Rails.logger.present?
|
64
|
+
true
|
65
|
+
else
|
66
|
+
raise HTTPRequestError.new error_message
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
# Compatibility with ActiveModel validates method which matches option keys to their validator class
|
74
|
+
ActiveModel::Validations::HasFaceValidator = HasFace::Validator
|
data/lib/has_face.rb
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
require 'has_face/configuration'
|
2
|
+
require 'has_face/validator'
|
3
|
+
require 'has_face/version'
|
4
|
+
require 'has_face/test/matchers'
|
5
|
+
|
6
|
+
module HasFace
|
7
|
+
|
8
|
+
# Error classes
|
9
|
+
class FaceAPIError < StandardError; end
|
10
|
+
class HTTPRequestError < StandardError; end
|
11
|
+
|
12
|
+
# Add load paths straight to I18n, so engines and application can overwrite it.
|
13
|
+
require 'i18n'
|
14
|
+
I18n.load_path << File.expand_path('../has_face/locales/en.yml', __FILE__)
|
15
|
+
|
16
|
+
class << self
|
17
|
+
|
18
|
+
configs = [ :api_key, :api_secret, :enable_validation, :detect_url, :skip_validation_on_error ]
|
19
|
+
|
20
|
+
configs.each do |config|
|
21
|
+
delegate config, "#{config}=", :to => HasFace::Configuration
|
22
|
+
end
|
23
|
+
|
24
|
+
def configure
|
25
|
+
yield self
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe HasFace::Test::Matchers do
|
4
|
+
|
5
|
+
context 'a user without face valdiation' do
|
6
|
+
|
7
|
+
subject { UserWithoutFaceValidation.new }
|
8
|
+
|
9
|
+
it { should_not validate_has_face_for :avatar }
|
10
|
+
|
11
|
+
end
|
12
|
+
|
13
|
+
context 'a user with face validation' do
|
14
|
+
|
15
|
+
subject { UserWithFaceValidation.new }
|
16
|
+
|
17
|
+
it { should validate_has_face_for :avatar }
|
18
|
+
|
19
|
+
end
|
20
|
+
|
21
|
+
context 'a user with face validation and allow blank' do
|
22
|
+
|
23
|
+
subject { UserWithAllowBlank.new }
|
24
|
+
|
25
|
+
it { should validate_has_face_for :avatar, :allow_blank => true }
|
26
|
+
it { should_not validate_has_face_for :avatar, :allow_blank => false }
|
27
|
+
|
28
|
+
end
|
29
|
+
|
30
|
+
context 'a user with face validation and allow nil' do
|
31
|
+
|
32
|
+
subject { UserWithAllowNil.new }
|
33
|
+
|
34
|
+
it { should validate_has_face_for :avatar, :allow_nil => true }
|
35
|
+
it { should_not validate_has_face_for :avatar, :allow_nil => false }
|
36
|
+
|
37
|
+
end
|
38
|
+
|
39
|
+
end
|
@@ -0,0 +1,211 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe HasFace::Validator do
|
4
|
+
|
5
|
+
let(:user) { User.new(:avatar => avatar) }
|
6
|
+
let(:avatar) { Avatar.new }
|
7
|
+
|
8
|
+
context 'when validation is globally turned on' do
|
9
|
+
|
10
|
+
context 'when the image is a valid face' do
|
11
|
+
|
12
|
+
before :each do
|
13
|
+
avatar.path = VALID_IMAGE_PATH
|
14
|
+
end
|
15
|
+
|
16
|
+
it 'should be valid' do
|
17
|
+
VCR.use_cassette('valid image') do
|
18
|
+
user.should be_valid
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
23
|
+
|
24
|
+
context 'when the image is not a valid face' do
|
25
|
+
|
26
|
+
before :each do
|
27
|
+
avatar.path = INVALID_IMAGE_PATH
|
28
|
+
end
|
29
|
+
|
30
|
+
it 'should not be valid' do
|
31
|
+
VCR.use_cassette('invalid image') do
|
32
|
+
user.should_not be_valid
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
it 'should have an error on the image field' do
|
37
|
+
VCR.use_cassette('invalid image') do
|
38
|
+
user.valid?
|
39
|
+
user.errors[:avatar].should == [ "We couldn't see a face in your photo, try taking another one." ]
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
46
|
+
|
47
|
+
context 'when face validation is globally turned off' do
|
48
|
+
|
49
|
+
before :each do
|
50
|
+
stub(HasFace).enable_validation { false }
|
51
|
+
avatar.path = INVALID_IMAGE_PATH
|
52
|
+
end
|
53
|
+
|
54
|
+
it 'should be valid' do
|
55
|
+
user.should be_valid
|
56
|
+
end
|
57
|
+
|
58
|
+
end
|
59
|
+
|
60
|
+
context 'handling a failure response from the face.com API' do
|
61
|
+
|
62
|
+
let(:logger) { Logger.new(nil) }
|
63
|
+
|
64
|
+
before :each do
|
65
|
+
stub(HasFace).api_key { 'invalid api key' }
|
66
|
+
stub(Rails).logger { logger }
|
67
|
+
avatar.path = INVALID_IMAGE_PATH
|
68
|
+
end
|
69
|
+
|
70
|
+
context 'when skipping validation on errors is enabled' do
|
71
|
+
|
72
|
+
before :each do
|
73
|
+
stub(HasFace).skip_validation_on_error { true }
|
74
|
+
avatar.path = INVALID_IMAGE_PATH
|
75
|
+
end
|
76
|
+
|
77
|
+
it 'should skip validation' do
|
78
|
+
VCR.use_cassette('invalid api key') do
|
79
|
+
user.should be_valid
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
it 'should log a warning' do
|
84
|
+
VCR.use_cassette('invalid api key') do
|
85
|
+
mock(logger).warn 'face.com API Error: "API_KEY_DOES_NOT_EXIST - invalid api key" Code: 201'
|
86
|
+
user.valid?
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
end
|
91
|
+
|
92
|
+
context 'when skipping validation on errors is disabled' do
|
93
|
+
|
94
|
+
it 'should raise an api error' do
|
95
|
+
VCR.use_cassette('invalid api key') do
|
96
|
+
expect { user.valid? }.to raise_error HasFace::FaceAPIError, 'face.com API Error: "API_KEY_DOES_NOT_EXIST - invalid api key" Code: 201'
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
end
|
101
|
+
|
102
|
+
end
|
103
|
+
|
104
|
+
context 'handling http request errors' do
|
105
|
+
|
106
|
+
let(:logger) { Logger.new(nil) }
|
107
|
+
|
108
|
+
before :each do
|
109
|
+
stub(HasFace).detect_url { 'http://face.com/invalid/lookup/url' }
|
110
|
+
stub(Rails).logger { logger }
|
111
|
+
avatar.path = INVALID_IMAGE_PATH
|
112
|
+
end
|
113
|
+
|
114
|
+
context 'when skipping validation on errors is enabled' do
|
115
|
+
|
116
|
+
before :each do
|
117
|
+
stub(HasFace).skip_validation_on_error { true }
|
118
|
+
end
|
119
|
+
|
120
|
+
it 'should skip validation' do
|
121
|
+
VCR.use_cassette('invalid detect url') do
|
122
|
+
user.should be_valid
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
it 'should log a warning' do
|
127
|
+
VCR.use_cassette('invalid detect url') do
|
128
|
+
mock(logger).warn 'has_face HTTP Request Error: "404 Resource Not Found"'
|
129
|
+
user.valid?
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
end
|
134
|
+
|
135
|
+
context 'when skipping validation on errors is disabled' do
|
136
|
+
|
137
|
+
it 'should raise an api error' do
|
138
|
+
VCR.use_cassette('invalid detect url') do
|
139
|
+
expect { user.valid? }.to raise_error HasFace::HTTPRequestError, 'has_face HTTP Request Error: "404 Resource Not Found"'
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
end
|
144
|
+
|
145
|
+
end
|
146
|
+
|
147
|
+
context 'allowing blank' do
|
148
|
+
|
149
|
+
context 'when allow blank is true' do
|
150
|
+
|
151
|
+
let(:user) { UserWithAllowBlank.new }
|
152
|
+
|
153
|
+
before :each do
|
154
|
+
stub(user).avatar { "" }
|
155
|
+
end
|
156
|
+
|
157
|
+
it 'should be valid with a blank avatar' do
|
158
|
+
user.should be_valid
|
159
|
+
end
|
160
|
+
|
161
|
+
end
|
162
|
+
|
163
|
+
context 'when allow blank is not true' do
|
164
|
+
|
165
|
+
let(:user) { UserWithoutAllowBlank.new }
|
166
|
+
|
167
|
+
before :each do
|
168
|
+
stub(user).avatar { "" }
|
169
|
+
end
|
170
|
+
|
171
|
+
it 'should not be valid' do
|
172
|
+
user.should_not be_valid
|
173
|
+
end
|
174
|
+
|
175
|
+
end
|
176
|
+
|
177
|
+
end
|
178
|
+
|
179
|
+
context 'allowing nil' do
|
180
|
+
|
181
|
+
context 'when allow nil is true' do
|
182
|
+
|
183
|
+
let(:user) { UserWithAllowNil.new }
|
184
|
+
|
185
|
+
before :each do
|
186
|
+
stub(user).avatar { nil }
|
187
|
+
end
|
188
|
+
|
189
|
+
it 'should be valid with a nil avatar' do
|
190
|
+
user.should be_valid
|
191
|
+
end
|
192
|
+
|
193
|
+
end
|
194
|
+
|
195
|
+
context 'when allow nil is not true' do
|
196
|
+
|
197
|
+
let(:user) { UserWithoutAllowNil.new }
|
198
|
+
|
199
|
+
before :each do
|
200
|
+
stub(user).avatar { nil }
|
201
|
+
end
|
202
|
+
|
203
|
+
it 'should not be valid' do
|
204
|
+
user.should_not be_valid
|
205
|
+
end
|
206
|
+
|
207
|
+
end
|
208
|
+
|
209
|
+
end
|
210
|
+
|
211
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
require 'rspec'
|
2
|
+
require 'vcr'
|
3
|
+
|
4
|
+
require 'rails'
|
5
|
+
require 'active_model'
|
6
|
+
require 'active_model/validations'
|
7
|
+
require 'active_record'
|
8
|
+
require 'lib/has_face'
|
9
|
+
|
10
|
+
Dir["spec/support/**/*.rb"].each {|f| require f}
|
11
|
+
|
12
|
+
VALID_IMAGE_PATH = 'spec/support/assets/hit.jpg'
|
13
|
+
INVALID_IMAGE_PATH = 'spec/support/assets/miss.jpg'
|
14
|
+
|
15
|
+
VCR.config do |c|
|
16
|
+
c.cassette_library_dir = 'spec/support/vcr_cassettes'
|
17
|
+
c.stub_with :fakeweb
|
18
|
+
c.default_cassette_options = { :record => :once }
|
19
|
+
end
|
20
|
+
|
21
|
+
RSpec.configure do |config|
|
22
|
+
|
23
|
+
config.mock_with :rr
|
24
|
+
config.include HasFace::Test::Matchers
|
25
|
+
|
26
|
+
config.before :all do
|
27
|
+
# Put your api details here for tesing. This is only required if VCR is not being used
|
28
|
+
# HasFace.api_key = 'api_key' if HasFace.api_key.blank?
|
29
|
+
# HasFace.api_secret = 'api_secret' if HasFace.api_secret.blank?
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
Binary file
|
Binary file
|
@@ -0,0 +1,17 @@
|
|
1
|
+
ActiveRecord::Base.establish_connection({:adapter => 'sqlite3', :database => ':memory:'})
|
2
|
+
ActiveRecord::Migration.verbose = false
|
3
|
+
|
4
|
+
ActiveRecord::Schema.define(:version => 1) do
|
5
|
+
|
6
|
+
create_table :avatars, :force => true do |t|
|
7
|
+
t.string :url
|
8
|
+
t.string :path
|
9
|
+
t.integer :user_id
|
10
|
+
end
|
11
|
+
|
12
|
+
create_table :base_users, :force => true do |t|
|
13
|
+
t.string :full_name
|
14
|
+
end
|
15
|
+
|
16
|
+
end
|
17
|
+
|
@@ -0,0 +1,38 @@
|
|
1
|
+
---
|
2
|
+
- !ruby/struct:VCR::HTTPInteraction
|
3
|
+
request: !ruby/struct:VCR::Request
|
4
|
+
method: :post
|
5
|
+
uri: http://api.face.com:80/faces/detect.json
|
6
|
+
body:
|
7
|
+
headers:
|
8
|
+
accept:
|
9
|
+
- "*/*; q=0.5, application/xml"
|
10
|
+
content-type:
|
11
|
+
- multipart/form-data; boundary=666980
|
12
|
+
accept-encoding:
|
13
|
+
- gzip, deflate
|
14
|
+
content-length:
|
15
|
+
- "69159"
|
16
|
+
response: !ruby/struct:VCR::Response
|
17
|
+
status: !ruby/struct:VCR::ResponseStatus
|
18
|
+
code: 200
|
19
|
+
message: OK
|
20
|
+
headers:
|
21
|
+
content-type:
|
22
|
+
- "application/json; Charset: utf-8"
|
23
|
+
server:
|
24
|
+
- Apache/2.2.16 (Debian) mod_ssl/2.2.16 OpenSSL/0.9.8o
|
25
|
+
date:
|
26
|
+
- Sun, 10 Jul 2011 14:43:22 GMT
|
27
|
+
content-length:
|
28
|
+
- "115"
|
29
|
+
content-encoding:
|
30
|
+
- gzip
|
31
|
+
vary:
|
32
|
+
- Accept-Encoding
|
33
|
+
body: !binary |
|
34
|
+
H4sIAAAAAAAAA6pWKi5JLCktVrJSSkvMzCktSlXSUUotKsovik/OT0lVsjIy
|
35
|
+
MIQJ5KYWFyemA8WUHAM8471dI+Nd/F2D4/38Q+JdIzyDQxR0FTLzyhJzMlMU
|
36
|
+
EgsyFbJTK5VqAQAAAP//AwApem9UYAAAAA==
|
37
|
+
|
38
|
+
http_version: "1.1"
|
@@ -0,0 +1,41 @@
|
|
1
|
+
---
|
2
|
+
- !ruby/struct:VCR::HTTPInteraction
|
3
|
+
request: !ruby/struct:VCR::Request
|
4
|
+
method: :post
|
5
|
+
uri: http://face.com:80/invalid/lookup/url
|
6
|
+
body:
|
7
|
+
headers:
|
8
|
+
accept:
|
9
|
+
- "*/*; q=0.5, application/xml"
|
10
|
+
content-type:
|
11
|
+
- multipart/form-data; boundary=104496
|
12
|
+
accept-encoding:
|
13
|
+
- gzip, deflate
|
14
|
+
content-length:
|
15
|
+
- "69176"
|
16
|
+
response: !ruby/struct:VCR::Response
|
17
|
+
status: !ruby/struct:VCR::ResponseStatus
|
18
|
+
code: 404
|
19
|
+
message: Not Found
|
20
|
+
headers:
|
21
|
+
content-type:
|
22
|
+
- text/html; charset=iso-8859-1
|
23
|
+
server:
|
24
|
+
- Apache/2.2.16 (Debian) mod_ssl/2.2.16 OpenSSL/0.9.8o
|
25
|
+
date:
|
26
|
+
- Sun, 10 Jul 2011 14:43:50 GMT
|
27
|
+
content-length:
|
28
|
+
- "261"
|
29
|
+
content-encoding:
|
30
|
+
- gzip
|
31
|
+
vary:
|
32
|
+
- Accept-Encoding
|
33
|
+
body: !binary |
|
34
|
+
H4sIAAAAAAAAA01QwWrDMAy95yu0nrbDrDSU0YExbE3KClkblvSw03BjjYQ5
|
35
|
+
cWY7Hfv7OSmFXQTvSe9JT/wmPWyq9yKDl+o1h+L4nO82sLhH3GXVFjGt0ksn
|
36
|
+
YTFitl+IiDe+04I3JFUAvvWaxCpewd542JqxVxwvZMRxHuIno34n3VL8mwko
|
37
|
+
4oOoGgJL3yM5TwqObzlg25+lbhVqY77GAUer4Uc66IP2c9KC6cE3rQNH9kyW
|
38
|
+
cRwmdxuKVMqSc+JpkHVDmLCELR/gNqVTK/s76Iz6cE5f+cNAfVnmGLNHtjZQ
|
39
|
+
znYgwxpZE6tNB4WxHtYxx6txyDSnCfdPX4j+AIuMg79AAQAA
|
40
|
+
|
41
|
+
http_version: "1.1"
|
@@ -0,0 +1,42 @@
|
|
1
|
+
---
|
2
|
+
- !ruby/struct:VCR::HTTPInteraction
|
3
|
+
request: !ruby/struct:VCR::Request
|
4
|
+
method: :post
|
5
|
+
uri: http://api.face.com:80/faces/detect.json
|
6
|
+
body:
|
7
|
+
headers:
|
8
|
+
accept:
|
9
|
+
- "*/*; q=0.5, application/xml"
|
10
|
+
content-type:
|
11
|
+
- multipart/form-data; boundary=976464
|
12
|
+
accept-encoding:
|
13
|
+
- gzip, deflate
|
14
|
+
content-length:
|
15
|
+
- "69176"
|
16
|
+
response: !ruby/struct:VCR::Response
|
17
|
+
status: !ruby/struct:VCR::ResponseStatus
|
18
|
+
code: 200
|
19
|
+
message: OK
|
20
|
+
headers:
|
21
|
+
content-type:
|
22
|
+
- "application/json; Charset: utf-8"
|
23
|
+
server:
|
24
|
+
- Apache/2.2.16 (Debian) mod_ssl/2.2.16 OpenSSL/0.9.8o
|
25
|
+
date:
|
26
|
+
- Sun, 10 Jul 2011 14:40:56 GMT
|
27
|
+
content-length:
|
28
|
+
- "298"
|
29
|
+
content-encoding:
|
30
|
+
- gzip
|
31
|
+
vary:
|
32
|
+
- Accept-Encoding
|
33
|
+
body: !binary |
|
34
|
+
H4sIAAAAAAAAA0yPMW/DIBCF/4p1a93A4YANU6cOXTuWKML4sIns2ApYrRTl
|
35
|
+
v5cMlbrdO733vbs7bNOa1wTm6w77bQYDU86bscyy4Dwd/LpY5rb4um/z6oZk
|
36
|
+
WWeZsMxbporlbzgWk2Xasl5ybEPrnRCkO98o2ZLjh8tGI9SwxaE0vL+pTkgc
|
37
|
+
nHKofB8a0QsZjtj2FDoiFZqzQKU8celJD10I3MtGe8fdQEEWqi6s7zjkCcxR
|
38
|
+
8homiuOUwSheRHbj85/T41RDyi7vRUHavaeUSm5PbiQw5d1E5Rqs4UaLi9d4
|
39
|
+
HQtNa13DHJdYaJI/cTdKlM85LnTO9FPW8Llf6wp59bHPleCIFUrTcINt9VIS
|
40
|
+
HP5nSkGDvEHssH08fgEAAP//AwD3K/WfbwEAAA==
|
41
|
+
|
42
|
+
http_version: "1.1"
|
@@ -0,0 +1,49 @@
|
|
1
|
+
---
|
2
|
+
- !ruby/struct:VCR::HTTPInteraction
|
3
|
+
request: !ruby/struct:VCR::Request
|
4
|
+
method: :post
|
5
|
+
uri: http://api.face.com:80/faces/detect.json
|
6
|
+
body:
|
7
|
+
headers:
|
8
|
+
accept:
|
9
|
+
- "*/*; q=0.5, application/xml"
|
10
|
+
content-type:
|
11
|
+
- multipart/form-data; boundary=591191
|
12
|
+
accept-encoding:
|
13
|
+
- gzip, deflate
|
14
|
+
content-length:
|
15
|
+
- "78189"
|
16
|
+
response: !ruby/struct:VCR::Response
|
17
|
+
status: !ruby/struct:VCR::ResponseStatus
|
18
|
+
code: 200
|
19
|
+
message: OK
|
20
|
+
headers:
|
21
|
+
content-type:
|
22
|
+
- "application/json; Charset: utf-8"
|
23
|
+
server:
|
24
|
+
- Apache/2.2.16 (Debian) mod_ssl/2.2.16 OpenSSL/0.9.8o
|
25
|
+
date:
|
26
|
+
- Sun, 10 Jul 2011 14:40:50 GMT
|
27
|
+
content-length:
|
28
|
+
- "608"
|
29
|
+
content-encoding:
|
30
|
+
- gzip
|
31
|
+
vary:
|
32
|
+
- Accept-Encoding
|
33
|
+
body: !binary |
|
34
|
+
H4sIAAAAAAAAA6xTPY/cIBD9Kyva7GEwBn9UaZIiUqRISRefLBZjmwjbKwO5
|
35
|
+
u5z2v2ew13vaa9KkMsy8eW/eMH5F52H2s0PVz1cUFosqNHh/ruqkTjqpNFbz
|
36
|
+
WCfybB7C2c6ydXVS1EleJ7ROGCTqJFsjYj9nJG1FrrJSdCpvVSuEYqLEv866
|
37
|
+
R0d0Ni0ofP5Y8EyddNnmqcxPMqPtSVJxSqmCIi151qQUCjXhCkBF1xHFWakk
|
38
|
+
ka3ueKFYCVxPpvUDqkRGjmjQph88qrICLl72mx+/qv349PVb8x8kG15gnjZZ
|
39
|
+
holoSEOghUWruZ/MH3myGlV+CRrUh0W7YbYgPQVrjyiYNrbzeES9uQWtPOk4
|
40
|
+
bCBR89SZZdSQ66R1QDHKKUh7u4KfXi/NW/HVeSowKd+8swzncFV68npB1St6
|
41
|
+
RtXa8hG9wGRi25cj0i+6sbrzOwCKihXABKb8Clg2xhUhOOZiQ3CcpYAY5+CH
|
42
|
+
OxKB2UbCU5zfEHediBSzdMdwfgPdSYldilNcMsBMs9N7kmHBNisMF7FRuVyb
|
43
|
+
2KYS71e2LaAGM+3nF/kEBnIMDMtsYbYPKS7KuJBewSiL1YD0fjGn4LWLmnH9
|
44
|
+
4/e3tAEOKD7v/lytnmKyiH30emo3mztylPY9Mo+Oeyud28h36PrE77CcANaN
|
45
|
+
xpqp/xc2o5fL5fECy+W89AG4kQtKaecAGJzsVwvBxe2icWFHaaaVNytL8G/N
|
46
|
+
aHyUJCRmnfaNN6NuvH6GMPoepuOBksOXYA8pofRAecVIRfPDB6jYfoG9BgQY
|
47
|
+
JYzSguaXy18AAAD//wMArlodeVkEAAA=
|
48
|
+
|
49
|
+
http_version: "1.1"
|
metadata
ADDED
@@ -0,0 +1,198 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: has_face
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 29
|
5
|
+
prerelease:
|
6
|
+
segments:
|
7
|
+
- 0
|
8
|
+
- 0
|
9
|
+
- 1
|
10
|
+
version: 0.0.1
|
11
|
+
platform: ruby
|
12
|
+
authors:
|
13
|
+
- Mario Visic
|
14
|
+
autorequire:
|
15
|
+
bindir: bin
|
16
|
+
cert_chain: []
|
17
|
+
|
18
|
+
date: 2011-07-11 00:00:00 +08:00
|
19
|
+
default_executable:
|
20
|
+
dependencies:
|
21
|
+
- !ruby/object:Gem::Dependency
|
22
|
+
name: rails
|
23
|
+
prerelease: false
|
24
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ">="
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
hash: 7
|
30
|
+
segments:
|
31
|
+
- 3
|
32
|
+
- 0
|
33
|
+
version: "3.0"
|
34
|
+
type: :runtime
|
35
|
+
version_requirements: *id001
|
36
|
+
- !ruby/object:Gem::Dependency
|
37
|
+
name: rest-client
|
38
|
+
prerelease: false
|
39
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
40
|
+
none: false
|
41
|
+
requirements:
|
42
|
+
- - ">="
|
43
|
+
- !ruby/object:Gem::Version
|
44
|
+
hash: 3
|
45
|
+
segments:
|
46
|
+
- 0
|
47
|
+
version: "0"
|
48
|
+
type: :runtime
|
49
|
+
version_requirements: *id002
|
50
|
+
- !ruby/object:Gem::Dependency
|
51
|
+
name: rspec
|
52
|
+
prerelease: false
|
53
|
+
requirement: &id003 !ruby/object:Gem::Requirement
|
54
|
+
none: false
|
55
|
+
requirements:
|
56
|
+
- - ">="
|
57
|
+
- !ruby/object:Gem::Version
|
58
|
+
hash: 3
|
59
|
+
segments:
|
60
|
+
- 2
|
61
|
+
- 0
|
62
|
+
version: "2.0"
|
63
|
+
type: :development
|
64
|
+
version_requirements: *id003
|
65
|
+
- !ruby/object:Gem::Dependency
|
66
|
+
name: rr
|
67
|
+
prerelease: false
|
68
|
+
requirement: &id004 !ruby/object:Gem::Requirement
|
69
|
+
none: false
|
70
|
+
requirements:
|
71
|
+
- - ">="
|
72
|
+
- !ruby/object:Gem::Version
|
73
|
+
hash: 15
|
74
|
+
segments:
|
75
|
+
- 1
|
76
|
+
- 0
|
77
|
+
version: "1.0"
|
78
|
+
type: :development
|
79
|
+
version_requirements: *id004
|
80
|
+
- !ruby/object:Gem::Dependency
|
81
|
+
name: vcr
|
82
|
+
prerelease: false
|
83
|
+
requirement: &id005 !ruby/object:Gem::Requirement
|
84
|
+
none: false
|
85
|
+
requirements:
|
86
|
+
- - ">="
|
87
|
+
- !ruby/object:Gem::Version
|
88
|
+
hash: 15
|
89
|
+
segments:
|
90
|
+
- 1
|
91
|
+
- 0
|
92
|
+
version: "1.0"
|
93
|
+
type: :development
|
94
|
+
version_requirements: *id005
|
95
|
+
- !ruby/object:Gem::Dependency
|
96
|
+
name: fakeweb
|
97
|
+
prerelease: false
|
98
|
+
requirement: &id006 !ruby/object:Gem::Requirement
|
99
|
+
none: false
|
100
|
+
requirements:
|
101
|
+
- - ">="
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
hash: 15
|
104
|
+
segments:
|
105
|
+
- 1
|
106
|
+
- 0
|
107
|
+
version: "1.0"
|
108
|
+
type: :development
|
109
|
+
version_requirements: *id006
|
110
|
+
description: An Active Model validator that uses the face.com API to ensures an image contains a face
|
111
|
+
email:
|
112
|
+
- mario@mariovisic.com
|
113
|
+
executables: []
|
114
|
+
|
115
|
+
extensions: []
|
116
|
+
|
117
|
+
extra_rdoc_files: []
|
118
|
+
|
119
|
+
files:
|
120
|
+
- .gitignore
|
121
|
+
- .rvmrc
|
122
|
+
- Gemfile
|
123
|
+
- README.md
|
124
|
+
- Rakefile
|
125
|
+
- has_face.gemspec
|
126
|
+
- lib/generators/has_face/install_generator.rb
|
127
|
+
- lib/generators/has_face/templates/has_face.rb
|
128
|
+
- lib/has_face.rb
|
129
|
+
- lib/has_face/configuration.rb
|
130
|
+
- lib/has_face/locales/en.yml
|
131
|
+
- lib/has_face/test/matchers.rb
|
132
|
+
- lib/has_face/validator.rb
|
133
|
+
- lib/has_face/version.rb
|
134
|
+
- spec/has_face/test/matchers_spec.rb
|
135
|
+
- spec/has_face/validator_spec.rb
|
136
|
+
- spec/spec_helper.rb
|
137
|
+
- spec/support/assets/hit.jpg
|
138
|
+
- spec/support/assets/miss.jpg
|
139
|
+
- spec/support/models/avatar.rb
|
140
|
+
- spec/support/models/base_user.rb
|
141
|
+
- spec/support/models/user.rb
|
142
|
+
- spec/support/models/user_with_allow_blank.rb
|
143
|
+
- spec/support/models/user_with_allow_nil.rb
|
144
|
+
- spec/support/schema_setup.rb
|
145
|
+
- spec/support/vcr_cassettes/invalid_api_key.yml
|
146
|
+
- spec/support/vcr_cassettes/invalid_detect_url.yml
|
147
|
+
- spec/support/vcr_cassettes/invalid_image.yml
|
148
|
+
- spec/support/vcr_cassettes/valid_image.yml
|
149
|
+
has_rdoc: true
|
150
|
+
homepage: https://github.com/mariovisic/has_face
|
151
|
+
licenses: []
|
152
|
+
|
153
|
+
post_install_message:
|
154
|
+
rdoc_options: []
|
155
|
+
|
156
|
+
require_paths:
|
157
|
+
- lib
|
158
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
159
|
+
none: false
|
160
|
+
requirements:
|
161
|
+
- - ">="
|
162
|
+
- !ruby/object:Gem::Version
|
163
|
+
hash: 3
|
164
|
+
segments:
|
165
|
+
- 0
|
166
|
+
version: "0"
|
167
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
168
|
+
none: false
|
169
|
+
requirements:
|
170
|
+
- - ">="
|
171
|
+
- !ruby/object:Gem::Version
|
172
|
+
hash: 3
|
173
|
+
segments:
|
174
|
+
- 0
|
175
|
+
version: "0"
|
176
|
+
requirements: []
|
177
|
+
|
178
|
+
rubyforge_project: has_face
|
179
|
+
rubygems_version: 1.6.2
|
180
|
+
signing_key:
|
181
|
+
specification_version: 3
|
182
|
+
summary: Easily validate if an image contains faces
|
183
|
+
test_files:
|
184
|
+
- spec/has_face/test/matchers_spec.rb
|
185
|
+
- spec/has_face/validator_spec.rb
|
186
|
+
- spec/spec_helper.rb
|
187
|
+
- spec/support/assets/hit.jpg
|
188
|
+
- spec/support/assets/miss.jpg
|
189
|
+
- spec/support/models/avatar.rb
|
190
|
+
- spec/support/models/base_user.rb
|
191
|
+
- spec/support/models/user.rb
|
192
|
+
- spec/support/models/user_with_allow_blank.rb
|
193
|
+
- spec/support/models/user_with_allow_nil.rb
|
194
|
+
- spec/support/schema_setup.rb
|
195
|
+
- spec/support/vcr_cassettes/invalid_api_key.yml
|
196
|
+
- spec/support/vcr_cassettes/invalid_detect_url.yml
|
197
|
+
- spec/support/vcr_cassettes/invalid_image.yml
|
198
|
+
- spec/support/vcr_cassettes/valid_image.yml
|