gorillib-model 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. data/.gitignore +4 -0
  2. data/Gemfile +12 -0
  3. data/README.md +21 -0
  4. data/Rakefile +15 -0
  5. data/gorillib-model.gemspec +27 -0
  6. data/lib/gorillib/builder.rb +239 -0
  7. data/lib/gorillib/core_ext/datetime.rb +23 -0
  8. data/lib/gorillib/core_ext/exception.rb +153 -0
  9. data/lib/gorillib/core_ext/module.rb +10 -0
  10. data/lib/gorillib/core_ext/object.rb +14 -0
  11. data/lib/gorillib/model/base.rb +273 -0
  12. data/lib/gorillib/model/collection/model_collection.rb +157 -0
  13. data/lib/gorillib/model/collection.rb +200 -0
  14. data/lib/gorillib/model/defaults.rb +115 -0
  15. data/lib/gorillib/model/errors.rb +24 -0
  16. data/lib/gorillib/model/factories.rb +555 -0
  17. data/lib/gorillib/model/field.rb +168 -0
  18. data/lib/gorillib/model/lint.rb +24 -0
  19. data/lib/gorillib/model/named_schema.rb +53 -0
  20. data/lib/gorillib/model/positional_fields.rb +35 -0
  21. data/lib/gorillib/model/schema_magic.rb +163 -0
  22. data/lib/gorillib/model/serialization/csv.rb +60 -0
  23. data/lib/gorillib/model/serialization/json.rb +44 -0
  24. data/lib/gorillib/model/serialization/lines.rb +30 -0
  25. data/lib/gorillib/model/serialization/to_wire.rb +54 -0
  26. data/lib/gorillib/model/serialization/tsv.rb +53 -0
  27. data/lib/gorillib/model/serialization.rb +41 -0
  28. data/lib/gorillib/model/type/extended.rb +83 -0
  29. data/lib/gorillib/model/type/ip_address.rb +153 -0
  30. data/lib/gorillib/model/type/url.rb +11 -0
  31. data/lib/gorillib/model/validate.rb +22 -0
  32. data/lib/gorillib/model/version.rb +5 -0
  33. data/lib/gorillib/model.rb +34 -0
  34. data/spec/builder_spec.rb +193 -0
  35. data/spec/core_ext/datetime_spec.rb +41 -0
  36. data/spec/core_ext/exception.rb +98 -0
  37. data/spec/core_ext/object.rb +45 -0
  38. data/spec/model/collection_spec.rb +290 -0
  39. data/spec/model/defaults_spec.rb +104 -0
  40. data/spec/model/factories_spec.rb +323 -0
  41. data/spec/model/lint_spec.rb +28 -0
  42. data/spec/model/serialization/csv_spec.rb +30 -0
  43. data/spec/model/serialization/tsv_spec.rb +28 -0
  44. data/spec/model/serialization_spec.rb +41 -0
  45. data/spec/model/type/extended_spec.rb +166 -0
  46. data/spec/model/type/ip_address_spec.rb +141 -0
  47. data/spec/model_spec.rb +261 -0
  48. data/spec/spec_helper.rb +15 -0
  49. data/spec/support/capture_output.rb +28 -0
  50. data/spec/support/nuke_constants.rb +9 -0
  51. data/spec/support/shared_context_for_builders.rb +59 -0
  52. data/spec/support/shared_context_for_models.rb +55 -0
  53. data/spec/support/shared_examples_for_factories.rb +71 -0
  54. data/spec/support/shared_examples_for_model_fields.rb +62 -0
  55. data/spec/support/shared_examples_for_models.rb +87 -0
  56. metadata +193 -0
