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 ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in model.gemspec
4
+ gemspec
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,8 @@
1
+ require "bundler/gem_tasks"
2
+
3
+ task :default => :test
4
+ task :spec => :test
5
+
6
+ task :test do
7
+ sh("bundle exec rspec spec")
8
+ end
@@ -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
@@ -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"
@@ -0,0 +1,3 @@
1
+ module DomainModel
2
+ VERSION = "0.1.0"
3
+ end
@@ -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
@@ -0,0 +1,2 @@
1
+ require "bundler/setup"
2
+ require "domain_model"
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