flex_columns 1.0.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.
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