soulless 0.1.0 → 0.2.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.
Files changed (70) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +8 -1
  3. data/Gemfile +15 -7
  4. data/README.md +223 -31
  5. data/lib/soulless/associations.rb +27 -0
  6. data/lib/soulless/locale/en.yml +30 -0
  7. data/lib/soulless/model.rb +8 -1
  8. data/lib/soulless/validations/associated_validator.rb +17 -0
  9. data/lib/soulless/validations/uniqueness_validator.rb +38 -0
  10. data/lib/soulless/validations.rb +10 -0
  11. data/lib/soulless/version.rb +1 -1
  12. data/lib/soulless.rb +10 -0
  13. data/soulless.gemspec +4 -2
  14. data/spec/associated_validator_spec.rb +31 -0
  15. data/spec/associations_spec.rb +45 -0
  16. data/spec/dummy/.gitignore +17 -0
  17. data/spec/dummy/README.rdoc +28 -0
  18. data/spec/dummy/Rakefile +6 -0
  19. data/spec/dummy/app/assets/images/.keep +0 -0
  20. data/spec/dummy/app/assets/javascripts/application.js +16 -0
  21. data/spec/dummy/app/assets/stylesheets/application.css +13 -0
  22. data/spec/dummy/app/controllers/application_controller.rb +5 -0
  23. data/spec/dummy/app/controllers/concerns/.keep +0 -0
  24. data/spec/dummy/app/forms/dummy_form.rb +7 -0
  25. data/spec/dummy/app/helpers/application_helper.rb +2 -0
  26. data/spec/dummy/app/mailers/.keep +0 -0
  27. data/spec/dummy/app/models/.keep +0 -0
  28. data/spec/dummy/app/models/concerns/.keep +0 -0
  29. data/spec/dummy/app/models/dummy_model.rb +3 -0
  30. data/spec/dummy/app/views/layouts/application.html.erb +14 -0
  31. data/spec/dummy/bin/bundle +3 -0
  32. data/spec/dummy/bin/rails +4 -0
  33. data/spec/dummy/bin/rake +4 -0
  34. data/spec/dummy/config/application.rb +28 -0
  35. data/spec/dummy/config/boot.rb +6 -0
  36. data/spec/dummy/config/database.yml +25 -0
  37. data/spec/dummy/config/environment.rb +5 -0
  38. data/spec/dummy/config/environments/development.rb +29 -0
  39. data/spec/dummy/config/environments/production.rb +80 -0
  40. data/spec/dummy/config/environments/test.rb +36 -0
  41. data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
  42. data/spec/dummy/config/initializers/filter_parameter_logging.rb +4 -0
  43. data/spec/dummy/config/initializers/inflections.rb +16 -0
  44. data/spec/dummy/config/initializers/mime_types.rb +5 -0
  45. data/spec/dummy/config/initializers/secret_token.rb +12 -0
  46. data/spec/dummy/config/initializers/session_store.rb +3 -0
  47. data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
  48. data/spec/dummy/config/locales/en.yml +23 -0
  49. data/spec/dummy/config/routes.rb +56 -0
  50. data/spec/dummy/config.ru +4 -0
  51. data/spec/dummy/db/migrate/20131106221200_create_dummy_models.rb +9 -0
  52. data/spec/dummy/db/schema.rb +22 -0
  53. data/spec/dummy/db/seeds.rb +7 -0
  54. data/spec/dummy/db/test.sqlite3 +0 -0
  55. data/spec/dummy/lib/assets/.keep +0 -0
  56. data/spec/dummy/lib/tasks/.keep +0 -0
  57. data/spec/dummy/log/.keep +0 -0
  58. data/spec/dummy/public/404.html +58 -0
  59. data/spec/dummy/public/422.html +58 -0
  60. data/spec/dummy/public/500.html +57 -0
  61. data/spec/dummy/public/favicon.ico +0 -0
  62. data/spec/dummy/public/robots.txt +5 -0
  63. data/spec/dummy/vendor/assets/javascripts/.keep +0 -0
  64. data/spec/dummy/vendor/assets/stylesheets/.keep +0 -0
  65. data/spec/soulless_spec.rb +1 -1
  66. data/spec/spec_helper.rb +22 -3
  67. data/spec/support/dummy_association.rb +30 -0
  68. data/spec/support/en.yml +8 -0
  69. data/spec/uniqueness_validator_spec.rb +17 -0
  70. metadata +148 -7
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: ed8e30f22d775b7646b996ff1a6ecf92ec96fe98
4
- data.tar.gz: 288d8bf2b2b3b077e0369477168f4ff6e1bdacd3
3
+ metadata.gz: b3a918d43aa402d0de13f3e8bcc28b1d4cbad074
4
+ data.tar.gz: a706a4c34294af699a1efdbda86dd649ffd278f5
5
5
  SHA512:
