domain_model 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +17 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +35 -0
- data/Rakefile +8 -0
- data/domain_model.gemspec +24 -0
- data/lib/domain_model.rb +322 -0
- data/lib/domain_model/version.rb +3 -0
- data/spec/model_spec.rb +453 -0
- data/spec/spec_helper.rb +2 -0
- metadata +106 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2014 TODO: Write your name
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
# DomainModel
|
2
|
+
|
3
|
+
TODO: Write a gem description
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Add this line to your application's Gemfile:
|
8
|
+
|
9
|
+
gem 'model'
|
10
|
+
|
11
|
+
And then execute:
|
12
|
+
|
13
|
+
$ bundle
|
14
|
+
|
15
|
+
Or install it yourself as:
|
16
|
+
|
17
|
+
$ gem install model
|
18
|
+
|
19
|
+
## Tests
|
20
|
+
|
21
|
+
Run the tests:
|
22
|
+
|
23
|
+
rake test
|
24
|
+
|
25
|
+
## Usage
|
26
|
+
|
27
|
+
TODO: Write usage instructions here
|
28
|
+
|
29
|
+
## Contributing
|
30
|
+
|
31
|
+
1. Fork it
|
32
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
33
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
34
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
35
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'domain_model/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "domain_model"
|
8
|
+
spec.version = DomainModel::VERSION
|
9
|
+
spec.authors = ["Rafer Hazen"]
|
10
|
+
spec.email = ["rafer@ralua.com"]
|
11
|
+
spec.summary = %q{Minimal framework for definition of type-aware domain models}
|
12
|
+
spec.description = spec.summary
|
13
|
+
spec.homepage = ""
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = `git ls-files`.split($/)
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.add_development_dependency "bundler", "~> 1.3"
|
22
|
+
spec.add_development_dependency "rake"
|
23
|
+
spec.add_development_dependency "rspec", "~> 2.14"
|
24
|
+
end
|
data/lib/domain_model.rb
ADDED
@@ -0,0 +1,322 @@
|
|
1
|
+
module DomainModel
|
2
|
+
def self.included(base)
|
3
|
+
base.extend(ClassMethods)
|
4
|
+
end
|
5
|
+
|
6
|
+
def initialize(attributes={})
|
7
|
+
self.class.fields.select(&:collection?).each do |field|
|
8
|
+
send("#{field.name}=", [])
|
9
|
+
end
|
10
|
+
|
11
|
+
attributes.each { |k,v | send("#{k}=", v) }
|
12
|
+
end
|
13
|
+
|
14
|
+
def errors
|
15
|
+
errors = ModelErrors.new
|
16
|
+
|
17
|
+
self.class.fields.each do |field|
|
18
|
+
errors.add(field.name, field.errors(self.send(field.name)))
|
19
|
+
end
|
20
|
+
|
21
|
+
self.class.validations.each { |v| v.execute(self, errors) }
|
22
|
+
|
23
|
+
errors
|
24
|
+
end
|
25
|
+
|
26
|
+
def valid?
|
27
|
+
errors.empty?
|
28
|
+
end
|
29
|
+
|
30
|
+
def ==(other)
|
31
|
+
other.is_a?(self.class) && attributes == other.attributes
|
32
|
+
end
|
33
|
+
|
34
|
+
def inspect
|
35
|
+
"#<#{self.class} " + attributes.map { |n, v| "#{n}: #{v.inspect}" }.join(", ") + ">"
|
36
|
+
end
|
37
|
+
|
38
|
+
def attributes
|
39
|
+
attributes = {}
|
40
|
+
self.class.fields.map(&:name).each do |name|
|
41
|
+
attributes[name] = send(name)
|
42
|
+
end
|
43
|
+
attributes
|
44
|
+
end
|
45
|
+
|
46
|
+
def to_primitive
|
47
|
+
Serializer.serialize(self)
|
48
|
+
end
|
49
|
+
|
50
|
+
module ClassMethods
|
51
|
+
def validate(*args, &block)
|
52
|
+
@validations ||= []
|
53
|
+
validations << Validation.new(*args, &block)
|
54
|
+
end
|
55
|
+
|
56
|
+
def field(*args)
|
57
|
+
fields << (field = Field.new(*args))
|
58
|
+
attr_accessor(field.name)
|
59
|
+
end
|
60
|
+
|
61
|
+
def fields
|
62
|
+
@fields ||= []
|
63
|
+
end
|
64
|
+
|
65
|
+
def validations
|
66
|
+
@validations ||= []
|
67
|
+
end
|
68
|
+
|
69
|
+
def from_primitive(primitive)
|
70
|
+
Deserializer.deserialize(self, primitive)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
class Serializer
|
75
|
+
def self.serialize(object)
|
76
|
+
new.serialize(object)
|
77
|
+
end
|
78
|
+
|
79
|
+
def serialize(object)
|
80
|
+
case object
|
81
|
+
when DomainModel
|
82
|
+
serialize(object.attributes)
|
83
|
+
when Hash
|
84
|
+
object.each {|k,v| object[k] = serialize(v) }
|
85
|
+
when Array
|
86
|
+
object.map { |o| serialize(o) }
|
87
|
+
else
|
88
|
+
object
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
class Deserializer
|
94
|
+
def self.deserialize(type, primitive)
|
95
|
+
new.deserialize(type, primitive)
|
96
|
+
end
|
97
|
+
|
98
|
+
def deserialize(type, primitive)
|
99
|
+
case
|
100
|
+
when type <= DomainModel
|
101
|
+
primitive.each do |k, v|
|
102
|
+
field = type.fields.find { |f| f.name.to_s == k.to_s }
|
103
|
+
|
104
|
+
next unless field && field.monotype
|
105
|
+
|
106
|
+
if field.collection?
|
107
|
+
primitive[k] = v.map { |e| deserialize(field.monotype, e) }
|
108
|
+
else
|
109
|
+
primitive[k] = deserialize(field.monotype, v)
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
type.new(primitive)
|
114
|
+
else
|
115
|
+
primitive
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
class Field
|
121
|
+
attr_reader :name, :types
|
122
|
+
|
123
|
+
def initialize(name, options = {})
|
124
|
+
@name = name
|
125
|
+
@required = options.fetch(:required, false)
|
126
|
+
@collection = options.fetch(:collection, false)
|
127
|
+
@validate = options.fetch(:validate, false)
|
128
|
+
|
129
|
+
raw_type = options.fetch(:type, BasicObject)
|
130
|
+
@types = raw_type.is_a?(Module) ? [raw_type] : raw_type
|
131
|
+
|
132
|
+
if required? and collection?
|
133
|
+
raise ArgumentError, "fields cannot be both :collection and :required"
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
def errors(value)
|
138
|
+
Validator.errors(self, value)
|
139
|
+
end
|
140
|
+
|
141
|
+
def monotype
|
142
|
+
types.first if types.count == 1
|
143
|
+
end
|
144
|
+
|
145
|
+
def required?
|
146
|
+
!!@required
|
147
|
+
end
|
148
|
+
|
149
|
+
def collection?
|
150
|
+
!!@collection
|
151
|
+
end
|
152
|
+
|
153
|
+
def validate?
|
154
|
+
!!@validate
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
class ModelErrors
|
159
|
+
def initialize
|
160
|
+
@hash = Hash.new
|
161
|
+
end
|
162
|
+
|
163
|
+
def add(field_name, error)
|
164
|
+
@hash[field_name] ||= []
|
165
|
+
@hash[field_name] += Array(error)
|
166
|
+
end
|
167
|
+
|
168
|
+
def [](field_name)
|
169
|
+
@hash[field_name] || []
|
170
|
+
|
171
|
+
end
|
172
|
+
|
173
|
+
def empty?
|
174
|
+
@hash.values.flatten.empty?
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
class FieldErrors
|
179
|
+
def initialize(model_errors, field)
|
180
|
+
@model_errors, @field = model_errors, field
|
181
|
+
end
|
182
|
+
|
183
|
+
def add(error)
|
184
|
+
@model_errors.add(@field.name, error)
|
185
|
+
end
|
186
|
+
|
187
|
+
def empty?
|
188
|
+
@model_errors[@field.name].empty?
|
189
|
+
end
|
190
|
+
end
|
191
|
+
|
192
|
+
class Validation
|
193
|
+
def initialize(*args, &block)
|
194
|
+
@field_name = args[0] if args[0].is_a?(Symbol)
|
195
|
+
@options = args[0] if args[0].is_a?(Hash)
|
196
|
+
@options = args[1] if args[1].is_a?(Hash)
|
197
|
+
@options = {} if @options.nil?
|
198
|
+
|
199
|
+
@block = block
|
200
|
+
end
|
201
|
+
|
202
|
+
def execute(model, errors)
|
203
|
+
if global?
|
204
|
+
if always? or errors.empty?
|
205
|
+
model.instance_exec(errors, &@block)
|
206
|
+
end
|
207
|
+
else
|
208
|
+
field = model.class.fields.find { |f| f.name == @field_name}
|
209
|
+
raise("No field called #{@field_name}") if field.nil?
|
210
|
+
|
211
|
+
field_errors = FieldErrors.new(errors, field)
|
212
|
+
|
213
|
+
if always? or field_errors.empty?
|
214
|
+
model.instance_exec(field_errors, &@block)
|
215
|
+
end
|
216
|
+
end
|
217
|
+
end
|
218
|
+
|
219
|
+
def global?
|
220
|
+
@field_name.nil?
|
221
|
+
end
|
222
|
+
|
223
|
+
def always?
|
224
|
+
@options.fetch(:always, global?)
|
225
|
+
end
|
226
|
+
end
|
227
|
+
|
228
|
+
class Validator
|
229
|
+
def self.errors(field, value)
|
230
|
+
validator = field.collection? ? Collection : Scalar
|
231
|
+
validator.new(field, value).errors
|
232
|
+
end
|
233
|
+
|
234
|
+
private
|
235
|
+
|
236
|
+
attr_reader :field
|
237
|
+
|
238
|
+
def types
|
239
|
+
field.types
|
240
|
+
end
|
241
|
+
|
242
|
+
class Collection < Validator
|
243
|
+
def initialize(field, values)
|
244
|
+
@field, @values = field, values
|
245
|
+
end
|
246
|
+
|
247
|
+
def errors
|
248
|
+
case
|
249
|
+
when (not enumerable?)
|
250
|
+
["was declared as a collection and is not enumerable"]
|
251
|
+
when type_mismatch?
|
252
|
+
["contains a value that is not an instance of #{types.map(&:inspect).join(' or ')}"]
|
253
|
+
when transitively_invalid?
|
254
|
+
["is invalid"]
|
255
|
+
else
|
256
|
+
[]
|
257
|
+
end
|
258
|
+
end
|
259
|
+
|
260
|
+
private
|
261
|
+
|
262
|
+
attr_reader :values
|
263
|
+
|
264
|
+
def enumerable?
|
265
|
+
values.is_a?(Enumerable)
|
266
|
+
end
|
267
|
+
|
268
|
+
def type_mismatch?
|
269
|
+
values.any? do |value|
|
270
|
+
field.types.none? { |t| value.is_a?(t) }
|
271
|
+
end
|
272
|
+
end
|
273
|
+
|
274
|
+
def transitively_invalid?
|
275
|
+
field.validate? and values.any? { |v| not v.valid? }
|
276
|
+
end
|
277
|
+
end
|
278
|
+
|
279
|
+
class Scalar < Validator
|
280
|
+
def initialize(field, value)
|
281
|
+
@field, @value = field, value
|
282
|
+
end
|
283
|
+
|
284
|
+
def errors
|
285
|
+
case
|
286
|
+
when legitimately_empty?
|
287
|
+
[]
|
288
|
+
when (value.nil? and field.required?)
|
289
|
+
["cannot be nil"]
|
290
|
+
when type_mismatch?
|
291
|
+
["is not an instance of #{type_string} (was #{value.class.inspect})"]
|
292
|
+
when transitively_invalid?
|
293
|
+
["is invalid"]
|
294
|
+
else
|
295
|
+
[]
|
296
|
+
end
|
297
|
+
end
|
298
|
+
|
299
|
+
private
|
300
|
+
|
301
|
+
attr_reader :value
|
302
|
+
|
303
|
+
def type_mismatch?
|
304
|
+
types.none? { |t| value.is_a?(t) }
|
305
|
+
end
|
306
|
+
|
307
|
+
def type_string
|
308
|
+
types.map(&:inspect).join(' or ')
|
309
|
+
end
|
310
|
+
|
311
|
+
def legitimately_empty?
|
312
|
+
value.nil? and not field.required?
|
313
|
+
end
|
314
|
+
|
315
|
+
def transitively_invalid?
|
316
|
+
field.validate? and not value.valid?
|
317
|
+
end
|
318
|
+
end
|
319
|
+
end
|
320
|
+
end
|
321
|
+
|
322
|
+
require "domain_model/version"
|
data/spec/model_spec.rb
ADDED
@@ -0,0 +1,453 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe DomainModel do
|
4
|
+
describe ".field" do
|
5
|
+
it "creates a getter and setter for the field" do
|
6
|
+
define { field :name }
|
7
|
+
|
8
|
+
client = Client.new
|
9
|
+
client.name = "Rafer"
|
10
|
+
|
11
|
+
expect(client.name).to eq("Rafer")
|
12
|
+
end
|
13
|
+
|
14
|
+
it "defaults :collection fields to an empty array" do
|
15
|
+
define { field :name, :collection => true }
|
16
|
+
|
17
|
+
client = Client.new
|
18
|
+
expect(client.name).to eq([])
|
19
|
+
end
|
20
|
+
|
21
|
+
it "does not allow the :required attribute with :collection" do
|
22
|
+
required_collection = lambda do
|
23
|
+
define { field :name, :collection => true, :required => true }
|
24
|
+
end
|
25
|
+
|
26
|
+
expect(&required_collection).to raise_error(ArgumentError, /fields cannot be both :collection and :required/ )
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
describe ".validate" do
|
31
|
+
it "runs added validations each time #errors is called" do
|
32
|
+
run_count = 0
|
33
|
+
|
34
|
+
define do
|
35
|
+
validate { run_count += 1 }
|
36
|
+
end
|
37
|
+
|
38
|
+
client = Client.new
|
39
|
+
|
40
|
+
expect { client.errors }.to change { run_count }.to(1)
|
41
|
+
expect { client.errors }.to change { run_count }.to(2)
|
42
|
+
end
|
43
|
+
|
44
|
+
describe "with no field name" do
|
45
|
+
it "is passed the errors object" do
|
46
|
+
define do
|
47
|
+
validate { |e| e.add(:field, "ERROR") }
|
48
|
+
end
|
49
|
+
|
50
|
+
client = Client.new
|
51
|
+
expect(client.errors[:field] ).to include("ERROR")
|
52
|
+
end
|
53
|
+
|
54
|
+
it "is executed after the built in field validations" do
|
55
|
+
define do
|
56
|
+
field :field, :required => true
|
57
|
+
validate { |e| e.add(:field, "There were #{e[:field].size} errors") }
|
58
|
+
end
|
59
|
+
|
60
|
+
client = Client.new
|
61
|
+
expect(client.errors[:field]).to include("There were 1 errors")
|
62
|
+
end
|
63
|
+
|
64
|
+
it "is not executed if there are any errors on the model with :always => false" do
|
65
|
+
define do
|
66
|
+
field :field, :required => true
|
67
|
+
validate(:always => false) { |e| e.add(:field, "never happen") }
|
68
|
+
end
|
69
|
+
|
70
|
+
client = Client.new
|
71
|
+
expect(client.errors[:field]).to eq(["cannot be nil"])
|
72
|
+
end
|
73
|
+
|
74
|
+
it "is executed irrespetive of other errors with :always => true" do
|
75
|
+
define do
|
76
|
+
field :field, :required => true
|
77
|
+
validate(:always => true) { |e| e.add(:field, "should happen") }
|
78
|
+
end
|
79
|
+
|
80
|
+
client = Client.new
|
81
|
+
expect(client.errors[:field]).to eq(["cannot be nil", "should happen"])
|
82
|
+
end
|
83
|
+
|
84
|
+
it "defaults :always to true" do
|
85
|
+
define do
|
86
|
+
field :field, :required => true
|
87
|
+
validate { |e| e.add(:field, "should happen") }
|
88
|
+
end
|
89
|
+
|
90
|
+
client = Client.new
|
91
|
+
expect(client.errors[:field]).to eq(["cannot be nil", "should happen"])
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
describe "with a field name" do
|
96
|
+
it "is passed an errors object for that field" do
|
97
|
+
define do
|
98
|
+
field :field
|
99
|
+
validate(:field) { |e| e.add("is not great")}
|
100
|
+
end
|
101
|
+
|
102
|
+
client = Client.new
|
103
|
+
expect(client.errors[:field]).to include("is not great")
|
104
|
+
end
|
105
|
+
|
106
|
+
it "is not executed if there are already errors on the specified field with :always => false" do
|
107
|
+
define do
|
108
|
+
field :field, :required => true
|
109
|
+
validate(:field, :always => false) { |e| e.add("never happen") }
|
110
|
+
end
|
111
|
+
|
112
|
+
client = Client.new
|
113
|
+
expect(client.errors[:field]).to eq(["cannot be nil"])
|
114
|
+
end
|
115
|
+
|
116
|
+
it "is executed irrespective of field errors with :always => true" do
|
117
|
+
define do
|
118
|
+
field :field, :required => true
|
119
|
+
validate(:field, :always => true) { |e| e.add("should happen") }
|
120
|
+
end
|
121
|
+
|
122
|
+
client = Client.new
|
123
|
+
expect(client.errors[:field]).to eq(["cannot be nil", "should happen"])
|
124
|
+
end
|
125
|
+
|
126
|
+
it "defaults :always to false" do
|
127
|
+
define do
|
128
|
+
field :field, :required => true
|
129
|
+
validate(:field) { |e| e.add("never happen") }
|
130
|
+
end
|
131
|
+
|
132
|
+
client = Client.new
|
133
|
+
expect(client.errors[:field]).to eq(["cannot be nil"])
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
describe ".new" do
|
139
|
+
before do
|
140
|
+
define do
|
141
|
+
field :name
|
142
|
+
field :children, :collection => true
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
it "accepts fields" do
|
147
|
+
client = Client.new(:name => "Rafer")
|
148
|
+
expect(client.name).to eq("Rafer")
|
149
|
+
end
|
150
|
+
|
151
|
+
it "accepts fields with string keys" do
|
152
|
+
client = Client.new("name" => "Rafer")
|
153
|
+
expect(client.name).to eq("Rafer")
|
154
|
+
end
|
155
|
+
|
156
|
+
it "accepts collection fields" do
|
157
|
+
client = Client.new(:children => ["child"])
|
158
|
+
expect(client.children).to eq(["child"])
|
159
|
+
end
|
160
|
+
|
161
|
+
it "accepts collection fields with string keys" do
|
162
|
+
client = Client.new("children" => ["child"])
|
163
|
+
expect(client.children).to eq(["child"])
|
164
|
+
end
|
165
|
+
|
166
|
+
it "defaults collection fields to an empty array" do
|
167
|
+
client = Client.new
|
168
|
+
expect(client.children).to eq([])
|
169
|
+
end
|
170
|
+
|
171
|
+
it "raises an exception for unrecognized parmeters" do
|
172
|
+
expect { Client.new(:wrong => "") }.to raise_error(NoMethodError)
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
describe ".errors" do
|
177
|
+
it "is empty when no fields or validations have been defined" do
|
178
|
+
define { }
|
179
|
+
expect(Client.new.errors).to be_empty
|
180
|
+
end
|
181
|
+
|
182
|
+
it "includes errors for incorrect types" do
|
183
|
+
define { field :field, :type => String }
|
184
|
+
client = Client.new(:field => :wrong)
|
185
|
+
|
186
|
+
expect(client.errors[:field]).to include("is not an instance of String (was Symbol)")
|
187
|
+
end
|
188
|
+
|
189
|
+
it "doesn't include errors for correct types" do
|
190
|
+
define { field :field, :type => String }
|
191
|
+
client = Client.new(:field => "right")
|
192
|
+
|
193
|
+
expect(client.errors[:field]).to eq([])
|
194
|
+
end
|
195
|
+
|
196
|
+
it "doesn't include errors for correct subtypes" do
|
197
|
+
define { field :field, :type => Numeric }
|
198
|
+
client = Client.new(:field => 1)
|
199
|
+
|
200
|
+
expect(client.errors[:field]).to eq([])
|
201
|
+
end
|
202
|
+
|
203
|
+
it "includes an error for required fields that are nil" do
|
204
|
+
define { field :field, :required => true }
|
205
|
+
client = Client.new(:field => nil)
|
206
|
+
|
207
|
+
expect(client.errors[:field]).to include("cannot be nil")
|
208
|
+
end
|
209
|
+
|
210
|
+
it "doesn't include errors for non-required, typed fields" do
|
211
|
+
define { field :field, :type => String }
|
212
|
+
|
213
|
+
expect(Client.new.errors[:field]).to eq([])
|
214
|
+
end
|
215
|
+
|
216
|
+
it "includes errors for incorrect types (when multiple are specified)" do
|
217
|
+
define { field :field, :type => [String, Symbol] }
|
218
|
+
client = Client.new(:field => 1)
|
219
|
+
|
220
|
+
expect(client.errors[:field]).to include("is not an instance of String or Symbol (was Fixnum)")
|
221
|
+
end
|
222
|
+
|
223
|
+
it "includes no errors for correct types (when multiple types are specified)" do
|
224
|
+
define { field :field, :type => [String, Symbol] }
|
225
|
+
|
226
|
+
expect(Client.new(:field => "right").errors[:field]).to eq([])
|
227
|
+
expect(Client.new(:field => :right).errors[:field]).to eq([])
|
228
|
+
end
|
229
|
+
|
230
|
+
it "includes only the 'empty' error for fields that are required and typed, with a nil value" do
|
231
|
+
define { field :field, :type => String, :required => true }
|
232
|
+
client = Client.new(:field => nil)
|
233
|
+
|
234
|
+
expect(client.errors[:field]).to include("cannot be nil")
|
235
|
+
end
|
236
|
+
|
237
|
+
it "includes errors for invalid collaborators (when :validate is specified)" do
|
238
|
+
define { field :field, :validate => true }
|
239
|
+
client = Client.new(:field => double(:valid? => false))
|
240
|
+
|
241
|
+
expect(client.errors[:field]).to include("is invalid")
|
242
|
+
end
|
243
|
+
|
244
|
+
it "doesn't includes errors for valid collaborators (when :validate is specified)" do
|
245
|
+
define { field :field, :validate => true }
|
246
|
+
client = Client.new(:field => double(:valid? => true))
|
247
|
+
|
248
|
+
expect(client.errors[:field]).to eq([])
|
249
|
+
end
|
250
|
+
|
251
|
+
it "doesn't validate collaborators if the type is incorrect" do
|
252
|
+
define { field :field, :type => String, :validate => true }
|
253
|
+
|
254
|
+
collaborator = double
|
255
|
+
client = Client.new(:field => collaborator)
|
256
|
+
|
257
|
+
expect(collaborator).not_to receive(:valid?)
|
258
|
+
|
259
|
+
client.errors
|
260
|
+
end
|
261
|
+
|
262
|
+
describe "collections" do
|
263
|
+
it "includes an error if the value is not enumerable" do
|
264
|
+
define { field :field, :type => String, :collection => true }
|
265
|
+
client = Client.new(:field => 1)
|
266
|
+
|
267
|
+
expect(client.errors[:field]).to include("was declared as a collection and is not enumerable")
|
268
|
+
end
|
269
|
+
|
270
|
+
it "doesn't include errors if there are no values" do
|
271
|
+
define { field :field, :type => String, :collection => true }
|
272
|
+
client = Client.new
|
273
|
+
|
274
|
+
expect(client.errors[:field]).to eq([])
|
275
|
+
end
|
276
|
+
|
277
|
+
it "includes errors for incorrect types" do
|
278
|
+
define { field :field, :type => String, :collection => true }
|
279
|
+
client = Client.new(:field => [:wrong])
|
280
|
+
|
281
|
+
expect(client.errors[:field]).to include("contains a value that is not an instance of String")
|
282
|
+
end
|
283
|
+
|
284
|
+
it "includes errors for incorrect types (multiple types)" do
|
285
|
+
define { field :field, :type => [String, Symbol], :collection => true }
|
286
|
+
client = Client.new(:field => [1])
|
287
|
+
|
288
|
+
expect(client.errors[:field]).to include("contains a value that is not an instance of String or Symbol")
|
289
|
+
end
|
290
|
+
|
291
|
+
it "doesn't include errors for correctly typed values" do
|
292
|
+
define { field :field, :type => String, :collection => true }
|
293
|
+
client = Client.new(:field => ["right", "right"])
|
294
|
+
|
295
|
+
expect(client.errors[:field]).to eq([])
|
296
|
+
end
|
297
|
+
|
298
|
+
it "doesn't include errors for correctly typed values (multiple types)" do
|
299
|
+
define { field :field, :type => [String, Symbol], :collection => true }
|
300
|
+
client = Client.new(:field => ["right", :right])
|
301
|
+
|
302
|
+
expect(client.errors[:field]).to eq([])
|
303
|
+
end
|
304
|
+
|
305
|
+
it "includes errors for invalid collaborators (when :validate is specified)" do
|
306
|
+
define { field :field, :validate => true, :collection => true }
|
307
|
+
client = Client.new(:field => [double(:valid? => false)])
|
308
|
+
|
309
|
+
expect(client.errors[:field]).to include("is invalid")
|
310
|
+
end
|
311
|
+
|
312
|
+
it "doesn't include errors for valid collaborators (when :validate is specified)" do
|
313
|
+
define { field :field, :validate => true, :collection => true }
|
314
|
+
client = Client.new(:field => [double(:valid? => true)])
|
315
|
+
|
316
|
+
expect(client.errors[:field]).to eq([])
|
317
|
+
end
|
318
|
+
|
319
|
+
it "doesn't validate collaborators if the type is incorrect" do
|
320
|
+
define { field :field, :type => String, :validate => true, :collection => true }
|
321
|
+
|
322
|
+
collaborator = double
|
323
|
+
client = Client.new(:field => [collaborator])
|
324
|
+
|
325
|
+
expect(collaborator).not_to receive(:valid?)
|
326
|
+
|
327
|
+
client.errors
|
328
|
+
end
|
329
|
+
end
|
330
|
+
end
|
331
|
+
|
332
|
+
describe "#==" do
|
333
|
+
before { define { field :field } }
|
334
|
+
|
335
|
+
it "is true if all fields are equal" do
|
336
|
+
client_1 = Client.new(:field => "A")
|
337
|
+
client_2 = Client.new(:field => "A")
|
338
|
+
|
339
|
+
expect(client_1).to eq(client_2)
|
340
|
+
end
|
341
|
+
|
342
|
+
it "is false if any field is different" do
|
343
|
+
client_1 = Client.new(:field => "A")
|
344
|
+
client_2 = Client.new(:field => "B")
|
345
|
+
|
346
|
+
expect(client_1).not_to eq(client_2)
|
347
|
+
end
|
348
|
+
|
349
|
+
it "is false if the object is of another type" do
|
350
|
+
expect(Client.new).not_to eq(double)
|
351
|
+
end
|
352
|
+
end
|
353
|
+
|
354
|
+
describe "#inspect" do
|
355
|
+
before { define { field :field } }
|
356
|
+
|
357
|
+
it "shows the name and value of all fields" do
|
358
|
+
client = Client.new(:field => "VALUE")
|
359
|
+
expect(client.inspect).to match(/field: "VALUE"/)
|
360
|
+
end
|
361
|
+
end
|
362
|
+
|
363
|
+
describe "#valid?" do
|
364
|
+
it "is true if there are no errors" do
|
365
|
+
define { field :field }
|
366
|
+
expect(Client.new.valid?).to be(true)
|
367
|
+
end
|
368
|
+
|
369
|
+
it "is false if there are errors" do
|
370
|
+
define { field :field, :required => true }
|
371
|
+
expect(Client.new.valid?).to be(false)
|
372
|
+
end
|
373
|
+
end
|
374
|
+
|
375
|
+
describe "#attributes" do
|
376
|
+
it "returns the models as a hash" do
|
377
|
+
define { field :field }
|
378
|
+
client = Client.new(:field => "VALUE")
|
379
|
+
|
380
|
+
expect(client.attributes).to eq({:field => "VALUE"})
|
381
|
+
end
|
382
|
+
end
|
383
|
+
|
384
|
+
describe "#to_primitive" do
|
385
|
+
class Child
|
386
|
+
include DomainModel
|
387
|
+
field :field
|
388
|
+
end
|
389
|
+
|
390
|
+
it "returns a hash of the field's attribtues" do
|
391
|
+
define { field :field }
|
392
|
+
client = Client.new(:field => "VALUE")
|
393
|
+
|
394
|
+
expect(client.to_primitive).to eq({:field => "VALUE"})
|
395
|
+
end
|
396
|
+
|
397
|
+
it "converts referenced models" do
|
398
|
+
define do
|
399
|
+
field :child, :type => Child
|
400
|
+
end
|
401
|
+
client = Client.new(:child => Child.new(:field => "VALUE"))
|
402
|
+
|
403
|
+
expect(client.to_primitive).to eq(:child => {:field => "VALUE"})
|
404
|
+
end
|
405
|
+
|
406
|
+
it "converts collection models" do
|
407
|
+
define { field :children, :collection => true }
|
408
|
+
client = Client.new(:children => [Child.new(:field => "VALUE")])
|
409
|
+
|
410
|
+
expect(client.to_primitive).to eq(:children => [ :field => "VALUE" ])
|
411
|
+
end
|
412
|
+
end
|
413
|
+
|
414
|
+
describe ".from_primitive" do
|
415
|
+
class Child
|
416
|
+
include DomainModel
|
417
|
+
field :field
|
418
|
+
end
|
419
|
+
|
420
|
+
it "parses simple fields " do
|
421
|
+
define { field :field }
|
422
|
+
|
423
|
+
client = Client.from_primitive({:field => "VALUE"})
|
424
|
+
expect(client.field).to eq("VALUE")
|
425
|
+
end
|
426
|
+
|
427
|
+
it "parses referenced DomainModels" do
|
428
|
+
define { field :child, :type => Child }
|
429
|
+
|
430
|
+
client = Client.from_primitive(:child => {:field => "VALUE"})
|
431
|
+
child = client.child
|
432
|
+
|
433
|
+
expect(child).to eq(Child.new(:field => "VALUE"))
|
434
|
+
end
|
435
|
+
|
436
|
+
it "parses collection DomainModels" do
|
437
|
+
define { field :children, :type => Child, :collection => true }
|
438
|
+
|
439
|
+
client = Client.from_primitive(:children => [{:field => "VALUE"}] )
|
440
|
+
children = client.children
|
441
|
+
|
442
|
+
expect(children).to eq([Child.new(:field => "VALUE")])
|
443
|
+
end
|
444
|
+
end
|
445
|
+
|
446
|
+
def define(&block)
|
447
|
+
client = Class.new do
|
448
|
+
include DomainModel
|
449
|
+
instance_eval(&block)
|
450
|
+
end
|
451
|
+
stub_const("Client", client)
|
452
|
+
end
|
453
|
+
end
|
data/spec/spec_helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,106 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: domain_model
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Rafer Hazen
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2014-02-18 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: bundler
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ~>
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '1.3'
|
22
|
+
type: :development
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ~>
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '1.3'
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: rake
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ! '>='
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: '0'
|
38
|
+
type: :development
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ! '>='
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: '0'
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
name: rspec
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
51
|
+
- - ~>
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '2.14'
|
54
|
+
type: :development
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ~>
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '2.14'
|
62
|
+
description: Minimal framework for definition of type-aware domain models
|
63
|
+
email:
|
64
|
+
- rafer@ralua.com
|
65
|
+
executables: []
|
66
|
+
extensions: []
|
67
|
+
extra_rdoc_files: []
|
68
|
+
files:
|
69
|
+
- .gitignore
|
70
|
+
- Gemfile
|
71
|
+
- LICENSE.txt
|
72
|
+
- README.md
|
73
|
+
- Rakefile
|
74
|
+
- domain_model.gemspec
|
75
|
+
- lib/domain_model.rb
|
76
|
+
- lib/domain_model/version.rb
|
77
|
+
- spec/model_spec.rb
|
78
|
+
- spec/spec_helper.rb
|
79
|
+
homepage: ''
|
80
|
+
licenses:
|
81
|
+
- MIT
|
82
|
+
post_install_message:
|
83
|
+
rdoc_options: []
|
84
|
+
require_paths:
|
85
|
+
- lib
|
86
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
87
|
+
none: false
|
88
|
+
requirements:
|
89
|
+
- - ! '>='
|
90
|
+
- !ruby/object:Gem::Version
|
91
|
+
version: '0'
|
92
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
93
|
+
none: false
|
94
|
+
requirements:
|
95
|
+
- - ! '>='
|
96
|
+
- !ruby/object:Gem::Version
|
97
|
+
version: '0'
|
98
|
+
requirements: []
|
99
|
+
rubyforge_project:
|
100
|
+
rubygems_version: 1.8.23
|
101
|
+
signing_key:
|
102
|
+
specification_version: 3
|
103
|
+
summary: Minimal framework for definition of type-aware domain models
|
104
|
+
test_files:
|
105
|
+
- spec/model_spec.rb
|
106
|
+
- spec/spec_helper.rb
|