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,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