6
- metadata.gz: bd80dec834541eb84183a91486b1e18d0dd5d35385b01a0cce3bf096ea8fa6d85f22eaeb181d2432bf3d90d1ce67086c53440ea0257fa3de90e7f0456d555cec
7
- data.tar.gz: faa287251a4f8ceaf9ef077d02279ece8755c55517d685a1c57d4da3c9f441e5d24d2428a22994d18cb9efb011a5b9caab6b0a3799a3ecdd12f646a4609dee5f
6
+ metadata.gz: 0187eb34dda842f89d9be39593e3168c1189ae7abbe61343b1ec5e80e14bebbfeaaebfd357f0c1c2257edd643534795f901f91104b2765f5ed614005ec0dd94f
7
+ data.tar.gz: fab7de2ebac15f30943af023b644e93759131fc40e3a3a46523440e3da42980f88e55aaae551ee1aad1b00e64c705bd6d0e9931821f2b6a39340db62aa2abd1a
data/.travis.yml CHANGED
@@ -5,4 +5,11 @@ rvm:
5
5
  - ruby-head
6
6
  - jruby-19mode
7
7
  - jruby-head
8
- - rbx-19mode
8
+ - rbx-19mode
9
+ env:
10
+ - "RAILS_VERSION=3.2.0"
11
+ - "RAILS_VERSION=4.0.0"
12
+ - "RAILS_VERSION=master"
13
+ matrix:
14
+ allow_failures:
15
+ - env: "RAILS_VERSION=master"
data/Gemfile CHANGED
@@ -3,13 +3,21 @@ source 'https://rubygems.org'
3
3
  # Specify your gem's dependencies in ar_book_finder.gemspec
4
4
  gemspec
5
5
 
6
- group :development do
7
- gem 'bundler'
8
- gem 'coveralls', require: false
9
- gem 'rake'
6
+ rails_version = case ENV['RAILS_VERSION'] || 'default'
7
+ when 'master'
8
+ { git: 'https://github.com/rails/rails.git' }
9
+ when 'default'
10
+ '~> 4.0'
11
+ else
12
+ "~> #{ENV['RAILS_VERSION']}"
10
13
  end
11
14
 
12
- group :test do
13
- gem 'rspec'
14
- gem 'simplecov'
15
+ gem 'rails', rails_version
16
+
17
+ platforms :ruby do
18
+ gem 'sqlite3'
19
+ end
20
+
21
+ platforms :jruby do
22
+ gem "activerecord-jdbcsqlite3-adapter"
15
23
  end
data/README.md CHANGED
@@ -1,12 +1,10 @@
1
1
  # Soulless
2
2
 
3
- Rails models without the database (and Rails).
3
+ Rails models without the database (and Rails). Great for implementing the form object pattern.
4
4
 
