flex_columns 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (50) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +18 -0
  3. data/.travis.yml +38 -0
  4. data/Gemfile +17 -0
  5. data/LICENSE.txt +22 -0
  6. data/README.md +124 -0
  7. data/Rakefile +6 -0
  8. data/flex_columns.gemspec +72 -0
  9. data/lib/flex_columns.rb +15 -0
  10. data/lib/flex_columns/active_record/base.rb +57 -0
  11. data/lib/flex_columns/contents/column_data.rb +376 -0
  12. data/lib/flex_columns/contents/flex_column_contents_base.rb +188 -0
  13. data/lib/flex_columns/definition/field_definition.rb +316 -0
  14. data/lib/flex_columns/definition/field_set.rb +89 -0
  15. data/lib/flex_columns/definition/flex_column_contents_class.rb +327 -0
  16. data/lib/flex_columns/errors.rb +236 -0
  17. data/lib/flex_columns/has_flex_columns.rb +187 -0
  18. data/lib/flex_columns/including/include_flex_columns.rb +179 -0
  19. data/lib/flex_columns/util/dynamic_methods_module.rb +86 -0
  20. data/lib/flex_columns/util/string_utils.rb +31 -0
  21. data/lib/flex_columns/version.rb +4 -0
  22. data/spec/flex_columns/helpers/database_helper.rb +174 -0
  23. data/spec/flex_columns/helpers/exception_helpers.rb +20 -0
  24. data/spec/flex_columns/helpers/system_helpers.rb +47 -0
  25. data/spec/flex_columns/system/basic_system_spec.rb +245 -0
  26. data/spec/flex_columns/system/bulk_system_spec.rb +153 -0
  27. data/spec/flex_columns/system/compression_system_spec.rb +218 -0
  28. data/spec/flex_columns/system/custom_methods_system_spec.rb +120 -0
  29. data/spec/flex_columns/system/delegation_system_spec.rb +175 -0
  30. data/spec/flex_columns/system/dynamism_system_spec.rb +158 -0
  31. data/spec/flex_columns/system/error_handling_system_spec.rb +117 -0
  32. data/spec/flex_columns/system/including_system_spec.rb +285 -0
  33. data/spec/flex_columns/system/json_alias_system_spec.rb +171 -0
  34. data/spec/flex_columns/system/performance_system_spec.rb +218 -0
  35. data/spec/flex_columns/system/postgres_json_column_type_system_spec.rb +85 -0
  36. data/spec/flex_columns/system/types_system_spec.rb +93 -0
  37. data/spec/flex_columns/system/unknown_fields_system_spec.rb +126 -0
  38. data/spec/flex_columns/system/validations_system_spec.rb +111 -0
  39. data/spec/flex_columns/unit/active_record/base_spec.rb +32 -0
  40. data/spec/flex_columns/unit/contents/column_data_spec.rb +520 -0
  41. data/spec/flex_columns/unit/contents/flex_column_contents_base_spec.rb +253 -0
  42. data/spec/flex_columns/unit/definition/field_definition_spec.rb +617 -0
  43. data/spec/flex_columns/unit/definition/field_set_spec.rb +142 -0
  44. data/spec/flex_columns/unit/definition/flex_column_contents_class_spec.rb +733 -0
  45. data/spec/flex_columns/unit/errors_spec.rb +297 -0
  46. data/spec/flex_columns/unit/has_flex_columns_spec.rb +365 -0
  47. data/spec/flex_columns/unit/including/include_flex_columns_spec.rb +144 -0
  48. data/spec/flex_columns/unit/util/dynamic_methods_module_spec.rb +105 -0
  49. data/spec/flex_columns/unit/util/string_utils_spec.rb +23 -0
  50. metadata +286 -0