@@ -0,0 +1,45 @@
1
+ require 'spec_helper'
2
+
3
+ describe 'try_dup', :simple_spec => true do
4
+ it "returns a duplicate version on regular objects" do
5
+ obj = Object.new
6
+ oth = obj.try_dup
7
+ obj.should_not === oth
8
+ end
9
+
10
+ it "returns self on Numerics" do
11
+ obj = 12
12
+ oth = obj.try_dup
13
+ obj.should === oth
14
+ end
15
+
16
+ it "returns self on Symbols" do
17
+ obj = :test
18
+ oth = obj.try_dup
19
+ obj.should === oth
20
+ end
21
+
22
+ it "returns self on true" do
23
+ obj = true
24
+ oth = obj.try_dup
25
+ obj.should === oth
26
+ end
27
+
28
+ it "returns self on false" do
29
+ obj = false
30
+ oth = obj.try_dup
31
+ obj.should === oth
32
+ end
33
+
34
+ it "returns self on nil" do
35
+ obj = nil
36
+ oth = obj.try_dup
37
+ obj.should === oth
38
+ end
39
+
40
+ it "returns self on modules" do
41
+ obj = Module.new
42
+ oth = obj.try_dup
43
+ obj.object_id.should == oth.object_id
44
+ end
45
+ end
@@ -0,0 +1,290 @@
1
+ require 'spec_helper'
2
+
3
+ require 'gorillib/model/collection/model_collection'
4
+
5
+ shared_examples_for 'a collection' do |options={}|
6
+ subject{ string_collection }
7
+
8
+ context '.receive' do
9
+ it 'makes a new collection, has it #receive! the cargo, returns it' do
10
+ mock_collection = double('collection')
11
+ mock_cargo = double('cargo')
12
+ mock_args = {key_method: double, item_type: double}
13
+ described_class.should_receive(:new).with(mock_args).and_return(mock_collection)
14
+ mock_collection.should_receive(:receive!).with(mock_cargo)
15
+ described_class.receive(mock_cargo, mock_args).should == mock_collection
16
+ end
17
+ end
18
+
19
+ context 'empty collection' do
20
+ subject{ described_class.new }
21
+ its(:length){ should == 0 }
22
+ its(:size ){ should == 0 }
23
+ its(:empty?){ should be true }
24
+ its(:blank?){ should be true }
25
+ its(:values){ should == [] }
26
+ its(:to_a ){ should == [] }
27
+ end
28
+ context 'non-empty collection' do
29
+ subject{ string_collection }
30
+ its(:length){ should == 4 }
31
+ its(:size ){ should == 4 }
32
+ its(:empty?){ should be false }
33
+ its(:blank?){ should be false }
34
+ end
35
+
36
+ context '#values returns an array' do
37
+ its(:values){ should == %w[wocket in my pocket] }
38
+ end
39
+ context '#to_a returns same as values' do
40
+ its(:to_a ){ should == %w[wocket in my pocket] }
41
+ end
42
+
43
+ context '#each_value' do
44
+ it 'each value in order' do
45
+ result = []
46
+ ret = subject.each_value{|val| result << val.reverse }
47
+ result.should == %w[tekcow ni ym tekcop]
48
+ end
49
+ end
50
+
51
+ unless (options[:receiving_arrays] == :skip)
52
+ context '#receive (array)', :if => :receives_arrays do
53
+ it 'adopts the contents of an array' do
54
+ string_collection.receive!(%w[horton hears a who])
55
+ string_collection.values.should == %w[wocket in my pocket horton hears a who]
56
+ end
57
+ it 'does not adopt duplicates' do
58
+ string_collection.receive!(%w[red fish blue fish])
59
+ string_collection.values.should == %w[wocket in my pocket red fish blue]
60
+ end
61
+ end
62
+ end
63
+
64
+ context '#receive (hash)' do
65
+ it 'adopts the values of a hash' do
66
+ string_collection.receive!({horton: "horton", hears: "hears", a: "a", who: "who" })
67
+ string_collection.values.should == %w[wocket in my pocket horton hears a who]
68
+ end
69
+ it 'does not adopt duplicates' do
70
+ string_collection.receive!({red: 'red', fish: 'fish'})
71
+ string_collection.receive!({blue: 'blue', fish: 'fish'})
72
+ string_collection.values.should == %w[wocket in my pocket red fish blue]
73
+ end
74
+ end
75
+
76
+ end
77
+
78
+ shared_examples_for 'a keyed collection' do
79
+ subject{ string_collection }
80
+
81
+ context '#[]' do
82
+ it 'retrieves stored objects' do
83
+ subject[1] = mock_val
84
+ subject[1].should equal(mock_val)
85
+ end
86
+ end
87
+
88
+ context '#fetch' do
89
+ let(:absent_key_err_re){ /(key not found: 69|index 69 outside)/ }
90
+ # ABSENT_KEY_ERROR_RE = /(key not found: 69|index 69 outside)/
91
+ it 'retrieves an object if present' do
92
+ subject[1] = mock_val
93
+ subject.fetch(1).should equal(mock_val)
94
+ end
95
+ it 'if absent and block given: calls block with label, returning its value' do
96
+ got_here = nil
97
+ subject.fetch(69){ got_here = 'yup' ; mock_val }.should equal(mock_val)
98
+ got_here.should == 'yup'
99
+ end
100
+ it 'if absent and no block given: raises an error' do
101
+ ->{ subject.fetch(69) }.should raise_error IndexError, absent_key_err_re# ABSENT_KEY_ERROR_RE
102
+ end
103
+ end
104
+
105
+ context '#delete' do
106
+ it 'retrieves an object if present' do
107
+ subject[1] = mock_val
108
+ subject.delete(1).should equal(mock_val)
109
+ subject.values.should_not include(mock_val)
110
+ end
111
+ it 'if absent and block given: calls block with label, returning its value' do
112
+ got_here = nil
113
+ subject.delete(69){ got_here = 'yup' ; mock_val }.should equal(mock_val)
114
+ got_here.should == 'yup'
115
+ end
116
+ it 'if absent and no block given: returns nil' do
117
+ subject.delete(69).should be nil
118
+ end
119
+ end
120
+ end
121
+
122
+ shared_examples_for 'an auto-keyed collection' do
123
+ subject{ string_collection }
124
+
125
+ it 'retrieves things by their label' do
126
+ string_collection[:pocket].should == "pocket"
127
+ string_collection['pocket'].should == nil
128
+ shouty_collection['POCKET'].should == "pocket"
129
+ shouty_collection['pocket'].should == nil
130
+ end
131
+
132
+ it 'gets label from key if none supplied' do
133
+ string_collection[:marvin].should be nil
134
+ string_collection << 'marvin'
135
+ string_collection[:marvin].should == 'marvin'
136
+ shouty_collection << 'marvin'
137
+ shouty_collection['MARVIN'].should == 'marvin'
138
+ end
139
+
140
+ context '#receive!' do
141
+ it 'extracts labels given an array' do
142
+ subject.receive!(%w[horton hears a who])
143
+ subject[:horton].should == 'horton'
144
+ end
145
+ it 'replaces labels in-place, preserving order' do
146
+ shouty_collection.receive!(%w[in MY pocKET wocKET])
147
+ shouty_collection['WOCKET'].should == 'wocKET'
148
+ shouty_collection.values.should == %w[wocKET in MY pocKET]
149
+ end
150
+ end
151
+
152
+ context '#<<' do
153
+ it 'adds element under its natural label, at end' do
154
+ subject << 'marvin'
155
+ subject.values.last.should == 'marvin'
156
+ subject[:marvin].should == 'marvin'
157
+ end
158
+ it 'replaces duplicate values' do
159
+ val = 'wocKET'
160
+ shouty_collection['WOCKET'].should == 'wocket'
161
+ shouty_collection['WOCKET'].should_not equal(val)
162
+ shouty_collection << val
163
+ shouty_collection['WOCKET'].should == 'wocKET'
164
+ shouty_collection['WOCKET'].should equal(val)
165
+ end
166
+ end
167
+
168
+ end
169
+
170
+ describe 'collections:', :model_spec => true, :collection_spec => true do
171
+ let(:symbolized_test_items){ {:wocket => 'wocket', :in => 'in', :my => 'my', :pocket => 'pocket'} }
172
+ let(:capitalized_test_items){ {'WOCKET' => 'wocket', 'IN' => 'in', 'MY' => 'my', 'POCKET' => 'pocket'} }
173
+
174
+ describe Gorillib::Collection do
175
+ context 'with no key_method (only explicit labels can be stored)' do
176
+ let(:string_collection){ described_class.receive(symbolized_test_items) }
177
+ let(:shouty_collection){ described_class.receive(capitalized_test_items) }
178
+ it_behaves_like 'a collection', :receiving_arrays => :skip
179
+ it_behaves_like 'a keyed collection'
180
+ end
181
+
182
+ context 'with no key_method (only explicit labels can be stored)' do
183
+ let(:string_collection){ described_class.receive(symbolized_test_items, key_method: :to_sym) }
184
+ let(:shouty_collection){ described_class.receive(capitalized_test_items, key_method: :upcase) }
185
+ it_behaves_like 'a collection', :receiving_arrays => true
186
+ it_behaves_like 'an auto-keyed collection'
187
+ end
188
+ end
189
+
190
+ describe Gorillib::ModelCollection, :model_spec => true do
191
+ context do
192
+ let(:string_collection){ described_class.receive(%w[wocket in my pocket], key_method: :to_sym, item_type: String) }
193
+ let(:shouty_collection){ described_class.receive(%w[wocket in my pocket], key_method: :upcase, item_type: String) }
194
+ it_behaves_like 'a collection'
195
+ it_behaves_like 'a keyed collection'
196
+ it_behaves_like 'an auto-keyed collection'
197
+ end
198
+
199
+ subject{ smurf_collection }
200
+ let(:smurf_collection){ described_class.receive([], key_method: :name, item_type: smurf_class) }
201
+ let(:test_item){ papa_smurf }
202
+ let(:test_attrs){ test_item.attributes }
203
+ let(:mock_factory){ mf = double('factory'); mf.stub(:native? => true) ; mf }
204
+
205
+ context '#receive_item' do
206
+ before do
207
+ @test_proc = ->{ 'test' };
208
+ subject.stub(item_type: mock_factory)
209
+ end
210
+
211
+ it 'sends it to item_type for receive, then adds it' do
212
+ mock_factory.should_receive(:receive).with(mock_val, &@test_proc).and_return(mock_val)
213
+ subject.should_receive(:add).with(mock_val, nil)
214
+ subject.receive_item(nil, mock_val, &@test_proc)
215
+ end
216
+ it 'accepts an explicit label' do
217
+ mock_factory.should_receive(:receive).with(mock_val, &@test_proc).and_return(mock_val)
218
+ subject.should_receive(:add).with(mock_val, 'truffula')
219
+ subject.receive_item('truffula', mock_val, &@test_proc)
220
+ end
221
+ end
222
+
223
+ context '#update_or_add' do
224
+ it "if absent, creates item with given attrs" do
225
+ test_proc = ->{ 'test' };
226
+ subject.should_receive(:receive_item).with('truffula', test_attrs.merge(name: 'truffula'), &test_proc).and_return(test_item)
227
+ #
228
+ subject.update_or_add('truffula', test_attrs, &test_proc)
229
+ end
230
+ it "if there's no key_method, does not it as an attr to create" do
231
+ subject.send(:remove_instance_variable, '@key_method')
232
+ subject.should_receive(:receive_item).with('truffula', test_attrs)
233
+ #
234
+ subject.update_or_add('truffula', test_attrs)
235
+ end
236
+ it "if present, updates item with attrs" do
237
+ test_proc = ->{ 'test' };
238
+ hsh = { :smurfiness => 99 }
239
+ subject['truffula'] = test_item
240
+ test_item.should_receive(:receive!).with(hsh, &test_proc)
241
+ #
242
+ subject.update_or_add('truffula', hsh, &test_proc)
243
+ end
244
+ it "returns item" do
245
+ updated_item = test_item.dup ; updated_item.name = 'truffula'
246
+ subject.update_or_add('truffula', test_attrs).should == updated_item
247
+ subject.update_or_add('truffula', test_item ).should == test_item
248
+ end
249
+ it "returns item" do
250
+ updated_item = test_item.dup ; updated_item.name = 'truffula'
251
+ subject.update_or_add('truffula', test_item ).should == updated_item
252
+ end
253
+ it "adds item to collection" do
254
+ updated_item = test_item.dup ; updated_item.name = 'truffula'
255
+ subject.update_or_add('truffula', test_attrs)
256
+ subject['truffula'].should == updated_item
257
+ end
258
+ end
259
+ end
260
+
261
+
262
+ describe Gorillib::Model do
263
+ describe '.collection' do
264
+ let(:described_class){ smurf_village_class }
265
+ subject{ described_class.new(name: :smurf_town) }
266
+ before do
267
+ smurf_collection_class ; smurf_village_class
268
+ smurf_class.field :village, smurf_village_class
269
+ end
270
+
271
+ it 'adds an eponymous field' do
272
+ described_class.should have_field(:smurfs)
273
+ end
274
+
275
+ it 'sets a default that auto-vivifies the collection field' do
276
+ subject.smurfs.should be_a(Gorillib::ModelCollection)
277
+ subject.smurfs.belongs_to.should == subject
278
+ end
279
+
280
+ it 'receives' do
281
+ subject = smurf_village_class.receive({name: :smurf_town,
282
+ smurfs: [
283
+ { name: 'whatever_smurf', smurfiness: 20},
284
+ ]})
285
+ subject.smurfs['whatever_smurf'].village.should == subject
286
+ end
287
+ end
288
+
289
+ end
290
+ end
@@ -0,0 +1,104 @@
1
+ require 'spec_helper'
2
+
3
+ module Gorillib::Test ; end
4
+ module Meta::Gorillib::Test ; end
5
+
6
+ describe Gorillib::Model, :model_spec => true do
7
+ after(:each){ Gorillib::Test.nuke_constants ; Meta::Gorillib::Test.nuke_constants }
8
+
9
+ let(:car_class) do
10
+ class Gorillib::Test::Car
11
+ include Gorillib::Model
12
+ field :name, Symbol
13
+ field :make_model, String
14
+ field :year, Integer
15
+ field :style, Symbol, :default => :sedan
16
+ field :doors, Integer,
17
+ :default => ->{ [:coupe, :convertible].include?(style) ? 2 : 4 }
18
+ self
19
+ end
20
+ Gorillib::Test::Car
21
+ end
22
+ let(:wildcat) do
23
+ car_class.receive( :name => :wildcat, :make_model => 'Buick Wildcat', :year => 1968, :doors => 2 )
24
+ end
25
+ let(:ford_39) do
26
+ car_class.receive( :name => :ford_39 )
27
+ end
28
+ let(:year_field ){ car_class.fields[:year] }
29
+ let(:doors_field){ car_class.fields[:doors] }
30
+ let(:style_field){ car_class.fields[:style] }
31
+
32
+ describe 'Field#default' do
33
+ it 'is itself a field on Gorillib::Model::Field (boy that is confusing)' do
34
+ Gorillib::Model::Field.should have_field(:default)
35
+ end
36
+ context '#has_default?' do
37
+ it 'is true if the default is set' do
38
+ year_field.should_not have_default
39
+ year_field.default = '2012'
40
+ year_field.should have_default
41
+ end
42
+ end
43
+ end
44
+
45
+ describe '#attribute_default' do
46
+ before{ car_class.class_eval{ public :attribute_default } }
47
+
48
+ it "if the default does not exist, returns nil" do
49
+ ford_39.attribute_default(year_field).should be_nil
50
+ end
51
+ it "if the default is a value, returns it, dup'd if possible" do
52
+ ford_39.attribute_default(style_field).should == :sedan
53
+ year_val = double ; dupd_year_val = double
54
+ year_val.should_receive(:try_dup).and_return(dupd_year_val)
55
+ year_field.default = year_val
56
+ ford_39.attribute_default(year_field).should equal(dupd_year_val)
57
+ end
58
+ it "if the default is a proc with no args, instance_exec it" do
59
+ ford_39.style = :sedan
60
+ ford_39.attribute_default(doors_field).should == 4
61
+ ford_39.style = :coupe
62
+ ford_39.attribute_default(doors_field).should == 2
63
+ end
64
+ it "if the default is a proc with no args, instance_exec it" do
65
+ year_field.default = ->{ self }
66
+ ford_39.attribute_default(year_field).should equal(ford_39)
67
+ end
68
+ it "if the default responds_to #call, call it, passing the instance and field name" do
69
+ callable = double ; expected = double
70
+ year_field.default = callable
71
+ callable.should_receive(:respond_to?).with(:call).and_return(true)
72
+ callable.should_receive(:call).with(ford_39, :year).and_return(expected)
73
+ ford_39.stub(:read_unset_attribute).and_return('bob')
74
+ ford_39.attribute_default(year_field).should equal(expected)
75
+ end
76
+ it "if the default is a proc with args, call it in current context with the model and field name" do
77
+ this = self ; expected = double
78
+ year_field.default = ->(inst, field_name){ [self, inst, field_name, expected] }
79
+ ford_39.attribute_default(year_field).should == [this, ford_39, :year, expected]
80
+ end
81
+ end
82
+
83
+ describe 'reading an attribute with a default' do
84
+ it "sets the default value on the field and returns it" do
85
+ ford_39.attribute_set?(:doors).should be_false
86
+ ford_39.read_attribute(:doors).should == 4
87
+ end
88
+ it "only calls a block default if the attribute is unset" do
89
+ val = 0
90
+ year_field.default = ->{ val += 1 }
91
+ ford_39.read_attribute(:year).should == 1
92
+ year_field.default.call.should == 2 # the next call to the block will return 3
93
+ ford_39.read_attribute(:year).should == 1 # repeated calls give the same value
94
+ ford_39.unset_attribute(:year)
95
+ ford_39.read_attribute(:year).should == 3 # see! since it was unset, the block was called again.
96
+ end
97
+ it "is attribute_set? after" do
98
+ ford_39.attribute_set?(:doors).should be_false
99
+ ford_39.read_attribute(:doors)
100
+ ford_39.attribute_set?(:doors).should be_true
101
+ end
102
+ end
103
+
104
+ end