5
5
  [![Build Status](https://travis-ci.org/anthonator/soulless.png?branch=master)](https://travis-ci.org/anthonator/soulless) [![Dependency Status](https://gemnasium.com/anthonator/soulless.png)](https://gemnasium.com/anthonator/soulless)
6
6
  [![Coverage Status](https://coveralls.io/repos/anthonator/soulless/badge.png?branch=master)](https://coveralls.io/r/anthonator/soulless?branch=master) [![Code Climate](https://codeclimate.com/github/anthonator/soulless.png)](https://codeclimate.com/github/anthonator/soulless)
7
7
 
8
- Great for implementing the form object pattern. [Check out #3](http://blog.codeclimate.com/blog/2012/10/17/7-ways-to-decompose-fat-activerecord-models/) of this great article by @brynary for more information on the form object pattern.
9
-
10
8
  ## Installation
11
9
 
12
10
  Add this line to your application's Gemfile:
@@ -23,77 +21,271 @@ Or install it yourself as:
23
21
 
24
22
  ## Usage
25
23
 
26
- Just define a plain-old-ruby-object and get crackin'!
24
+ Just define a plain-old-ruby-object, include Soulless and get crackin'!
27
25
 
28
26
  ```ruby
29
27
  class UserSignupForm
30
28
  include Soulless.model
31
29
 
32
- attribute :email, String
33
30
  attribute :name, String
34
- attribute :password
35
- attribute :super_awesome, Boolean, default: false
36
-
37
- validates :email, presence: true
31
+ attribute :email, String
32
+ attribute :password, String
38
33
 
39
34
  validates :name, presence: true
40
35
 
36
+ validates :email, presence: true,
37
+ uniqueness: { model: User }
38
+
41
39
  validates :password, presence: true,
42
40
  lenght: { is_at_least: 8 }
43
41
 
44
42
  private
45
43
  def persist!
46
- # You're gonna need this!
47
- # Define what to do with this form when ready to save here.
44
+ # Define what to do when this form is ready to be saved.
48
45
  end
49
46
  end
50
47
  ```
51
48
 
52
- Awesome. Now let's do something with it.
49
+ ### Default Values
50
+
51
+ When defining attributes it's possible to specify default values.
53
52
 
54
53
  ```ruby
55
- UsersController < ApplicationController
56
- def create
57
- @form = UserSignupForm.new(params.permit(user: [:email, :name, :password, :super_awesome]))
58
- if @form.save
59
- # do something here
60
- else
61
- render :new
62
- end
63
- end
54
+ class UserSignupForm
55
+
56
+ ...
57
+
58
+ attribute :name, String, default: 'Anthony'
59
+
60
+ ...
64
61
  end
65
62
  ```
66
63
 
67
- ### Configuration
64
+ ### Embedded Values
65
+
66
+ It's possible to use other Soulless objects as attribute types.
68
67
 
69
68
  ```ruby
69
+ class User
70
+ include Soulless.model
71
+
72
+ attribute :name, String
73
+ attribute :email, String
74
+ attribute :password, String
75
+ end
76
+
70
77
  class UserSignupForm
71
- # include attribute DSL + constructor + mass-assignment
72
78
  include Soulless.model
79
+
80
+ attribute :user, User
73
81
  end
74
82
  ```
75
83
 
84
+ ### Collection Coercions
85
+
86
+ Define collections with specific types.
87
+
88
+ ```ruby
89
+ class PhoneNumber
90
+ include Soulless.model
91
+
92
+ attribute :number, String
93
+ end
94
+
95
+ class Person
96
+ include Soulless.model
97
+
98
+ attribute :phone_numbers, Array[PhoneNumber]
99
+ end
100
+ ```
101
+
102
+ ### Processing an Object
103
+
104
+ Soulless let's _you_ define what happens when your object is ready to be processed.
105
+
76
106
  ```ruby
77
107
  class UserSignupForm
78
- # include attribute DSL + constructor
79
- include Soulless.model(mass_assignment: false)
108
+
109
+ ...
110
+
111
+ private
112
+ def persist!
113
+ user = User.create!(name: name, email: email, password: password)
114
+ UserMailer.send_activation_code(user).deliver
115
+ user.charge_card!
116
+ end
80
117
  end
81
118
  ```
82
119
 
120
+ Process your Soulless object by calling ```save```. Just like a Rails model!
121
+
122
+ ```ruby
123
+ form = UserSignupForm.new(name: name, email: email, password: passord)
124
+ if form.save
125
+ # Called persist! and all is good!
126
+ else
127
+ # Looks like a validation failed. Try again.
128
+ end
129
+ ```
130
+
131
+ ### Validations and Errors
132
+
133
+ Soulless lets you define your validations and manage your errors just like you did in Rails.
134
+
83
135
  ```ruby
84
136
  class UserSignupForm
85
- # include attribute DSL
86
- include Soulless.model(constructor: false, mass_assignment: false)
137
+
138
+ ...
139
+
140
+ validates :name, presence: true
141
+
142
+ validates :email, presence: true,
143
+ uniqueness: { model: User }
144
+
145
+ validates :password, presence: true,
146
+ lenght: { minimum: 8 }
147
+
148
+ ...
149
+
87
150
  end
88
151
  ```
89
152
 
90
- ### Batteries included...
153
+ Check to see if your object is valid by calling ```valid?```.
91
154
 
92
- The great thing about Soulless? Rails isn't required.
155
+ ```ruby
156
+ form = UserSignupForm.new(name: name, email: email)
157
+ form.valid? # => false
158
+ ```
159
+
160
+ See what errors are popping up using the ```errors``` attribute.
93
161
 
94
- ### This looks familir...
162
+ ```ruby
163
+ form = UserSignupForm.new(name: name, email: email)
164
+ form.valid?
165
+ form.errors[:password] # => ["is too short (minimum is 8 characters)"]
166
+ ```
95
167
 
96
- You noticed? We're using Virtus to power this gem. That means you get all the Virtus goodies for free. Check out [their GitHub page](https://github.com/solnic/virtus) to see what's up.
168
+ #### Uniqueness Validations
169
+
170
+ If you're using Soulless in Rails it's even possible to validate uniqueness.
171
+
172
+ ```ruby
173
+ class UserSignupForm
174
+
175
+ ...
176
+
177
+ validates :primary_email, presence: true,
178
+ uniqueness: { model: User, attribute: :email }
179
+
180
+ ...
181
+
182
+ end
183
+ ```
184
+
185
+ Just let the validator know what ActiveRecord model to use when performing the validation using the ```model``` option.
186
+
187
+ If your Soulless object attribute doesn't match up to the ActiveRecord model attribute just map it using the ```attribute``` option.
188
+
189
+ ### ```has_one``` and ```has_many``` Associations
190
+
191
+ When you need associations use ```has_one``` and ```has_many```. Look familiar?
192
+
193
+ ```ruby
194
+ class Person
195
+ include Soulless.model
196
+
197
+ attribute :name, String
198
+
199
+ validates :name, presence: true
200
+
201
+ has_one :spouse do
202
+ attribute :name, String
203
+ end
204
+
205
+ has_many :friends do
206
+ attribute :name, String
207
+ end
208
+ end
209
+ ```
210
+
211
+ You can set ```has_one``` and ```has_many``` attributes by setting their values hashes and hash arrays.
212
+
213
+ ```ruby
214
+ person = Person.new(name: 'Anthony')
215
+ person.spouse = { name: 'Megan' }
216
+ person.friends = [{ name: 'Yaw' }, { name: 'Biff' }]
217
+ ```
218
+
219
+ It's also possible for an association to inherit from a parent class and then extend functionality.
220
+
221
+ ```ruby
222
+ class Person
223
+ include Soulless.model
224
+
225
+ attribute :name, String
226
+
227
+ validates :name, presence: true
228
+
229
+ has_one :spouse, Person do # inherits 'name' and validation from Person
230
+ attribute :anniversary, DateTime
231
+
232
+ validates :anniversary, presence: true
233
+ end
234
+
235
+ has_many :friends, Person # just inherit from Person, don't extend
236
+ end
237
+ ```
238
+
239
+ When you need to make sure an association is valid before processing the object use ```validates_associated```.
240
+
241
+ ```ruby
242
+ class Person
243
+
244
+ ...
245
+
246
+ has_one :spouse do
247
+ attribute :name, String
248
+
249
+ validates :name, presence: true
250
+ end
251
+
252
+ ...
253
+
254
+ end
255
+
256
+ person = Person.new(name: 'Anthony')
257
+ person.spouse = { name: nil }
258
+ person.valid? # => false
259
+ person.errors[:spouse] # => ["is invalid"]
260
+ person.spouse.errors[:name] # => ["can't be blank"]
261
+ ```
262
+
263
+ ### I18n
264
+
265
+ Define locales similar to how you would define them in Rails.
266
+
267
+
268
+ ```yaml
269
+ en:
270
+ soulless:
271
+ errors:
272
+ models:
273
+ person:
274
+ name:
275
+ blank: "there's nothing here"
276
+ ```
277
+
278
+ For attributes defined as ```has_one``` and ```has_many``` associations use the enclosing class as the locale key's namespace.
279
+
280
+ ```yaml
281
+ en:
282
+ soulless:
283
+ errors:
284
+ models:
285
+ person/spouse:
286
+ name:
287
+ blank: "there's nothing here"
288
+ ```
97
289
 
98
290
  ## Contributing
99
291
 
@@ -0,0 +1,27 @@
1
+ module Soulless
2
+ module Associations
3
+ def self.included(base)
4
+ base.instance_eval do |object|
5
+ def has_one(name, superclass = Object, &block)
6
+ klass = define_virtus_class(name, superclass, &block)
7
+ send(:attribute, name, klass)
8
+ end
9
+
10
+ def has_many(name, superclass = Object, &block)
11
+ klass = define_virtus_class(name, superclass, &block)
12
+ send(:attribute, name, Array[klass])
13
+ end
14
+
15
+ private
16
+ def define_virtus_class(name, superclass, &block)
17
+ klass_name = name.to_s.singularize.classify + '_' + SecureRandom.hex
18
+ klass = const_set(klass_name, Class.new(superclass))
19
+ klass.send(:include, Soulless.model) unless klass.included_modules.include?(Model)
20
+ klass.instance_eval(&block) if block_given?
21
+ klass.model_name.instance_variable_set(:@i18n_key, klass.model_name.i18n_key.to_s.gsub(/_[^_]+$/, '').underscore.to_sym)
22
+ klass
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,30 @@
1
+ en:
2
+ errors:
3
+ # The default format to use in full error messages.
4
+ format: "%{attribute} %{message}"
5
+
6
+ # The values :model, :attribute and :value are always available for interpolation
7
+ # The value :count is available when applicable. Can be used for pluralization.
8
+ messages:
9
+ inclusion: "is not included in the list"
10
+ exclusion: "is reserved"
11
+ invalid: "is invalid"
12
+ confirmation: "doesn't match %{attribute}"
13
+ accepted: "must be accepted"
14
+ empty: "can't be empty"
15
+ blank: "can't be blank"
16
+ present: "must be blank"
17
+ too_long: "is too long (maximum is %{count} characters)"
18
+ too_short: "is too short (minimum is %{count} characters)"
19
+ wrong_length: "is the wrong length (should be %{count} characters)"
20
+ not_a_number: "is not a number"
21
+ not_an_integer: "must be an integer"
22
+ greater_than: "must be greater than %{count}"
23
+ greater_than_or_equal_to: "must be greater than or equal to %{count}"
24
+ equal_to: "must be equal to %{count}"
25
+ less_than: "must be less than %{count}"
26
+ less_than_or_equal_to: "must be less than or equal to %{count}"
27
+ other_than: "must be other than %{count}"
28
+ odd: "must be odd"
29
+ even: "must be even"
30
+ taken: "has already been taken"
@@ -3,8 +3,15 @@ module Soulless
3
3
  def self.included(base)
4
4
  base.class_eval do
5
5
  extend ActiveModel::Naming
6
- include ActiveModel::Conversion
6
+ extend ActiveModel::Translation
7
7
  include ActiveModel::Validations
8
+ include ActiveModel::Conversion
9
+
10
+ class << self
11
+ def i18n_scope
12
+ :soulless
13
+ end
14
+ end
8
15
 
9
16
  def persisted?
10
17
  false
@@ -0,0 +1,17 @@
1
+ module Soulless
2
+ module Validations
3
+ class AssociatedValidator < ActiveModel::EachValidator
4
+ def validate_each(record, attribute, value)
5
+ if Array.wrap(value).reject { |r| r.valid? }.any?
6
+ record.errors.add(attribute, :invalid, options.merge(value: value))
7
+ end
8
+ end
9
+ end
10
+
11
+ module ClassMethods
12
+ def validates_associated(*attr_names)
13
+ validates_with(AssociatedValidator, _merge_attributes(attr_names))
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,38 @@
1
+ module Soulless
2
+ module Validations
3
+ class UniquenessValidator < ActiveModel::EachValidator
4
+ def initialize(options)
5
+ raise 'ActiveRecord is not defined. The Soulless uniqueness validator cannot be used when ActiveRecord is not present.' unless Object.const_defined?('ActiveRecord')
6
+ @model = options[:model]
7
+ @attribute = options[:attribute]
8
+ options.merge!(class: @model) if ActiveModel::VERSION::STRING >= '4.1.0'
9
+ @validator = ActiveRecord::Validations::UniquenessValidator.new(options)
10
+ super(options)
11
+ end
12
+
13
+ def validate_each(record, attribute, value)
14
+ if !@model
15
+ raise ArgumentError, 'Missing required argument "model"'
16
+ else
17
+ record_orig, attribute_orig = record, attribute
18
+
19
+ attribute = @attribute if @attribute
20
+ record = @model.new(attribute => value)
21
+
22
+ @validator.setup(@model) if ActiveModel::VERSION::STRING < '4.1'
23
+ @validator.validate_each(record, attribute, value)
24
+
25
+ if record.errors.any?
26
+ record_orig.errors.add(attribute, :taken, options.except(:case_sensitive, :scope).merge(value: value))
27
+ end
28
+ end
29
+ end
30
+ end
31
+
32
+ module ClassMethods
33
+ def validates_uniqueness_of(*attr_name)
34
+ validates_with(UniquenessValidator, _merge_attributes(attr_name))
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,10 @@
1
+ require 'soulless/validations/associated_validator'
2
+ require 'soulless/validations/uniqueness_validator'
3
+
4
+ module Soulless
5
+ module Validations
6
+ def self.included(base)
7
+ base.send(:extend, ClassMethods)
8
+ end
9
+ end
10
+ end
@@ -1,3 +1,3 @@
1
1
  module Soulless
2
- VERSION = "0.1.0"
2
+ VERSION = "0.2.0"
3
3
  end
data/lib/soulless.rb CHANGED
@@ -1,8 +1,11 @@
1
1
  require 'virtus'
2
+ require 'securerandom'
3
+ require 'active_support/core_ext/array/wrap'
2
4
  require 'active_support/core_ext/class/attribute'
3
5
  require 'active_support/core_ext/module/delegation'
4
6
  require 'active_support/callbacks'
5
7
  require 'active_support/concern'
8
+ require 'active_support/inflector'
6
9
  require 'active_model/naming'
7
10
  require 'active_model/translation'
8
11
  require 'active_model/callbacks'
@@ -10,16 +13,23 @@ require 'active_model/validator'
10
13
  require 'active_model/errors'
11
14
  require 'active_model/validations'
12
15
  require 'active_model/conversion'
16
+ require 'active_model/version'
13
17
 
14
18
  require 'soulless/model'
19
+ require 'soulless/associations'
20
+ require 'soulless/validations'
15
21
  require 'soulless/version'
16
22
 
17
23
  module Soulless
24
+ I18n.load_path += Dir.glob('lib/soulless/locale/*.{rb,yml}')
25
+
18
26
  def self.model(options = {})
19
27
  mod = Module.new
20
28
  mod.define_singleton_method :included do |object|
21
29
  object.send(:include, Virtus.model(options))
22
30
  object.send(:include, Model)
31
+ object.send(:include, Associations)
32
+ object.send(:include, Validations)
23
33
  end
24
34
  mod
25
35
  end
data/soulless.gemspec CHANGED
@@ -8,7 +8,7 @@ Gem::Specification.new do |spec|
8
8
  spec.version = Soulless::VERSION
9
9
  spec.authors = ['Anthony Smith']
10
10
  spec.email = ['anthony@sticksnleaves.com']
11
- spec.description = %q{Models without a soul.}
11
+ spec.description = %q{Rails models without a database (and Rails).}
12
12
  spec.summary = %q{Create Rails style models without the database.}
13
13
  spec.homepage = ''
14
14
  spec.license = 'MIT'
@@ -22,6 +22,8 @@ Gem::Specification.new do |spec|
22
22
  spec.add_runtime_dependency 'activemodel', '>= 3.2'
23
23
  spec.add_runtime_dependency 'virtus', '>= 1.0'
24
24
 
25
- spec.add_development_dependency 'bundler', '~> 1.3'
25
+ spec.add_development_dependency 'bundler'
26
+ spec.add_development_dependency 'database_cleaner'
26
27
  spec.add_development_dependency 'rake'
28
+ spec.add_development_dependency 'rspec'
27
29
  end
@@ -0,0 +1,31 @@
1
+ require 'spec_helper'
2
+
3
+ describe Soulless::Validations::AssociatedValidator do
4
+ it 'should not be valid if has_one associations are invalid' do
5
+ dummy_association = DummyAssociation.new
6
+ dummy_association.spouse = { name: nil }
7
+ dummy_association.valid?.should be_false
8
+ dummy_association.errors[:spouse].should include('is invalid')
9
+ dummy_association.spouse.errors[:name].should include("can't be blank")
10
+ end
11
+
12
+ it 'should be valid if has_one associations are valid' do
13
+ dummy_association = DummyAssociation.new
14
+ dummy_association.spouse = { name: 'Megan' }
15
+ dummy_association.valid?.should be_true
16
+ end
17
+
18
+ it 'should not be valid if has_many associations are invalid' do
19
+ dummy_association = DummyAssociation.new
20
+ dummy_association.friends = [{ name: nil }, { name: nil }]
21
+ dummy_association.valid?.should be_false
22
+ dummy_association.errors[:friends].should include('is invalid')
23
+ dummy_association.friends[0].errors[:name].should include("can't be blank")
24
+ end
25
+
26
+ it 'should be valid if has_many associations are valid' do
27
+ dummy_association = DummyAssociation.new
28
+ dummy_association.friends = [{ name: 'Yaw' }, { name: 'Biff' }]
29
+ dummy_association.valid?.should be_true
30
+ end
31
+ end
@@ -0,0 +1,45 @@
1
+ require 'spec_helper'
2
+
3
+ describe Soulless::Associations do
4
+ before(:each) do
5
+ @dummy_class = DummyAssociation.new
6
+ end
7
+
8
+ describe 'has_one' do
9
+ it 'should allow hash values' do
10
+ @dummy_class.spouse = { name: 'Megan' }
11
+ @dummy_class.spouse.name.should == 'Megan'
12
+ end
13
+
14
+ it 'should allow a class type to be defined' do
15
+ @dummy_class.dummy_clone = { name: 'Megan' }
16
+ @dummy_class.dummy_clone.class.name.should match(/\ADummyAssociation::DummyClone/)
17
+ end
18
+
19
+ it 'should properly pull down error translations' do
20
+ @dummy_class.dummy_clone = { name: nil }
21
+ @dummy_class.dummy_clone.save
22
+ @dummy_class.dummy_clone.errors[:name][0].should == "can't be blank"
23
+ end
24
+
25
+ it 'should properly pull down custom error translations' do
26
+ I18n.load_path += Dir.glob(File.dirname(__FILE__) + '/support/en.yml')
27
+ I18n.backend.load_translations
28
+ @dummy_class.dummy_clone = { name: nil }
29
+ @dummy_class.dummy_clone.save
30
+ @dummy_class.dummy_clone.errors[:name][0].should == 'this is a test'
31
+ end
32
+ end
33
+
34
+ describe 'has_many' do
35
+ it 'should allow array values' do
36
+ @dummy_class.friends = [{ name: 'Biff' }]
37
+ @dummy_class.friends[0].name.should == 'Biff'
38
+ end
39
+
40
+ it 'should allow a class type to be defined' do
41
+ @dummy_class.dummy_clones = [{ name: 'Biff' }]
42
+ @dummy_class.dummy_clones[0].class.name.should match(/\ADummyAssociation::DummyClone/)
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,17 @@
1
+ # See https://help.github.com/articles/ignoring-files for more about ignoring files.
2
+ #
3
+ # If you find yourself ignoring temporary files generated by your text editor
4
+ # or operating system, you probably want to add a global ignore instead:
5
+ # git config --global core.excludesfile '~/.gitignore_global'
6
+
7
+ # Ignore bundler config.
8
+ /.bundle
9
+
10
+ # Ignore the default SQLite database.
11
+ /db/development.sqlite3
12
+ /db/production.sqlite3
13
+ /db/*.sqlite3-journal
14
+
15
+ # Ignore all logfiles and tempfiles.
16
+ /log/*.log
17
+ /tmp