@@ -0,0 +1,111 @@
1
+ require 'flex_columns'
2
+ require 'flex_columns/helpers/exception_helpers'
3
+ require 'flex_columns/helpers/system_helpers'
4
+
5
+ describe "FlexColumns validations" do
6
+ include FlexColumns::Helpers::SystemHelpers
7
+ include FlexColumns::Helpers::ExceptionHelpers
8
+
9
+ before :each do
10
+ @dh = FlexColumns::Helpers::DatabaseHelper.new
11
+ @dh.setup_activerecord!
12
+
13
+ create_standard_system_spec_tables!
14
+ end
15
+
16
+ after :each do
17
+ drop_standard_system_spec_tables!
18
+ end
19
+
20
+ it "should allow validating fields in the column definition" do
21
+ define_model_class(:User, 'flexcols_spec_users') do
22
+ flex_column :user_attributes do
23
+ field :wants_email
24
+
25
+ validates :wants_email, :presence => true
26
+ end
27
+ end
28
+
29
+ user = ::User.new
30
+ user.name = 'User 1'
31
+ user.user_attributes.touch!
32
+
33
+ e = capture_exception(::ActiveRecord::RecordInvalid) { user.save! }
34
+
35
+ e.record.should be(user)
36
+ e.record.errors.keys.should == [ :'user_attributes.wants_email' ]
37
+ messages = e.record.errors.get(:'user_attributes.wants_email')
38
+ messages.length.should == 1
39
+
40
+ message = messages[0]
41
+ message.should match(/be blank/i)
42
+ end
43
+
44
+ it "should allow validating fields outside the column definition" do
45
+ define_model_class(:User, 'flexcols_spec_users') do
46
+ flex_column :user_attributes do
47
+ field :wants_email
48
+ end
49
+
50
+ validates :wants_email, :presence => true
51
+ end
52
+
53
+ user = ::User.new
54
+ user.name = 'User 1'
55
+
56
+ e = capture_exception(::ActiveRecord::RecordInvalid) { user.save! }
57
+
58
+ e.record.should be(user)
59
+ e.record.errors.keys.should == [ :wants_email ]
60
+ messages = e.record.errors.get(:wants_email)
61
+ messages.length.should == 1
62
+
63
+ message = messages[0]
64
+ message.should match(/be blank/i)
65
+ end
66
+
67
+ it "should combine validations inside and outside the column definition" do
68
+ define_model_class(:User, 'flexcols_spec_users') do
69
+ flex_column :user_attributes do
70
+ field :wants_email
71
+
72
+ validates :wants_email, :numericality => { :less_than => 100 }
73
+ end
74
+
75
+ validates :wants_email, :numericality => { :greater_than => 10 }
76
+ end
77
+
78
+ user = ::User.new
79
+ user.name = 'User 1'
80
+ user.wants_email = 5
81
+
82
+ e = capture_exception(::ActiveRecord::RecordInvalid) { user.save! }
83
+
84
+ e.record.should be(user)
85
+ e.record.errors.keys.should == [ :wants_email ]
86
+ messages = e.record.errors.get(:wants_email)
87
+ messages.length.should == 1
88
+
89
+ message = messages[0]
90
+ message.should match(/be greater than 10/i)
91
+
92
+ user = ::User.new
93
+ user.name = 'User 1'
94
+ user.wants_email = 200
95
+
96
+ e = capture_exception(::ActiveRecord::RecordInvalid) { user.save! }
97
+
98
+ e.record.should be(user)
99
+ e.record.errors.keys.should == [ :'user_attributes.wants_email' ]
100
+ messages = e.record.errors.get(:'user_attributes.wants_email')
101
+ messages.length.should == 1
102
+
103
+ message = messages[0]
104
+ message.should match(/be less than 100/i)
105
+
106
+ user.wants_email = 50
107
+ user.save!
108
+
109
+ user.wants_email.should == 50
110
+ end
111
+ end
@@ -0,0 +1,32 @@
1
+ require 'flex_columns'
2
+
3
+ describe FlexColumns::ActiveRecord::Base do
4
+ before :each do
5
+ @klass = Class.new
6
+ @klass.send(:include, FlexColumns::ActiveRecord::Base)
7
+ end
8
+
9
+ it "should say #has_any_flex_columns? is false by default" do
10
+ @klass.has_any_flex_columns?.should_not be
11
+ end
12
+
13
+ it "should include HasFlexColumns on flex_column" do
14
+ block = lambda { "hi!" }
15
+
16
+ expect(@klass).to receive(:include).once.with(FlexColumns::HasFlexColumns) do
17
+ expect(@klass).to receive(:flex_column).once.with(:foo, &block)
18
+ end
19
+
20
+ @klass.flex_column(:foo, &block).should == "hi!"
21
+ end
22
+
23
+ it "should include IncludeFlexColumns on include_flex_columns_from" do
24
+ block = lambda { "hi!" }
25
+
26
+ expect(@klass).to receive(:include).once.with(FlexColumns::Including::IncludeFlexColumns) do
27
+ expect(@klass).to receive(:include_flex_columns_from).once.with(:foo, &block)
28
+ end
29
+
30
+ @klass.include_flex_columns_from(:foo, &block).should == "hi!"
31
+ end
32
+ end
@@ -0,0 +1,520 @@
1
+ require 'flex_columns'
2
+ require 'flex_columns/helpers/exception_helpers'
3
+
4
+ describe FlexColumns::Contents::ColumnData do
5
+ include FlexColumns::Helpers::ExceptionHelpers
6
+
7
+ before :each do
8
+ @field_set = double("field_set")
9
+ allow(@field_set).to receive(:kind_of?).with(FlexColumns::Definition::FieldSet).and_return(true)
10
+ allow(@field_set).to receive(:all_field_names).with().and_return([ :foo, :bar, :baz ])
11
+
12
+ @field_foo = double("field_foo")
13
+ allow(@field_foo).to receive(:field_name).and_return(:foo)
14
+ allow(@field_foo).to receive(:json_storage_name).and_return(:foo)
15
+ @field_bar = double("field_bar")
16
+ allow(@field_bar).to receive(:field_name).and_return(:bar)
17
+ allow(@field_bar).to receive(:json_storage_name).and_return(:bar)
18
+ @field_baz = double("field_baz")
19
+ allow(@field_baz).to receive(:field_name).and_return(:baz)
20
+ allow(@field_baz).to receive(:json_storage_name).and_return(:baz)
21
+
22
+ allow(@field_set).to receive(:field_named) do |x|
23
+ case x.to_sym
24
+ when :foo then @field_foo
25
+ when :bar then @field_bar
26
+ when :baz then @field_baz
27
+ else nil
28
+ end
29
+ end
30
+
31
+ allow(@field_set).to receive(:field_with_json_storage_name) do |x|
32
+ case x.to_sym
33
+ when :foo then @field_foo
34
+ when :bar then @field_bar
35
+ when :baz then @field_baz
36
+ else nil
37
+ end
38
+ end
39
+
40
+ @data_source = double("data_source")
41
+ allow(@data_source).to receive(:describe_flex_column_data_source).with().and_return("describedescribe")
42
+ allow(@data_source).to receive(:notification_hash_for_flex_column_data_source).and_return(:notif1 => :a, :notif2 => :b)
43
+
44
+ @json_string = ' {"bar":123,"foo":"bar","baz":"quux"} '
45
+ end
46
+
47
+ def klass
48
+ FlexColumns::Contents::ColumnData
49
+ end
50
+
51
+ def new_with_string(s, options = { })
52
+ new_with(options.merge(:storage_string => s))
53
+ end
54
+
55
+ def new_with(options)
56
+ effective_options = {
57
+ :data_source => @data_source, :unknown_fields => :preserve, :storage => :text, :storage_string => nil, :binary_header => true, :null => true
58
+ }.merge(options)
59
+ klass.new(@field_set, effective_options)
60
+ end
61
+
62
+ it "should validate options properly" do
63
+ valid_options = {
64
+ :data_source => @data_source,
65
+ :unknown_fields => :preserve,
66
+ :storage => :text
67
+ }
68
+
69
+ lambda { klass.new(double("not_a_field_set"), valid_options) }.should raise_error(ArgumentError)
70
+ lambda { klass.new(@field_set, valid_options.merge(:data_source => nil)) }.should raise_error(ArgumentError)
71
+ lambda { klass.new(@field_set, valid_options.merge(:unknown_fields => :foo)) }.should raise_error(ArgumentError)
72
+ lambda { klass.new(@field_set, valid_options.merge(:storage => :foo)) }.should raise_error(ArgumentError)
73
+ lambda { klass.new(@field_set, valid_options.merge(:length_limit => 'foo')) }.should raise_error(ArgumentError)
74
+ lambda { klass.new(@field_set, valid_options.merge(:length_limit => 3)) }.should raise_error(ArgumentError)
75
+ lambda { klass.new(@field_set, valid_options.merge(:compress_if_over_length => 3.5)) }.should raise_error(ArgumentError)
76
+ lambda { klass.new(@field_set, valid_options.merge(:compress_if_over_length => 'foo')) }.should raise_error(ArgumentError)
77
+ lambda { klass.new(@field_set, valid_options.merge(:null => 'foo')) }.should raise_error(ArgumentError)
78
+ end
79
+
80
+ context "with a valid instance" do
81
+ before :each do
82
+ @instance = new_with_string(@json_string)
83
+ end
84
+
85
+ describe "[]" do
86
+ it "should reject invalid field names" do
87
+ expect(@field_set).to receive(:field_named).with(:quux).and_return(nil)
88
+
89
+ e = capture_exception(FlexColumns::Errors::NoSuchFieldError) { @instance[:quux] }
90
+ e.data_source.should be(@data_source)
91
+ e.field_name.should == :quux
92
+ e.all_field_names.should == [ :foo, :bar, :baz ]
93
+ end
94
+
95
+ it "should return data from a valid field correctly" do
96
+ @instance[:foo].should == 'bar'
97
+ end
98
+ end
99
+
100
+ describe "[]=" do
101
+ it "should reject invalid field names" do
102
+ expect(@field_set).to receive(:field_named).with(:quux).and_return(nil)
103
+
104
+ e = capture_exception(FlexColumns::Errors::NoSuchFieldError) { @instance[:quux] = "a" }
105
+ end
106
+
107
+ it "should assign data to a valid field correctly" do
108
+ @instance[:foo] = "abc"
109
+ @instance[:foo].should == "abc"
110
+ end
111
+
112
+ it "should transform Symbols to Strings" do
113
+ @instance[:foo] = :abc
114
+ @instance[:foo].should == "abc"
115
+ end
116
+ end
117
+
118
+ it "should return keys for #keys" do
119
+ @instance.keys.sort_by(&:to_s).should == [ :foo, :bar, :baz ].sort_by(&:to_s)
120
+ end
121
+
122
+ it "should not return things set to nil in #keys" do
123
+ @instance[:bar] = nil
124
+ @instance.keys.sort_by(&:to_s).should == [ :foo, :baz ].sort_by(&:to_s)
125
+ end
126
+
127
+ describe "touching" do
128
+ it "should deserialize, if needed, on touch!" do
129
+ instance = new_with_string("---unparseable JSON---")
130
+
131
+ lambda { instance.touch! }.should raise_error(FlexColumns::Errors::UnparseableJsonInDatabaseError)
132
+ end
133
+
134
+ it "should not be touched if you simply read from it" do
135
+ @instance.touched?.should_not be
136
+ @instance[:foo]
137
+ @instance.touched?.should_not be
138
+ end
139
+
140
+ it "should not be touched if you set a field to the same thing" do
141
+ @instance.touched?.should_not be
142
+ @instance[:foo] = 'bar'
143
+ @instance.touched?.should_not be
144
+ end
145
+
146
+ it "should be touched if you set a field to something different" do
147
+ @instance.touched?.should_not be
148
+ @instance[:foo] = 'baz'
149
+ @instance.touched?.should be
150
+ end
151
+ end
152
+
153
+ it "should return JSON data with #to_json" do
154
+ json = @instance.to_json
155
+ parsed = JSON.parse(json)
156
+ parsed.keys.sort.should == %w{foo bar baz}.sort
157
+ parsed['foo'].should == 'bar'
158
+ parsed['bar'].should == 123
159
+ parsed['baz'].should == 'quux'
160
+ end
161
+
162
+ it "should return JSON data with #to_json" do
163
+ json = @instance.to_json
164
+ parsed = JSON.parse(json)
165
+ parsed.keys.sort.should == %w{foo bar baz}.sort
166
+ parsed['foo'].should == 'bar'
167
+ parsed['bar'].should == 123
168
+ parsed['baz'].should == 'quux'
169
+ end
170
+
171
+ it "should accept a Hash as JSON, already parsed by the database stack" do
172
+ @instance = new_with(:storage_string => { 'foo' => 'bar', 'baz' => 123, 'bar' => 'quux' })
173
+ @instance['foo'].should == 'bar'
174
+ @instance['bar'].should == 'quux'
175
+ @instance['baz'].should == 123
176
+ end
177
+
178
+ describe "#to_stored_data" do
179
+ it "should return JSON data properly" do
180
+ json = @instance.to_stored_data
181
+ parsed = JSON.parse(json)
182
+ parsed.keys.sort.should == %w{foo bar baz}.sort
183
+ parsed['foo'].should == 'bar'
184
+ parsed['bar'].should == 123
185
+ parsed['baz'].should == 'quux'
186
+ end
187
+
188
+ it "should return a raw JSON hash if the column type is :json" do
189
+ @instance = new_with_string(@json_string, :storage => :json)
190
+ @instance.to_stored_data.should == { :foo => 'bar', :baz => 'quux', :bar => 123 }
191
+ end
192
+
193
+ describe "with a text column" do
194
+ it "should return nil if there's no data and the column allows it" do
195
+ @instance = new_with_string("{}")
196
+ @instance.to_stored_data.should == nil
197
+ end
198
+
199
+ it "should return the empty string if there's no data and the column does not allow nulls" do
200
+ @instance = new_with_string("{}", :null => false)
201
+ @instance.to_stored_data.should == ""
202
+ end
203
+ end
204
+
205
+ describe "with a binary column" do
206
+ it "should return nil if there's no data and the column allows it" do
207
+ @instance = new_with_string("{}", :storage => :binary)
208
+ @instance.to_stored_data.should == nil
209
+ end
210
+
211
+ it "should return the empty string if there's no data and the column does not allow nulls" do
212
+ @instance = new_with_string("{}", :storage => :binary, :null => false)
213
+ @instance.to_stored_data.should == ""
214
+ end
215
+ end
216
+
217
+ it "should return JSON from a binary column with :header => false" do
218
+ @instance = new_with_string(@json_string, :storage => :binary, :binary_header => false)
219
+ json = @instance.to_stored_data
220
+ parsed = JSON.parse(json)
221
+ parsed.keys.sort.should == %w{foo bar baz}.sort
222
+ parsed['foo'].should == 'bar'
223
+ parsed['bar'].should == 123
224
+ parsed['baz'].should == 'quux'
225
+ end
226
+
227
+ it "should return uncompressed JSON from a binary column without compression" do
228
+ @instance = new_with_string(@json_string, :storage => :binary)
229
+ stored = @instance.to_stored_data
230
+ stored.should match(/^FC:01,0,/)
231
+ json = stored[8..-1]
232
+
233
+ parsed = JSON.parse(json)
234
+ parsed.keys.sort.should == %w{foo bar baz}.sort
235
+ parsed['foo'].should == 'bar'
236
+ parsed['bar'].should == 123
237
+ parsed['baz'].should == 'quux'
238
+ end
239
+
240
+ it "should return compressed JSON from a binary column with compression" do
241
+ @json_string = ({ :foo => 'bar' * 1000, :bar => 123, :baz => 'quux' }.to_json)
242
+ @instance = new_with_string(@json_string, :storage => :binary, :compress_if_over_length => 1)
243
+ stored = @instance.to_stored_data
244
+ stored.should match(/^FC:01,1,/)
245
+ compressed = stored[8..-1]
246
+
247
+ require 'stringio'
248
+ input = StringIO.new(compressed, "r")
249
+ reader = Zlib::GzipReader.new(input)
250
+ uncompressed = reader.read
251
+
252
+ parsed = JSON.parse(uncompressed)
253
+ parsed.keys.sort.should == %w{foo bar baz}.sort
254
+ parsed['foo'].should == 'bar' * 1000
255
+ parsed['bar'].should == 123
256
+ parsed['baz'].should == 'quux'
257
+ end
258
+
259
+ it "should return uncompressed JSON from a binary column with compression, but that isn't long enough" do
260
+ @json_string = ({ :foo => 'bar', :bar => 123, :baz => 'quux' }.to_json)
261
+ @instance = new_with_string(@json_string, :storage => :binary, :compress_if_over_length => 10_000)
262
+ stored = @instance.to_stored_data
263
+ stored.should match(/^FC:01,0,/)
264
+ json = stored[8..-1]
265
+
266
+ parsed = JSON.parse(json)
267
+ parsed.keys.sort.should == %w{foo bar baz}.sort
268
+ parsed['foo'].should == 'bar'
269
+ parsed['bar'].should == 123
270
+ parsed['baz'].should == 'quux'
271
+ end
272
+
273
+ it "should blow up if the string won't fit" do
274
+ @json_string = ({ :foo => 'bar' * 1000, :bar => 123, :baz => 'quux' }.to_json)
275
+ @instance = new_with_string(@json_string, :storage => :binary, :length_limit => 1_000)
276
+
277
+ e = capture_exception(FlexColumns::Errors::JsonTooLongError) { @instance.to_stored_data }
278
+ e.data_source.should be(@data_source)
279
+ e.limit.should == 1_000
280
+ e.json_string.should match(/^FC:01,0,/)
281
+ e.json_string.length.should >= 3_000
282
+ end
283
+ end
284
+
285
+ describe "deserialization" do
286
+ it "should raise an error if encoding is wrong" do
287
+ bad_encoding = double("bad_encoding")
288
+ allow(bad_encoding).to receive(:kind_of?).with(String).and_return(true)
289
+ allow(bad_encoding).to receive(:kind_of?).with(Hash).and_return(false)
290
+ expect(bad_encoding).to receive(:valid_encoding?).with().and_return(false)
291
+
292
+ exception = StandardError.new("bonk")
293
+ expect(FlexColumns::Errors::IncorrectlyEncodedStringInDatabaseError).to receive(:new).once.with(@data_source, bad_encoding).and_return(exception)
294
+
295
+ capture_exception { new_with_string(bad_encoding)[:foo] }.should be(exception)
296
+ end
297
+
298
+ it "should accept blank strings just fine" do
299
+ instance = new_with_string(" ")
300
+ instance[:foo].should be_nil
301
+ instance[:bar].should be_nil
302
+ instance[:baz].should be_nil
303
+ end
304
+
305
+ it "should raise an error if the JSON doesn't parse" do
306
+ bogus_json = "---unparseable JSON---"
307
+ instance = new_with_string(bogus_json)
308
+
309
+ e = capture_exception(FlexColumns::Errors::UnparseableJsonInDatabaseError) { instance[:foo] }
310
+ e.data_source.should be(@data_source)
311
+ e.raw_string.should == bogus_json
312
+ e.source_exception.kind_of?(JSON::ParserError).should be
313
+ e.message.should match(/describedescribe/)
314
+ end
315
+
316
+ it "should raise an error if the JSON doesn't represent a Hash" do
317
+ bogus_json = "[ 1, 2, 3 ]"
318
+ instance = new_with_string(bogus_json)
319
+
320
+ e = capture_exception(FlexColumns::Errors::InvalidJsonInDatabaseError) { instance[:foo] }
321
+ e.data_source.should be(@data_source)
322
+ e.raw_string.should == bogus_json
323
+ e.returned_data.should == [ 1, 2, 3 ]
324
+ e.message.should match(/describedescribe/)
325
+ end
326
+
327
+ it "should accept uncompressed strings with a header" do
328
+ instance = new_with_string("FC:01,0,#{@json_string}")
329
+ instance[:foo].should == "bar"
330
+ instance[:bar].should == 123
331
+ instance[:baz].should == "quux"
332
+ end
333
+
334
+ it "should accept compressed strings with a header" do
335
+ require 'stringio'
336
+ stream = StringIO.new("w")
337
+ writer = Zlib::GzipWriter.new(stream)
338
+ writer.write(@json_string)
339
+ writer.close
340
+
341
+ header = "FC:01,1,"
342
+ header.force_encoding("BINARY") if header.respond_to?(:force_encoding)
343
+ total = header + stream.string
344
+ total.force_encoding("BINARY") if total.respond_to?(:force_encoding)
345
+ instance = new_with_string(total)
346
+ instance[:foo].should == "bar"
347
+ instance[:bar].should == 123
348
+ instance[:baz].should == "quux"
349
+ end
350
+
351
+ it "should fail if the version number is too big" do
352
+ bad_string = "FC:02,0,#{@json_string}"
353
+ instance = new_with_string(bad_string)
354
+
355
+ e = capture_exception(FlexColumns::Errors::InvalidFlexColumnsVersionNumberInDatabaseError) { instance[:foo] }
356
+ e.data_source.should be(@data_source)
357
+ e.raw_string.should == bad_string
358
+ e.version_number_in_database.should == 2
359
+ e.max_version_number_supported.should == 1
360
+ e.message.should match(/describedescribe/)
361
+ end
362
+
363
+ it "should fail if the compression number is too big" do
364
+ require 'stringio'
365
+ stream = StringIO.new("w")
366
+ writer = Zlib::GzipWriter.new(stream)
367
+ writer.write(@json_string)
368
+ writer.close
369
+
370
+ header = "FC:01,2,"
371
+ header.force_encoding("BINARY") if header.respond_to?(:force_encoding)
372
+ bad_string = header + stream.string
373
+ bad_string.force_encoding("BINARY") if bad_string.respond_to?(:force_encoding)
374
+
375
+ instance = new_with_string(bad_string)
376
+ e = capture_exception(FlexColumns::Errors::InvalidDataInDatabaseError) { instance[:foo] }
377
+ e.data_source.should be(@data_source)
378
+ e.raw_string.should == bad_string
379
+ e.message.should match(/2/)
380
+ e.message.should match(/describedescribe/)
381
+ end
382
+
383
+ it "should fail if the compressed data is bogus" do
384
+ require 'stringio'
385
+ stream = StringIO.new("w")
386
+ writer = Zlib::GzipWriter.new(stream)
387
+ writer.write(@json_string)
388
+ writer.close
389
+ compressed_data = stream.string
390
+
391
+ 100.times do
392
+ pos_1 = rand(10)
393
+ pos_2 = rand(10)
394
+ tmp = compressed_data[pos_1]
395
+ compressed_data[pos_1] = compressed_data[pos_2]
396
+ compressed_data[pos_2] = tmp
397
+ end
398
+
399
+ header = "FC:01,1,"
400
+ header.force_encoding("BINARY") if header.respond_to?(:force_encoding)
401
+ bad_string = header + compressed_data
402
+ bad_string.force_encoding("BINARY") if bad_string.respond_to?(:force_encoding)
403
+
404
+ instance = new_with_string(bad_string)
405
+ e = capture_exception(FlexColumns::Errors::InvalidCompressedDataInDatabaseError) { instance[:foo] }
406
+ e.data_source.should be(@data_source)
407
+ e.raw_string.should == bad_string
408
+ e.source_exception.class.should == Zlib::GzipFile::Error
409
+ e.message.should match(/describedescribe/)
410
+ end
411
+ end
412
+
413
+ describe "notifications" do
414
+ before :each do
415
+ @deserializations = [ ]
416
+ ds = @deserializations
417
+
418
+ ActiveSupport::Notifications.subscribe('flex_columns.deserialize') do |name, start, finish, id, payload|
419
+ ds << payload
420
+ end
421
+
422
+ @serializations = [ ]
423
+ s = @serializations
424
+
425
+ ActiveSupport::Notifications.subscribe('flex_columns.serialize') do |name, start, finish, id, payload|
426
+ s << payload
427
+ end
428
+ end
429
+
430
+ it "should trigger a notification on deserialization" do
431
+ @deserializations.length.should == 0
432
+
433
+ @instance[:foo].should == 'bar'
434
+ @deserializations.length.should == 1
435
+ @deserializations[0].should == { :notif1 => :a, :notif2 => :b, :raw_data => @json_string }
436
+ end
437
+
438
+ it "should trigger a notification on serialization" do
439
+ @serializations.length.should == 0
440
+
441
+ @instance[:foo].should == 'bar'
442
+ @serializations.length.should == 0
443
+
444
+ @instance.to_stored_data
445
+
446
+ @serializations.length.should == 1
447
+ @serializations[0].should == { :notif1 => :a, :notif2 => :b }
448
+ end
449
+
450
+ it "should not trigger a notification on #to_json" do
451
+ @serializations.length.should == 0
452
+
453
+ @instance[:foo].should == 'bar'
454
+ @serializations.length.should == 0
455
+
456
+ @instance.to_json
457
+
458
+ @serializations.length.should == 0
459
+ end
460
+
461
+ it "should not deserialize until data is required" do
462
+ @deserializations.length.should == 0
463
+ end
464
+ end
465
+
466
+ describe "unknown-field handling" do
467
+ it "should hang on to unknown data if asked" do
468
+ s = { :foo => 'bar', :quux => 'baz' }.to_json
469
+ @instance = new_with_string(s)
470
+ parsed = JSON.parse(@instance.to_json)
471
+ parsed['quux'].should == 'baz'
472
+ end
473
+
474
+ it "should discard unknown data if asked" do
475
+ s = { :foo => 'bar', :quux => 'baz' }.to_json
476
+ @instance = new_with_string(s, :unknown_fields => :delete)
477
+ parsed = JSON.parse(@instance.to_json)
478
+ parsed.keys.should == [ 'foo' ]
479
+ parsed['quux'].should_not be
480
+ end
481
+
482
+ it "should not allow unknown data to conflict with known data" do
483
+ field_set = double("field_set")
484
+ allow(field_set).to receive(:kind_of?).with(FlexColumns::Definition::FieldSet).and_return(true)
485
+ allow(field_set).to receive(:all_field_names).with().and_return([ :foo ])
486
+
487
+ field_foo = double("field_foo")
488
+ allow(field_foo).to receive(:field_name).and_return(:foo)
489
+ allow(field_foo).to receive(:json_storage_name).and_return(:bar)
490
+
491
+ allow(field_set).to receive(:field_named) do |x|
492
+ case x.to_sym
493
+ when :foo then field_foo
494
+ else nil
495
+ end
496
+ end
497
+
498
+ allow(field_set).to receive(:field_with_json_storage_name) do |x|
499
+ case x.to_sym
500
+ when :bar then field_foo
501
+ else nil
502
+ end
503
+ end
504
+
505
+ json_string = { :foo => 'aaa', :bar => 'bbb' }.to_json
506
+ instance = klass.new(field_set, :storage_string => json_string, :data_source => @data_source,
507
+ :unknown_fields => :preserve, :storage => :text, :storage_string => json_string, :binary_header => true,
508
+ :null => true)
509
+
510
+ instance[:foo].should == 'bbb'
511
+ lambda { instance[:bar] }.should raise_error(FlexColumns::Errors::NoSuchFieldError)
512
+
513
+ reparsed = JSON.parse(instance.to_json)
514
+ reparsed.keys.sort.should == %w{foo bar}.sort
515
+ reparsed['foo'].should == 'aaa'
516
+ reparsed['bar'].should == 'bbb'
517
+ end
518
+ end
519
+ end
520
+ end