gorillib-model 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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