domain_model 0.1.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.
- 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
|