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,297 @@
1
+ require 'flex_columns'
2
+
3
+ describe FlexColumns::Errors do
4
+ def should_be_a_subclass(klass, expected_superclass)
5
+ current = klass
6
+
7
+ while current != expected_superclass && current != Object
8
+ current = current.superclass
9
+ end
10
+
11
+ if current == Object
12
+ raise "Expected #{klass} to have #{expected_superclass} as a superclass, but it doesn't"
13
+ end
14
+ end
15
+
16
+ before :each do
17
+ @data_source = double("data_source")
18
+ allow(@data_source).to receive(:describe_flex_column_data_source).with().and_return("dfcds")
19
+ end
20
+
21
+ describe FlexColumns::Errors::FieldError do
22
+ it "should inherit from Base" do
23
+ should_be_a_subclass(FlexColumns::Errors::FieldError, FlexColumns::Errors::Base)
24
+ end
25
+ end
26
+
27
+ describe FlexColumns::Errors::NoSuchFieldError do
28
+ it "should inherit from FieldError" do
29
+ should_be_a_subclass(FlexColumns::Errors::NoSuchFieldError, FlexColumns::Errors::FieldError)
30
+ end
31
+
32
+ it "should take data_source, data_name, and all_field_names, and use them in its message" do
33
+ instance = FlexColumns::Errors::NoSuchFieldError.new(@data_source, :foo, [ :bar, :baz, :quux ])
34
+ instance.message.should match(/dfcds/i)
35
+ instance.message.should match(/foo/i)
36
+ instance.message.should match(/bar.*baz.*quux/i)
37
+
38
+ instance.data_source.should be(@data_source)
39
+ instance.field_name.should == :foo
40
+ instance.all_field_names.should == [ :bar, :baz, :quux ]
41
+ end
42
+ end
43
+
44
+ describe FlexColumns::Errors::ConflictingJsonStorageNameError do
45
+ it "should inherit from FieldError" do
46
+ should_be_a_subclass(FlexColumns::Errors::ConflictingJsonStorageNameError, FlexColumns::Errors::FieldError)
47
+ end
48
+
49
+ it "should take model_class, column_name, new_field_name, existing_field_name, and json_storage_name" do
50
+ model_class = double("model_class")
51
+ allow(model_class).to receive(:name).with().and_return("mcname")
52
+
53
+ instance = FlexColumns::Errors::ConflictingJsonStorageNameError.new(model_class, :foo, :bar, :baz, :quux)
54
+ instance.model_class.should be(model_class)
55
+ instance.column_name.should == :foo
56
+ instance.new_field_name.should == :bar
57
+ instance.existing_field_name.should == :baz
58
+ instance.json_storage_name.should == :quux
59
+
60
+ instance.message.should match(/mcname/i)
61
+ instance.message.should match(/foo/i)
62
+ instance.message.should match(/bar/i)
63
+ instance.message.should match(/baz/i)
64
+ instance.message.should match(/quux/i)
65
+ end
66
+ end
67
+
68
+ describe FlexColumns::Errors::DefinitionError do
69
+ it "should inherit from Base" do
70
+ should_be_a_subclass(FlexColumns::Errors::DefinitionError, FlexColumns::Errors::Base)
71
+ end
72
+ end
73
+
74
+ describe FlexColumns::Errors::NoSuchColumnError do
75
+ it "should inherit from DefinitionError" do
76
+ should_be_a_subclass(FlexColumns::Errors::NoSuchColumnError, FlexColumns::Errors::DefinitionError)
77
+ end
78
+ end
79
+
80
+ describe FlexColumns::Errors::InvalidColumnTypeError do
81
+ it "should inherit from DefinitionError" do
82
+ should_be_a_subclass(FlexColumns::Errors::InvalidColumnTypeError, FlexColumns::Errors::DefinitionError)
83
+ end
84
+ end
85
+
86
+ describe FlexColumns::Errors::DataError do
87
+ it "should inherit from Base" do
88
+ should_be_a_subclass(FlexColumns::Errors::DataError, FlexColumns::Errors::Base)
89
+ end
90
+ end
91
+
92
+ describe FlexColumns::Errors::JsonTooLongError do
93
+ it "should inherit from DataError" do
94
+ should_be_a_subclass(FlexColumns::Errors::JsonTooLongError, FlexColumns::Errors::DataError)
95
+ end
96
+
97
+ it "should take data_source, limit, and json_string" do
98
+ instance = FlexColumns::Errors::JsonTooLongError.new(@data_source, 123, "a" * 10_000)
99
+ instance.data_source.should be(@data_source)
100
+ instance.limit.should == 123
101
+ instance.json_string.should == "a" * 10_000
102
+
103
+ instance.message.length.should < 1_000
104
+ instance.message.should match(/123/)
105
+ instance.message.should match(/dfcds/)
106
+ instance.message.should match("a" * 30)
107
+ end
108
+ end
109
+
110
+ describe FlexColumns::Errors::InvalidDataInDatabaseError do
111
+ it "should inherit from DataError" do
112
+ should_be_a_subclass(FlexColumns::Errors::InvalidDataInDatabaseError, FlexColumns::Errors::DataError)
113
+ end
114
+
115
+ it "should take data_source, raw_string, and additional_message" do
116
+ instance = FlexColumns::Errors::InvalidDataInDatabaseError.new(@data_source, "aaa" * 1_000, "holy hell")
117
+ instance.data_source.should be(@data_source)
118
+ instance.raw_string.should == "aaa" * 1_000
119
+ instance.additional_message.should == "holy hell"
120
+
121
+ instance.message.length.should < 1_000
122
+ instance.message.should match(/dfcds/)
123
+ instance.message.should match("a" * 20)
124
+ instance.message.should match(/holy hell/)
125
+ end
126
+ end
127
+
128
+ describe FlexColumns::Errors::InvalidCompressedDataInDatabaseError do
129
+ it "should inherit from InvalidDataInDatabaseError" do
130
+ should_be_a_subclass(FlexColumns::Errors::InvalidCompressedDataInDatabaseError, FlexColumns::Errors::InvalidDataInDatabaseError)
131
+ end
132
+
133
+ it "should take data_source, raw_string, and source_exception" do
134
+ source_exception = double("source_exception")
135
+ source_exception_class = double("source_exception_class")
136
+ allow(source_exception_class).to receive(:name).with().and_return("secname")
137
+ allow(source_exception).to receive(:class).with().and_return(source_exception_class)
138
+ allow(source_exception).to receive(:to_s).with().and_return("seto_s")
139
+
140
+ instance = FlexColumns::Errors::InvalidCompressedDataInDatabaseError.new(@data_source, "a" * 1_000, source_exception)
141
+ instance.data_source.should be(@data_source)
142
+ instance.raw_string.should == "a" * 1_000
143
+ instance.source_exception.should be(source_exception)
144
+
145
+ instance.message.length.should < 1_000
146
+ instance.message.should match(/dfcds/)
147
+ instance.message.should match("a" * 20)
148
+ instance.message.should match(/secname/)
149
+ instance.message.should match(/seto_s/)
150
+ end
151
+ end
152
+
153
+ describe FlexColumns::Errors::InvalidFlexColumnsVersionNumberInDatabaseError do
154
+ it "should inherit from InvalidDataInDatabaseError" do
155
+ should_be_a_subclass(FlexColumns::Errors::InvalidFlexColumnsVersionNumberInDatabaseError, FlexColumns::Errors::InvalidDataInDatabaseError)
156
+ end
157
+
158
+ it "should take data_source, raw_string, version_number_in_database, max_version_number_supported" do
159
+ instance = FlexColumns::Errors::InvalidFlexColumnsVersionNumberInDatabaseError.new(@data_source, "a" * 1_000, 17, 15)
160
+ instance.data_source.should be(@data_source)
161
+ instance.raw_string.should == "a" * 1_000
162
+ instance.version_number_in_database.should == 17
163
+ instance.max_version_number_supported.should == 15
164
+
165
+ instance.message.length.should < 1_000
166
+ instance.message.should match(/dfcds/)
167
+ instance.message.should match("a" * 20)
168
+ instance.message.should match(/17/)
169
+ instance.message.should match(/15/)
170
+ end
171
+ end
172
+
173
+ describe FlexColumns::Errors::UnparseableJsonInDatabaseError do
174
+ it "should inherit from InvalidDataInDatabaseError" do
175
+ should_be_a_subclass(FlexColumns::Errors::UnparseableJsonInDatabaseError, FlexColumns::Errors::InvalidDataInDatabaseError)
176
+ end
177
+
178
+ it "should take data_source, raw_string, and source_exception" do
179
+ source_exception = double("source_exception")
180
+ source_exception_class = double("source_exception_class")
181
+ allow(source_exception_class).to receive(:name).with().and_return("secname")
182
+ allow(source_exception).to receive(:class).with().and_return(source_exception_class)
183
+ allow(source_exception).to receive(:message).with().and_return("semessage")
184
+
185
+ instance = FlexColumns::Errors::UnparseableJsonInDatabaseError.new(@data_source, "a" * 1_000, source_exception)
186
+ instance.data_source.should be(@data_source)
187
+ instance.raw_string.should == "a" * 1_000
188
+ instance.source_exception.should be(source_exception)
189
+
190
+ instance.message.length.should < 1_000
191
+ instance.message.should match(/dfcds/)
192
+ instance.message.should match("a" * 20)
193
+ instance.message.should match(/secname/)
194
+ instance.message.should match(/semessage/)
195
+ end
196
+
197
+ it "should strip out characters that aren't of a valid encoding" do
198
+ source_exception = double("source_exception")
199
+ source_exception_class = double("source_exception_class")
200
+ allow(source_exception_class).to receive(:name).with().and_return("secname")
201
+ allow(source_exception).to receive(:class).with().and_return(source_exception_class)
202
+
203
+ source_message = double("source_message")
204
+ expect(source_message).to receive(:force_encoding).once.with("UTF-8")
205
+ chars = [ double("char-a"), double("char-b"), double("char-c") ]
206
+ allow(chars[0]).to receive(:valid_encoding?).with().and_return(true)
207
+ allow(chars[0]).to receive(:to_s).with().and_return("X")
208
+ allow(chars[0]).to receive(:inspect).with().and_return("X")
209
+ allow(chars[1]).to receive(:valid_encoding?).with().and_return(false)
210
+ allow(chars[2]).to receive(:valid_encoding?).with().and_return(true)
211
+ allow(chars[2]).to receive(:to_s).with().and_return("Z")
212
+ allow(chars[2]).to receive(:inspect).with().and_return("Z")
213
+ allow(source_message).to receive(:chars).with().and_return(chars)
214
+
215
+ allow(source_exception).to receive(:message).with().and_return(source_message)
216
+
217
+ instance = FlexColumns::Errors::UnparseableJsonInDatabaseError.new(@data_source, "a" * 1_000, source_exception)
218
+ instance.data_source.should be(@data_source)
219
+ instance.raw_string.should == "a" * 1_000
220
+ instance.source_exception.should be(source_exception)
221
+
222
+ instance.message.length.should < 1_000
223
+ instance.message.should match(/dfcds/)
224
+ instance.message.should match("a" * 20)
225
+ instance.message.should match(/secname/)
226
+ instance.message.should match(/XZ/)
227
+ end
228
+ end
229
+
230
+ describe FlexColumns::Errors::IncorrectlyEncodedStringInDatabaseError do
231
+ it "should inherit from InvalidDataInDatabaseError" do
232
+ should_be_a_subclass(FlexColumns::Errors::IncorrectlyEncodedStringInDatabaseError, FlexColumns::Errors::InvalidDataInDatabaseError)
233
+ end
234
+
235
+ it "should take data_source and raw_string, and analyze the string" do
236
+ chars = [ ]
237
+ 200.times { |i| chars << double("char-#{i}") }
238
+ chars.each_with_index do |char, i|
239
+ allow(char).to receive(:valid_encoding?).with().and_return(true)
240
+ allow(char).to receive(:to_s).with().and_return((65 + (i % 26)).chr)
241
+ allow(char).to receive(:inspect).with().and_return((65 + (i % 26)).chr)
242
+ end
243
+
244
+ [ 53, 68, 95 ].each do |bad_char_pos|
245
+ allow(chars[bad_char_pos]).to receive(:valid_encoding?).with().and_return(false)
246
+ allow(chars[bad_char_pos]).to receive(:unpack).with("H*").and_return("UPC#{bad_char_pos}")
247
+ end
248
+
249
+ allow(chars[53]).to receive(:valid_encoding?).with().and_return(false)
250
+ allow(chars[68]).to receive(:valid_encoding?).with().and_return(false)
251
+ allow(chars[95]).to receive(:valid_encoding?).with().and_return(false)
252
+
253
+ raw_string = double("raw_string")
254
+ allow(raw_string).to receive(:chars).with().and_return(chars)
255
+
256
+ instance = FlexColumns::Errors::IncorrectlyEncodedStringInDatabaseError.new(@data_source, raw_string)
257
+
258
+ instance.data_source.should be(@data_source)
259
+ instance.raw_string.should match(/^ABCDEF/)
260
+ instance.raw_string.length.should == 197
261
+ instance.raw_string[52..54].should == "ACD"
262
+ instance.raw_string[66..68].should == "PRS"
263
+ instance.raw_string[91..93].should == "PQS"
264
+
265
+ instance.invalid_chars_as_array.should == [ chars[53], chars[68], chars[95] ]
266
+ instance.raw_data_as_array.should == chars
267
+ instance.first_bad_position.should == 53
268
+
269
+ instance.message.should match(/dfcds/)
270
+ instance.message.should match(/3/)
271
+ instance.message.should match(/200/)
272
+ instance.message.should match(/53/)
273
+ instance.message.should match(/UPC53.*UPC68.*UPC95/)
274
+ end
275
+ end
276
+
277
+ describe FlexColumns::Errors::InvalidJsonInDatabaseError do
278
+ it "should inherit from InvalidDataInDatabaseError" do
279
+ should_be_a_subclass(FlexColumns::Errors::InvalidJsonInDatabaseError, FlexColumns::Errors::InvalidDataInDatabaseError)
280
+ end
281
+
282
+ it "should take a data_source, raw_string, and returned_data" do
283
+ raw_string = " [ 1, 2, 3 ] "
284
+ returned_data = [ 9, 12, 15 ]
285
+ instance = FlexColumns::Errors::InvalidJsonInDatabaseError.new(@data_source, raw_string, returned_data)
286
+
287
+ instance.data_source.should be(@data_source)
288
+ instance.raw_string.should == raw_string
289
+ instance.returned_data.should be(returned_data)
290
+
291
+ instance.message.should match(/dfcds/)
292
+ instance.message.should match(raw_string)
293
+ instance.message.should match(/Array/)
294
+ instance.message.should match(/9\s*,\s*12\s*,\s*15\s*/)
295
+ end
296
+ end
297
+ end
@@ -0,0 +1,365 @@
1
+ require 'flex_columns'
2
+ require 'flex_columns/helpers/exception_helpers'
3
+
4
+ describe FlexColumns::HasFlexColumns do
5
+ include FlexColumns::Helpers::ExceptionHelpers
6
+
7
+ before :each do
8
+ @superclass = Class.new
9
+
10
+ @klass = Class.new(@superclass) do
11
+ class << self
12
+ def before_validation(*args)
13
+ @_before_validation_calls ||= [ ]
14
+ @_before_validation_calls << args
15
+ end
16
+
17
+ def before_save(*args)
18
+ @_before_save_calls ||= [ ]
19
+ @_before_save_calls << args
20
+ end
21
+
22
+ def before_validation_calls
23
+ @_before_validation_calls
24
+ end
25
+
26
+ def before_save_calls
27
+ @_before_save_calls
28
+ end
29
+ end
30
+ end
31
+
32
+ @klass_name = "HasFlexColumnsSpec_#{rand(1_000_000_000)}"
33
+ ::Object.const_set(@klass_name, @klass)
34
+
35
+ @klass.send(:include, FlexColumns::HasFlexColumns)
36
+ end
37
+
38
+ it "should call before_validation and before_save to set up hooks properly" do
39
+ @klass.before_validation_calls.length.should == 1
40
+ @klass.before_validation_calls[0].should == [ :_flex_columns_before_validation! ]
41
+ @klass.before_save_calls.length.should == 1
42
+ @klass.before_save_calls[0].should == [ :_flex_columns_before_save! ]
43
+ end
44
+
45
+ it "should normalize names properly" do
46
+ @klass._flex_column_normalize_name(" FoO ").should == :foo
47
+ @klass._flex_column_normalize_name(:fOo).should == :foo
48
+ end
49
+
50
+ it "should say it has flex columns" do
51
+ @klass.has_any_flex_columns?.should be
52
+ end
53
+
54
+ describe "#flex_column" do
55
+ it "should normalize the name of the column" do
56
+ fcc = double("fcc")
57
+ expect(Class).to receive(:new).once.with(FlexColumns::Contents::FlexColumnContentsBase).and_return(fcc)
58
+ expect(fcc).to receive(:setup!).once.with(@klass, :foo, { })
59
+ allow(fcc).to receive(:sync_methods!).with()
60
+
61
+ @klass.flex_column(' fOo ')
62
+ end
63
+
64
+ it "should replace existing columns, call #remove_all_methods! and #sync_methods! appropriately, and define a method that returns the right object" do
65
+ fcc_foo = double("fcc_foo")
66
+ expect(Class).to receive(:new).once.with(FlexColumns::Contents::FlexColumnContentsBase).and_return(fcc_foo)
67
+ expect(fcc_foo).to receive(:quux).once.with(:a, :z, :q)
68
+
69
+ passed_block = nil
70
+ expect(fcc_foo).to receive(:setup!).once.with(@klass, :foo, { }) do |*args, &block|
71
+ passed_block = block
72
+ end
73
+ allow(fcc_foo).to receive(:column_name).with().and_return(:foo)
74
+
75
+ dmm = double("dmm")
76
+ expect(FlexColumns::Util::DynamicMethodsModule).to receive(:new).once.with(@klass, :FlexColumnsDynamicMethods).and_return(dmm)
77
+
78
+ expect(dmm).to receive(:remove_all_methods!).once.with()
79
+ expect(fcc_foo).to receive(:sync_methods!).once.with()
80
+ @klass.flex_column(:foo) do
81
+ quux(:a, :z, :q)
82
+ end
83
+ fcc_foo.instance_eval(&passed_block)
84
+
85
+ @klass._flex_column_class_for(:foo).should be(fcc_foo)
86
+
87
+ instance = @klass.new
88
+ fcc_foo_instance = double("fcc_foo_instance")
89
+ expect(fcc_foo).to receive(:new).once.with(instance).and_return(fcc_foo_instance)
90
+ instance.foo.should be(fcc_foo_instance)
91
+
92
+
93
+
94
+ fcc_bar = double("fcc_bar")
95
+ expect(Class).to receive(:new).once.with(FlexColumns::Contents::FlexColumnContentsBase).and_return(fcc_bar)
96
+ expect(fcc_bar).to receive(:setup!).once.with(@klass, :bar, { })
97
+ allow(fcc_bar).to receive(:column_name).with().and_return(:bar)
98
+
99
+ expect(dmm).to receive(:remove_all_methods!).once.with()
100
+ expect(fcc_foo).to receive(:sync_methods!).once.with()
101
+ expect(fcc_bar).to receive(:sync_methods!).once.with()
102
+ @klass.flex_column(:bar)
103
+
104
+ @klass._flex_column_class_for(:foo).should be(fcc_foo)
105
+ @klass._flex_column_class_for(:bar).should be(fcc_bar)
106
+
107
+ fcc_bar_instance = double("fcc_bar_instance")
108
+ expect(fcc_bar).to receive(:new).once.with(instance).and_return(fcc_bar_instance)
109
+ instance.foo.should be(fcc_foo_instance)
110
+ instance.bar.should be(fcc_bar_instance)
111
+
112
+
113
+ fcc_foo_2 = double("fcc_foo_2")
114
+ expect(Class).to receive(:new).once.with(FlexColumns::Contents::FlexColumnContentsBase).and_return(fcc_foo_2)
115
+ expect(fcc_foo_2).to receive(:setup!).once.with(@klass, :foo, { :a => :b, :c => :d })
116
+ allow(fcc_foo_2).to receive(:column_name).with().and_return(:foo)
117
+
118
+ expect(dmm).to receive(:remove_all_methods!).once.with()
119
+ expect(fcc_foo_2).to receive(:sync_methods!).once.with()
120
+ expect(fcc_bar).to receive(:sync_methods!).once.with()
121
+
122
+ @klass.flex_column(:foo, :a => :b, :c => :d)
123
+
124
+ @klass._flex_column_class_for(:foo).should be(fcc_foo_2)
125
+ @klass._flex_column_class_for(:bar).should be(fcc_bar)
126
+
127
+ instance = @klass.new
128
+ fcc_foo_2_instance = double("fcc_foo_2_instance")
129
+ expect(fcc_foo_2).to receive(:new).once.with(instance).and_return(fcc_foo_2_instance)
130
+ instance.foo.should be(fcc_foo_2_instance)
131
+
132
+ instance = @klass.new
133
+ fcc_bar_instance = double("fcc_bar_instance")
134
+ expect(fcc_bar).to receive(:new).once.with(instance).and_return(fcc_bar_instance)
135
+ instance.bar.should be(fcc_bar_instance)
136
+ end
137
+ end
138
+
139
+ context "with two declared flex columns" do
140
+ before :each do
141
+ @fcc_foo = double("fcc_foo")
142
+ expect(Class).to receive(:new).once.with(FlexColumns::Contents::FlexColumnContentsBase).and_return(@fcc_foo)
143
+ expect(@fcc_foo).to receive(:setup!).once.with(@klass, :foo, { :aaa => :bbb, :ccc => :ddd })
144
+ expect(@fcc_foo).to receive(:sync_methods!).once
145
+
146
+ allow(@fcc_foo).to receive(:column_name).with().and_return(:foo)
147
+
148
+ @dmm = double("dmm")
149
+ expect(FlexColumns::Util::DynamicMethodsModule).to receive(:new).once.with(@klass, :FlexColumnsDynamicMethods).and_return(@dmm)
150
+ expect(@dmm).to receive(:remove_all_methods!).once.with()
151
+
152
+ @klass.flex_column(:foo, :aaa => :bbb, :ccc => :ddd)
153
+
154
+ @fcc_bar = double("fcc_bar")
155
+ expect(Class).to receive(:new).once.with(FlexColumns::Contents::FlexColumnContentsBase).and_return(@fcc_bar)
156
+ expect(@fcc_bar).to receive(:setup!).once.with(@klass, :bar, { })
157
+ expect(@fcc_bar).to receive(:sync_methods!).once
158
+
159
+ allow(@fcc_bar).to receive(:column_name).with().and_return(:bar)
160
+
161
+ expect(@dmm).to receive(:remove_all_methods!).once.with()
162
+ expect(@fcc_foo).to receive(:sync_methods!).once
163
+ @klass.flex_column(:bar)
164
+ end
165
+
166
+ it "should return the same DynamicMethodsModule every time" do
167
+ @klass._flex_column_dynamic_methods_module.should be(@dmm)
168
+ @klass._flex_column_dynamic_methods_module.should be(@dmm)
169
+ end
170
+
171
+ it "should call through on before_validation to only flex column objects that have been touched" do
172
+ instance = @klass.new
173
+ instance._flex_columns_before_validation!
174
+
175
+ fcc_foo_instance = double("fcc_foo_instance")
176
+ expect(@fcc_foo).to receive(:new).once.with(instance).and_return(fcc_foo_instance)
177
+ instance._flex_column_object_for(:foo).should be(fcc_foo_instance)
178
+ allow(fcc_foo_instance).to receive(:touched?).with().and_return(false)
179
+
180
+ instance._flex_columns_before_validation!
181
+
182
+
183
+ fcc_bar_instance = double("fcc_bar_instance")
184
+ expect(@fcc_bar).to receive(:new).once.with(instance).and_return(fcc_bar_instance)
185
+ instance._flex_column_object_for(:bar).should be(fcc_bar_instance)
186
+ allow(fcc_bar_instance).to receive(:touched?).with().and_return(true)
187
+
188
+ expect(fcc_bar_instance).to receive(:before_validation!).once.with()
189
+ instance._flex_columns_before_validation!
190
+
191
+ allow(fcc_foo_instance).to receive(:touched?).with().and_return(true)
192
+ allow(fcc_bar_instance).to receive(:touched?).with().and_return(true)
193
+
194
+ expect(fcc_foo_instance).to receive(:before_validation!).once.with()
195
+ expect(fcc_bar_instance).to receive(:before_validation!).once.with()
196
+ instance._flex_columns_before_validation!
197
+ end
198
+
199
+ it "should call through on before_save to only flex column objects that say they need it" do
200
+ instance = @klass.new
201
+ allow(@fcc_foo).to receive(:requires_serialization_on_save?).with(instance).and_return(false)
202
+ allow(@fcc_bar).to receive(:requires_serialization_on_save?).with(instance).and_return(false)
203
+ instance._flex_columns_before_save!
204
+
205
+ fcc_foo_instance = double("fcc_foo_instance")
206
+ expect(@fcc_foo).to receive(:new).once.with(instance).and_return(fcc_foo_instance)
207
+ instance._flex_column_object_for(:foo).should be(fcc_foo_instance)
208
+ allow(@fcc_foo).to receive(:requires_serialization_on_save?).with(instance).and_return(false)
209
+
210
+ instance._flex_columns_before_save!
211
+
212
+
213
+ fcc_bar_instance = double("fcc_bar_instance")
214
+ expect(@fcc_bar).to receive(:new).once.with(instance).and_return(fcc_bar_instance)
215
+ instance._flex_column_object_for(:bar).should be(fcc_bar_instance)
216
+ allow(@fcc_bar).to receive(:requires_serialization_on_save?).with(instance).and_return(true)
217
+
218
+ expect(fcc_bar_instance).to receive(:before_save!).once.with()
219
+ instance._flex_columns_before_save!
220
+
221
+ allow(@fcc_foo).to receive(:requires_serialization_on_save?).with(instance).and_return(true)
222
+ allow(@fcc_bar).to receive(:requires_serialization_on_save?).with(instance).and_return(true)
223
+
224
+ expect(fcc_foo_instance).to receive(:before_save!).once.with()
225
+ expect(fcc_bar_instance).to receive(:before_save!).once.with()
226
+ instance._flex_columns_before_save!
227
+ end
228
+
229
+ it "should return the flex-column class from #_flex_column_class_for" do
230
+ @klass._flex_column_class_for(:foo).should be(@fcc_foo)
231
+ @klass._flex_column_class_for(:bar).should be(@fcc_bar)
232
+ @klass._flex_column_class_for(:foo).should be(@fcc_foo)
233
+
234
+ e = capture_exception(FlexColumns::Errors::NoSuchColumnError) { @klass._flex_column_class_for(:baz) }
235
+ e.message.should match(/baz/)
236
+ e.message.should match(/foo/)
237
+ e.message.should match(/bar/)
238
+ e.message.should match(@klass_name)
239
+ end
240
+
241
+ it "should create a method that returns a new instance, but only once" do
242
+ instance = @klass.new
243
+ fcc_foo_instance = double("fcc_foo_instance")
244
+ expect(@fcc_foo).to receive(:new).once.with(instance).and_return(fcc_foo_instance)
245
+ instance.foo.should be(fcc_foo_instance)
246
+ instance.foo.should be(fcc_foo_instance)
247
+ end
248
+
249
+ it "should overwrite previous columns with new ones" do
250
+ fcc_conflicting = double("fcc_conflicting")
251
+ expect(Class).to receive(:new).once.with(FlexColumns::Contents::FlexColumnContentsBase).and_return(fcc_conflicting)
252
+ expect(fcc_conflicting).to receive(:setup!).once.with(@klass, :foo, { })
253
+ expect(fcc_conflicting).to receive(:sync_methods!).once
254
+ allow(fcc_conflicting).to receive(:column_name).with().and_return(:foo)
255
+
256
+ expect(@dmm).to receive(:remove_all_methods!).once.with()
257
+
258
+ expect(@fcc_bar).to receive(:sync_methods!).once
259
+ @klass.flex_column(:foo)
260
+
261
+ instance = @klass.new
262
+ fcc_instance = double("fcc_instance")
263
+ expect(fcc_conflicting).to receive(:new).once.with(instance).and_return(fcc_instance)
264
+ instance.foo.should be(fcc_instance)
265
+ instance.foo.should be(fcc_instance)
266
+ end
267
+
268
+ it "should create, and hold on to, flex-column objects properly" do
269
+ instance = @klass.new
270
+
271
+ fcc_foo_instance = double("fcc_foo_instance")
272
+ expect(@fcc_foo).to receive(:new).once.with(instance).and_return(fcc_foo_instance)
273
+ instance._flex_column_object_for(:foo).should be(fcc_foo_instance)
274
+
275
+ fcc_bar_instance = double("fcc_bar_instance")
276
+ expect(@fcc_bar).to receive(:new).once.with(instance).and_return(fcc_bar_instance)
277
+ instance._flex_column_object_for(:bar).should be(fcc_bar_instance)
278
+
279
+ instance._flex_column_object_for(:foo).should be(fcc_foo_instance)
280
+ instance._flex_column_object_for(' bAr ').should be(fcc_bar_instance)
281
+ end
282
+
283
+ it "should re-create flex-column objects on reload, and call super" do
284
+ @superclass.class_eval do
285
+ def reload
286
+ @reloads ||= 0
287
+ @reloads += 1
288
+ end
289
+
290
+ def reloads
291
+ @reloads ||= 0
292
+ end
293
+ end
294
+
295
+ instance = @klass.new
296
+
297
+ fcc_foo_instance = double("fcc_foo_instance")
298
+ expect(@fcc_foo).to receive(:new).once.with(instance).and_return(fcc_foo_instance)
299
+ instance._flex_column_object_for(:foo).should be(fcc_foo_instance)
300
+
301
+ fcc_bar_instance = double("fcc_bar_instance")
302
+ expect(@fcc_bar).to receive(:new).once.with(instance).and_return(fcc_bar_instance)
303
+ instance._flex_column_object_for(:bar).should be(fcc_bar_instance)
304
+
305
+ instance._flex_column_object_for(:foo).should be(fcc_foo_instance)
306
+ instance._flex_column_object_for(:bar).should be(fcc_bar_instance)
307
+
308
+ instance.reloads.should == 0
309
+ instance.reload
310
+ instance.reloads.should == 1
311
+
312
+ fcc_foo_instance_2 = double("fcc_foo_instance_2")
313
+ expect(@fcc_foo).to receive(:new).once.with(instance).and_return(fcc_foo_instance_2)
314
+ instance._flex_column_object_for(:foo).should be(fcc_foo_instance_2)
315
+
316
+ fcc_bar_instance_2 = double("fcc_bar_instance_2")
317
+ expect(@fcc_bar).to receive(:new).once.with(instance).and_return(fcc_bar_instance_2)
318
+ instance._flex_column_object_for(:bar).should be(fcc_bar_instance_2)
319
+ end
320
+
321
+ it "should tell you what flex-column names have been defined" do
322
+ @klass._all_flex_column_names.sort_by(&:to_s).should == [ :foo, :bar ].sort_by(&:to_s)
323
+ end
324
+
325
+ it "should normalize column names properly" do
326
+ @klass._flex_column_normalize_name(:baz).should == :baz
327
+ @klass._flex_column_normalize_name(:' bAz ').should == :baz
328
+ @klass._flex_column_normalize_name(' bAZ ').should == :baz
329
+ end
330
+
331
+ it "should create flex-column objects upon request that aren't attached to a model instance" do
332
+ fcc_foo_instance = double("fcc_foo_instance")
333
+ expect(@fcc_foo).to receive(:new).once.with(nil).and_return(fcc_foo_instance)
334
+ @klass.create_flex_object_from(:foo, nil).should be(fcc_foo_instance)
335
+
336
+ fcc_foo_instance = double("fcc_foo_instance")
337
+ expect(@fcc_foo).to receive(:new).once.with(" JSON string ").and_return(fcc_foo_instance)
338
+ @klass.create_flex_object_from(:foo, " JSON string ").should be(fcc_foo_instance)
339
+
340
+ fcc_bar_instance_1 = double("fcc_bar_instance_1")
341
+ expect(@fcc_bar).to receive(:new).once.with(nil).and_return(fcc_bar_instance_1)
342
+ fcc_bar_instance_2 = double("fcc_bar_instance_2")
343
+ expect(@fcc_bar).to receive(:new).once.with(" JSON string ").and_return(fcc_bar_instance_2)
344
+ @klass.create_flex_objects_from(:bar, [ nil, " JSON string " ]).should == [ fcc_bar_instance_1, fcc_bar_instance_2 ]
345
+ end
346
+ end
347
+
348
+ describe "flex-column objects" do
349
+ it "should prefer to just return super from _flex_column_object_for" do
350
+ superclass = Class.new do
351
+ def _flex_column_object_for(x)
352
+ "A_#{x}_Z"
353
+ end
354
+ end
355
+
356
+ subclass = Class.new(superclass)
357
+ allow(subclass).to receive(:before_validation).with(:_flex_columns_before_validation!)
358
+ allow(subclass).to receive(:before_save).with(:_flex_columns_before_save!)
359
+ subclass.send(:include, FlexColumns::HasFlexColumns)
360
+
361
+ instance = subclass.new
362
+ instance._flex_column_object_for(:foo).should == "A_foo_Z"
363
+ end
364
+ end
365
+ end