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.
- checksums.yaml +4 -4
- data/.travis.yml +8 -1
- data/Gemfile +15 -7
- data/README.md +223 -31
- data/lib/soulless/associations.rb +27 -0
- data/lib/soulless/locale/en.yml +30 -0
- data/lib/soulless/model.rb +8 -1
- data/lib/soulless/validations/associated_validator.rb +17 -0
- data/lib/soulless/validations/uniqueness_validator.rb +38 -0
- data/lib/soulless/validations.rb +10 -0
- data/lib/soulless/version.rb +1 -1
- data/lib/soulless.rb +10 -0
- data/soulless.gemspec +4 -2
- data/spec/associated_validator_spec.rb +31 -0
- data/spec/associations_spec.rb +45 -0
- data/spec/dummy/.gitignore +17 -0
- data/spec/dummy/README.rdoc +28 -0
- data/spec/dummy/Rakefile +6 -0
- data/spec/dummy/app/assets/images/.keep +0 -0
- data/spec/dummy/app/assets/javascripts/application.js +16 -0
- data/spec/dummy/app/assets/stylesheets/application.css +13 -0
- data/spec/dummy/app/controllers/application_controller.rb +5 -0
- data/spec/dummy/app/controllers/concerns/.keep +0 -0
- data/spec/dummy/app/forms/dummy_form.rb +7 -0
- data/spec/dummy/app/helpers/application_helper.rb +2 -0
- data/spec/dummy/app/mailers/.keep +0 -0
- data/spec/dummy/app/models/.keep +0 -0
- data/spec/dummy/app/models/concerns/.keep +0 -0
- data/spec/dummy/app/models/dummy_model.rb +3 -0
- data/spec/dummy/app/views/layouts/application.html.erb +14 -0
- data/spec/dummy/bin/bundle +3 -0
- data/spec/dummy/bin/rails +4 -0
- data/spec/dummy/bin/rake +4 -0
- data/spec/dummy/config/application.rb +28 -0
- data/spec/dummy/config/boot.rb +6 -0
- data/spec/dummy/config/database.yml +25 -0
- data/spec/dummy/config/environment.rb +5 -0
- data/spec/dummy/config/environments/development.rb +29 -0
- data/spec/dummy/config/environments/production.rb +80 -0
- data/spec/dummy/config/environments/test.rb +36 -0
- data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
- data/spec/dummy/config/initializers/filter_parameter_logging.rb +4 -0
- data/spec/dummy/config/initializers/inflections.rb +16 -0
- data/spec/dummy/config/initializers/mime_types.rb +5 -0
- data/spec/dummy/config/initializers/secret_token.rb +12 -0
- data/spec/dummy/config/initializers/session_store.rb +3 -0
- data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
- data/spec/dummy/config/locales/en.yml +23 -0
- data/spec/dummy/config/routes.rb +56 -0
- data/spec/dummy/config.ru +4 -0
- data/spec/dummy/db/migrate/20131106221200_create_dummy_models.rb +9 -0
- data/spec/dummy/db/schema.rb +22 -0
- data/spec/dummy/db/seeds.rb +7 -0
- data/spec/dummy/db/test.sqlite3 +0 -0
- data/spec/dummy/lib/assets/.keep +0 -0
- data/spec/dummy/lib/tasks/.keep +0 -0
- data/spec/dummy/log/.keep +0 -0
- data/spec/dummy/public/404.html +58 -0
- data/spec/dummy/public/422.html +58 -0
- data/spec/dummy/public/500.html +57 -0
- data/spec/dummy/public/favicon.ico +0 -0
- data/spec/dummy/public/robots.txt +5 -0
- data/spec/dummy/vendor/assets/javascripts/.keep +0 -0
- data/spec/dummy/vendor/assets/stylesheets/.keep +0 -0
- data/spec/soulless_spec.rb +1 -1
- data/spec/spec_helper.rb +22 -3
- data/spec/support/dummy_association.rb +30 -0
- data/spec/support/en.yml +8 -0
- data/spec/uniqueness_validator_spec.rb +17 -0
- metadata +148 -7
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b3a918d43aa402d0de13f3e8bcc28b1d4cbad074
|
4
|
+
data.tar.gz: a706a4c34294af699a1efdbda86dd649ffd278f5
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 0187eb34dda842f89d9be39593e3168c1189ae7abbe61343b1ec5e80e14bebbfeaaebfd357f0c1c2257edd643534795f901f91104b2765f5ed614005ec0dd94f
|
7
|
+
data.tar.gz: fab7de2ebac15f30943af023b644e93759131fc40e3a3a46523440e3da42980f88e55aaae551ee1aad1b00e64c705bd6d0e9931821f2b6a39340db62aa2abd1a
|
data/.travis.yml
CHANGED
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
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
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
|
-
|
13
|
-
|
14
|
-
|
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
|
[](https://travis-ci.org/anthonator/soulless) [](https://gemnasium.com/anthonator/soulless)
|
6
6
|
[](https://coveralls.io/r/anthonator/soulless?branch=master) [](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 :
|
35
|
-
attribute :
|
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
|
-
#
|
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
|
-
|
49
|
+
### Default Values
|
50
|
+
|
51
|
+
When defining attributes it's possible to specify default values.
|
53
52
|
|
54
53
|
```ruby
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
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
|
-
###
|
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
|
-
|
79
|
-
|
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
|
-
|
86
|
-
|
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
|
-
|
153
|
+
Check to see if your object is valid by calling ```valid?```.
|
91
154
|
|
92
|
-
|
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
|
-
|
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
|
-
|
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"
|
data/lib/soulless/model.rb
CHANGED
@@ -3,8 +3,15 @@ module Soulless
|
|
3
3
|
def self.included(base)
|
4
4
|
base.class_eval do
|
5
5
|
extend ActiveModel::Naming
|
6
|
-
|
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
|
data/lib/soulless/version.rb
CHANGED
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{
|
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'
|
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
|