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,218 @@
1
+ require 'flex_columns'
2
+ require 'flex_columns/helpers/system_helpers'
3
+
4
+ describe "FlexColumns performance" do
5
+ include FlexColumns::Helpers::SystemHelpers
6
+
7
+ before :each do
8
+ @dh = FlexColumns::Helpers::DatabaseHelper.new
9
+ @dh.setup_activerecord!
10
+
11
+ create_standard_system_spec_tables!
12
+
13
+ define_model_class(:User, 'flexcols_spec_users') do
14
+ flex_column :user_attributes do
15
+ field :wants_email
16
+ end
17
+ end
18
+
19
+ @deserializations = [ ]
20
+ @serializations = [ ]
21
+
22
+ ds = @deserializations
23
+ s = @serializations
24
+
25
+ ActiveSupport::Notifications.subscribe('flex_columns.deserialize') do |name, start, finish, id, payload|
26
+ ds << payload
27
+ end
28
+
29
+ ActiveSupport::Notifications.subscribe('flex_columns.serialize') do |name, start, finish, id, payload|
30
+ s << payload
31
+ end
32
+ end
33
+
34
+ after :each do
35
+ drop_standard_system_spec_tables!
36
+ end
37
+
38
+ it "should fire a notification when deserializing and serializing" do
39
+ user = ::User.new
40
+ user.name = 'User 1'
41
+ user.wants_email = 'foo'
42
+
43
+ @serializations.length.should == 0
44
+ user.save!
45
+
46
+ @serializations.length.should == 1
47
+ @serializations[0].class.should == Hash
48
+ @serializations[0].keys.sort_by(&:to_s).should == [ :model_class, :model, :column_name ].sort_by(&:to_s)
49
+ @serializations[0][:model_class].should be(::User)
50
+ @serializations[0][:model].should be(user)
51
+ @serializations[0][:column_name].should == :user_attributes
52
+
53
+ user_again = ::User.find(user.id)
54
+
55
+ @deserializations.length.should == 0
56
+ user_again.wants_email.should == 'foo'
57
+ @deserializations.length.should == 1
58
+ @deserializations[0].class.should == Hash
59
+ @deserializations[0].keys.sort_by(&:to_s).should == [ :model_class, :model, :column_name, :raw_data ].sort_by(&:to_s)
60
+ @deserializations[0][:model_class].should be(::User)
61
+ @deserializations[0][:model].should be(user_again)
62
+ @deserializations[0][:column_name].should == :user_attributes
63
+ @deserializations[0][:raw_data].should == user_again.user_attributes.to_json
64
+ end
65
+
66
+ it "should not deserialize columns if they aren't touched" do
67
+ user = ::User.new
68
+ user.name = 'User 1'
69
+ user.wants_email = 'foo'
70
+ user.save!
71
+
72
+ user_again = ::User.find(user.id)
73
+ user_again.user_attributes.should be
74
+
75
+ @deserializations.length.should == 0
76
+ end
77
+
78
+ it "should not deserialize columns to run validations if there aren't any" do
79
+ user = ::User.new
80
+ user.name = 'User 1'
81
+ user.wants_email = 'foo'
82
+ user.save!
83
+
84
+ user_again = ::User.find(user.id)
85
+ user_again.user_attributes.should be
86
+ user_again.valid?.should be
87
+ user_again.user_attributes.valid?.should be
88
+
89
+ @deserializations.length.should == 0
90
+ end
91
+
92
+ it "should deserialize columns to run validations if there are any" do
93
+ define_model_class(:User, 'flexcols_spec_users') do
94
+ flex_column :user_attributes do
95
+ field :wants_email, :integer
96
+ end
97
+ end
98
+
99
+ user = ::User.new
100
+ user.name = 'User 1'
101
+ user.wants_email = 12345
102
+ user.save!
103
+
104
+ user_again = ::User.find(user.id)
105
+ user_again.user_attributes.should be
106
+
107
+ @deserializations.length.should == 0
108
+
109
+ user_again.valid?.should be
110
+ user_again.user_attributes.valid?.should be
111
+
112
+ @deserializations.length.should == 1
113
+ end
114
+
115
+ context "with NULLable and non-NULLable text and binary columns" do
116
+ def check_text_column_data(text_data, key, value)
117
+ parsed = JSON.parse(text_data)
118
+ parsed.keys.should == [ key.to_s ]
119
+ parsed[key.to_s].should == value
120
+ end
121
+
122
+ def check_binary_column_data(binary_data, key, value)
123
+ if binary_data =~ /^FC:01,0,/
124
+ without_header = binary_data[8..-1]
125
+ parsed = JSON.parse(without_header)
126
+ parsed.keys.should == [ key.to_s ]
127
+ parsed[key.to_s].should == value
128
+ end
129
+ end
130
+
131
+ before :each do
132
+ migrate do
133
+ drop_table :flexcols_spec_users rescue nil
134
+ create_table :flexcols_spec_users do |t|
135
+ t.string :name, :null => false
136
+ t.text :text_attrs_nonnull, :null => false
137
+ t.text :text_attrs_null
138
+ t.binary :binary_attrs_nonnull, :null => false
139
+ t.binary :binary_attrs_null
140
+ end
141
+ end
142
+
143
+ ::User.reset_column_information
144
+
145
+ define_model_class(:User, 'flexcols_spec_users') do
146
+ flex_column :text_attrs_nonnull do
147
+ field :aaa
148
+ end
149
+ flex_column :text_attrs_null do
150
+ field :bbb
151
+ end
152
+ flex_column :binary_attrs_nonnull do
153
+ field :ccc
154
+ end
155
+ flex_column :binary_attrs_null do
156
+ field :ddd
157
+ end
158
+ end
159
+
160
+ define_model_class(:UserBackdoor, 'flexcols_spec_users') { }
161
+
162
+ ::UserBackdoor.reset_column_information
163
+ end
164
+
165
+ it "should be smart enough to store an empty JSON string to the database, if necessary, if the column is non-NULL" do
166
+ # JRuby with the ActiveRecord-JDB adapter and MySQL seems to have the following issue: if you define a column as
167
+ # non-NULL, and create a new model instance, then ask that model instance for the value of that column, you get
168
+ # back empty string (""), not nil. Yet when trying to save that instance, you get an exception because it's not
169
+ # specifying that column at all. Only setting that column to a string with spaces in it (or something else) works,
170
+ # not even just setting it to the empty string again; as such, we're just going to give up on this example under
171
+ # those circumstances, rather than trying to work around this (pretty broken) behavior that's also a pretty rare
172
+ # edge case for us.
173
+ unless defined?(RUBY_ENGINE) && RUBY_ENGINE == 'jruby' && @dh.database_type == :mysql
174
+ my_user = ::User.new
175
+ my_user.name = 'User 1'
176
+ my_user.save!
177
+
178
+ user_bd = ::UserBackdoor.find(my_user.id)
179
+ user_bd.name.should == 'User 1'
180
+ user_bd.text_attrs_nonnull.should == ""
181
+ user_bd.text_attrs_null.should == nil
182
+ user_bd.binary_attrs_nonnull.should == ""
183
+ user_bd.binary_attrs_null.should == nil
184
+ end
185
+ end
186
+
187
+ it "should store NULL or the empty string in the database, as appropriate, if there's no data left any more" do
188
+ my_user = ::User.new
189
+ my_user.name = 'User 1'
190
+ my_user.aaa = 'aaa1'
191
+ my_user.bbb = 'bbb1'
192
+ my_user.ccc = 'ccc1'
193
+ my_user.ddd = 'ddd1'
194
+ my_user.save!
195
+
196
+ user_bd = ::UserBackdoor.find(my_user.id)
197
+ user_bd.name.should == 'User 1'
198
+ check_text_column_data(user_bd.text_attrs_nonnull, 'aaa', 'aaa1')
199
+ check_text_column_data(user_bd.text_attrs_null, 'bbb', 'bbb1')
200
+ check_binary_column_data(user_bd.binary_attrs_nonnull, 'ccc', 'ccc1')
201
+ check_binary_column_data(user_bd.binary_attrs_null, 'ddd', 'ddd1')
202
+
203
+ user_again = ::User.find(my_user.id)
204
+ user_again.aaa = nil
205
+ user_again.bbb = nil
206
+ user_again.ccc = nil
207
+ user_again.ddd = nil
208
+ user_again.save!
209
+
210
+ user_bd_again = ::UserBackdoor.find(my_user.id)
211
+ user_bd_again.name.should == 'User 1'
212
+ user_bd_again.text_attrs_nonnull.should == ""
213
+ user_bd_again.text_attrs_null.should == nil
214
+ user_bd_again.binary_attrs_nonnull.should == ""
215
+ user_bd_again.binary_attrs_null.should == nil
216
+ end
217
+ end
218
+ end
@@ -0,0 +1,85 @@
1
+ require 'flex_columns'
2
+ require 'flex_columns/helpers/system_helpers'
3
+
4
+ describe "FlexColumns PostgreSQL JSON column type support" do
5
+ include FlexColumns::Helpers::SystemHelpers
6
+
7
+ before :each do
8
+ @dh = FlexColumns::Helpers::DatabaseHelper.new
9
+ @dh.setup_activerecord!
10
+ end
11
+
12
+ dbtype = FlexColumns::Helpers::DatabaseHelper.new.database_type
13
+
14
+ if dbtype == :postgres
15
+ before :each do
16
+ create_table_error = nil
17
+
18
+ migrate do
19
+ drop_table :flexcols_spec_users rescue nil
20
+
21
+ begin
22
+ create_table :flexcols_spec_users do |t|
23
+ t.string :name, :null => false
24
+ t.column :user_attributes, :json
25
+ end
26
+ rescue ActiveRecord::StatementInvalid => si
27
+ create_table_error = si
28
+ end
29
+ end
30
+
31
+ @create_table_error = create_table_error
32
+ end
33
+
34
+ after :each do
35
+ migrate do
36
+ # drop_table :flexcols_spec_users rescue nil
37
+ end
38
+ end
39
+
40
+ it "should store JSON, without a binary header or compression, in a column typed of JSON" do
41
+ if @create_table_error
42
+ $stderr.puts "Skipping PostgreSQL test of JSON type, because PostgreSQL didn't seem to create our table successfully -- likely because its version is < 9.2, and thus has no support for the JSON type: #{@create_table_error.message} (#{@create_table_error.class.name})"
43
+ else
44
+ define_model_class(:User, 'flexcols_spec_users') do
45
+ flex_column :user_attributes do
46
+ field :wants_email
47
+ end
48
+ end
49
+
50
+ define_model_class(:UserBackdoor, 'flexcols_spec_users') { }
51
+
52
+ user = ::User.new
53
+ user.name = 'User 1'
54
+ user.wants_email = 'foo' * 10_000
55
+ user.save!
56
+
57
+ user_again = ::User.find(user.id)
58
+ user_again.name.should == 'User 1'
59
+ user_again.wants_email.should == 'foo' * 10_000
60
+
61
+ user_bd = ::UserBackdoor.find(user.id)
62
+ raw = user_bd.user_attributes
63
+
64
+ parsed = nil
65
+ if raw.kind_of?(String)
66
+ string.length.should > 30_000
67
+ string.should match(/^\s*\{/i)
68
+ parsed = JSON.parse(string)
69
+ elsif raw.kind_of?(Hash)
70
+ parsed = raw
71
+ else
72
+ raise "Unknown raw: #{raw.inspect}"
73
+ end
74
+
75
+ parsed['wants_email'].should == "foo" * 10_000
76
+ parsed.keys.should == [ 'wants_email' ]
77
+
78
+ if raw.kind_of?(Hash)
79
+ as_stored = user.user_attributes.to_stored_data
80
+ as_stored.class.should == Hash
81
+ end
82
+ end
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,93 @@
1
+ require 'flex_columns'
2
+ require 'flex_columns/helpers/system_helpers'
3
+
4
+ describe "FlexColumns basic operations" do
5
+ include FlexColumns::Helpers::SystemHelpers
6
+
7
+ before :each do
8
+ @dh = FlexColumns::Helpers::DatabaseHelper.new
9
+ @dh.setup_activerecord!
10
+
11
+ create_standard_system_spec_tables!
12
+ end
13
+
14
+ after :each do
15
+ drop_standard_system_spec_tables!
16
+ end
17
+
18
+ def should_fail_validation(field, value, pattern = nil)
19
+ object = ::User.new
20
+ object.some_integer = 123
21
+
22
+ object.send("#{field}=", value)
23
+
24
+ object.valid?.should_not be
25
+
26
+ full_name = "user_attributes.#{field}".to_sym
27
+ object.errors.keys.should == [ full_name ]
28
+
29
+ if pattern
30
+ object.errors[full_name].length.should == 1
31
+ object.errors[full_name][0].should match(pattern)
32
+ end
33
+ end
34
+
35
+ it "should allow 'types' as shorthand for validations" do
36
+ define_model_class(:User, 'flexcols_spec_users') do
37
+ flex_column :user_attributes do
38
+ field :some_integer, :integer, :null => false
39
+ field :some_string, :string, :limit => 100
40
+ field :some_string_2, :text
41
+ field :some_float, :float
42
+ field :some_float_2, :decimal
43
+ field :some_date, :date
44
+ field :some_time, :time
45
+ field :some_datetime, :datetime
46
+ field :some_timestamp, :timestamp
47
+ field :some_boolean, :boolean
48
+ field :some_enum, :enum => [ 'foo', 'bar', 'baz', nil ]
49
+ end
50
+ end
51
+
52
+ should_fail_validation(:some_integer, "foo", /is not a number/i)
53
+ should_fail_validation(:some_string, 12345, /must be a String/i)
54
+ should_fail_validation(:some_string_2, 12345, /must be a String/i)
55
+ should_fail_validation(:some_string, "foobar" * 100, /is too long/i)
56
+ should_fail_validation(:some_float, "foo", /is not a number/i)
57
+ should_fail_validation(:some_float_2, "foo", /is not a number/i)
58
+ should_fail_validation(:some_time, "foo", /must be a Time/i)
59
+ should_fail_validation(:some_datetime, "foo", /must be a Time/i)
60
+ should_fail_validation(:some_timestamp, "foo", /must be a Time/i)
61
+ should_fail_validation(:some_boolean, "true", /is not included in the list/i)
62
+ should_fail_validation(:some_enum, "quux", /is not included in the list/i)
63
+
64
+ user = ::User.new
65
+ user.name = 'User 1'
66
+
67
+ user.user_attributes.some_integer = 12345
68
+ user.user_attributes.some_string = "foo"
69
+ user.user_attributes.some_string_2 = "bar"
70
+ user.user_attributes.some_float = 5.2
71
+ user.user_attributes.some_float_2 = 10.7
72
+ user.user_attributes.some_date = Date.today
73
+ user.user_attributes.some_time = Time.now
74
+ user.user_attributes.some_datetime = 1.day.from_now
75
+ user.user_attributes.some_timestamp = 1.minute.from_now
76
+ user.user_attributes.some_boolean = true
77
+ user.user_attributes.some_enum = 'foo'
78
+
79
+ user.valid?.should be
80
+
81
+ user.user_attributes.some_string = ''
82
+ user.valid?.should be
83
+
84
+ user.user_attributes.some_float = 15
85
+ user.valid?.should be
86
+
87
+ user.user_attributes.some_boolean = nil
88
+ user.valid?.should be
89
+
90
+ user.user_attributes.some_string = :bonk
91
+ user.valid?.should be
92
+ end
93
+ end
@@ -0,0 +1,126 @@
1
+ require 'flex_columns'
2
+ require 'flex_columns/helpers/system_helpers'
3
+
4
+ describe "FlexColumns unknown fields" do
5
+ include FlexColumns::Helpers::SystemHelpers
6
+
7
+ before :each do
8
+ @dh = FlexColumns::Helpers::DatabaseHelper.new
9
+ @dh.setup_activerecord!
10
+
11
+ create_standard_system_spec_tables!
12
+
13
+ define_model_class(:UserBackdoor, 'flexcols_spec_users') do
14
+ flex_column :user_attributes do
15
+ field :wants_email
16
+ field :some_unknown_attribute
17
+ end
18
+ end
19
+
20
+ @user_bd = ::UserBackdoor.new
21
+ @user_bd.name = 'User 1'
22
+ @user_bd.some_unknown_attribute = 'bongo'
23
+ @user_bd.save!
24
+ end
25
+
26
+ after :each do
27
+ drop_standard_system_spec_tables!
28
+ end
29
+
30
+ it "should preserve unknown fields by default" do
31
+ define_model_class(:User, 'flexcols_spec_users') do
32
+ flex_column :user_attributes do
33
+ field :wants_email
34
+ end
35
+ end
36
+
37
+ user = ::User.find(@user_bd.id)
38
+ user.name.should == 'User 1'
39
+ user.wants_email.should be_nil
40
+ user.wants_email = 'does_want'
41
+
42
+ user.respond_to?(:some_unknown_attribute).should_not be
43
+ lambda { user.send(:some_unknown_attribute) }.should raise_error(NoMethodError)
44
+ lambda { user.user_attributes[:some_unknown_attribute] }.should raise_error(FlexColumns::Errors::NoSuchFieldError)
45
+ lambda { user.user_attributes[:some_unknown_attribute] = 123 }.should raise_error(FlexColumns::Errors::NoSuchFieldError)
46
+
47
+ user.save!
48
+
49
+ user_bd_again = ::UserBackdoor.find(@user_bd.id)
50
+ user_bd_again.wants_email.should == 'does_want'
51
+ user_bd_again.some_unknown_attribute.should == 'bongo'
52
+ end
53
+
54
+ it "should delete unknown fields if asked to" do
55
+ define_model_class(:User, 'flexcols_spec_users') do
56
+ flex_column :user_attributes, :unknown_fields => :delete do
57
+ field :wants_email
58
+ end
59
+ end
60
+
61
+ user = ::User.find(@user_bd.id)
62
+ user.name.should == 'User 1'
63
+ user.wants_email.should be_nil
64
+ user.wants_email = 'does_want'
65
+
66
+ user.respond_to?(:some_unknown_attribute).should_not be
67
+ lambda { user.send(:some_unknown_attribute) }.should raise_error(NoMethodError)
68
+ lambda { user.user_attributes[:some_unknown_attribute] }.should raise_error(FlexColumns::Errors::NoSuchFieldError)
69
+ lambda { user.user_attributes[:some_unknown_attribute] = 123 }.should raise_error(FlexColumns::Errors::NoSuchFieldError)
70
+
71
+ user.save!
72
+
73
+ user_bd_again = ::UserBackdoor.find(@user_bd.id)
74
+ user_bd_again.wants_email.should == 'does_want'
75
+ user_bd_again.some_unknown_attribute.should be_nil
76
+ end
77
+
78
+ it "should not delete unknown fields if asked to, but we only read from the model" do
79
+ @user_bd.wants_email = 'foo'
80
+ @user_bd.save!
81
+
82
+ define_model_class(:User, 'flexcols_spec_users') do
83
+ flex_column :user_attributes, :unknown_fields => :delete do
84
+ field :wants_email
85
+ end
86
+ end
87
+
88
+ user = ::User.find(@user_bd.id)
89
+ user.name.should == 'User 1'
90
+ user.wants_email.should == 'foo'
91
+
92
+ user.respond_to?(:some_unknown_attribute).should_not be
93
+ lambda { user.send(:some_unknown_attribute) }.should raise_error(NoMethodError)
94
+ lambda { user.user_attributes[:some_unknown_attribute] }.should raise_error(FlexColumns::Errors::NoSuchFieldError)
95
+ lambda { user.user_attributes[:some_unknown_attribute] = 123 }.should raise_error(FlexColumns::Errors::NoSuchFieldError)
96
+
97
+ user.save!
98
+
99
+ user_bd_again = ::UserBackdoor.find(@user_bd.id)
100
+ user_bd_again.wants_email.should == 'foo'
101
+ user_bd_again.some_unknown_attribute.should == 'bongo'
102
+ end
103
+
104
+ it "should have a method that explicitly will purge unknown methods, even if deserialization hasn't happened for any other reason, but not before then" do
105
+ define_model_class(:User, 'flexcols_spec_users') do
106
+ flex_column :user_attributes, :unknown_fields => :delete do
107
+ field :wants_email
108
+ end
109
+ end
110
+
111
+ user = ::User.find(@user_bd.id)
112
+ user.save!
113
+
114
+ user_bd_again = ::UserBackdoor.find(@user_bd.id)
115
+ user_bd_again.wants_email.should be_nil
116
+ user_bd_again.some_unknown_attribute.should == 'bongo'
117
+
118
+ user = ::User.find(@user_bd.id)
119
+ user.user_attributes.touch!
120
+ user.save!
121
+
122
+ user_bd_again = ::UserBackdoor.find(@user_bd.id)
123
+ user_bd_again.wants_email.should be_nil
124
+ user_bd_again.some_unknown_attribute.should be_nil
125
+ end
126
+ end