has_face 0.0.1
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/.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
|