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,31 @@
1
+ module FlexColumns
2
+ module Util
3
+ # Contains a single method for abbreviating strings.
4
+ #
5
+ # Yes, this is very un-Ruby-like -- to define a separate utility function, rather than just adding a method to
6
+ # String. However, this method is used in such limited context (generating exception messages) that polluting the
7
+ # namespace of one of the most important classes in Ruby is probably a pretty bad idea.
8
+ class StringUtils
9
+ class << self
10
+ MAX_LENGTH_FOR_ABBREVIATED_STRING = 100
11
+ ABBREVIATED_STRING_SEPARATOR = "..."
12
+
13
+ # Returns a string of length no more than MAX_LENGTH_FOR_ABBREVIATED_STRING, by eliding, if necessary,
14
+ # characters from the middle. This is used when throwing exceptions: +flex_columns+ can generate very long
15
+ # strings of JSON data, and having many kilobytes (or even megabytes) of JSON make its way into an exception
16
+ # message is probably a really bad idea.
17
+ def abbreviated_string(s)
18
+ if s && s.length > MAX_LENGTH_FOR_ABBREVIATED_STRING
19
+ before_separator_length = ((MAX_LENGTH_FOR_ABBREVIATED_STRING - ABBREVIATED_STRING_SEPARATOR.length) / 2.0).floor
20
+ out = s[0..(before_separator_length - 1)] + ABBREVIATED_STRING_SEPARATOR
21
+ remaining = MAX_LENGTH_FOR_ABBREVIATED_STRING - out.length
22
+ out << s[(-remaining + 1)..-1]
23
+ out
24
+ else
25
+ s
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,4 @@
1
+ module FlexColumns
2
+ # The current version of FlexColumns.
3
+ VERSION = "1.0.0"
4
+ end
@@ -0,0 +1,174 @@
1
+ module FlexColumns
2
+ module Helpers
3
+ class DatabaseHelper
4
+ class InvalidDatabaseConfigurationError < StandardError; end
5
+
6
+ class << self
7
+ def maybe_database_gem_name
8
+ begin
9
+ dh = new
10
+ dh.database_gem_name
11
+ rescue InvalidDatabaseConfigurationError => idce
12
+ nil
13
+ end
14
+ end
15
+ end
16
+
17
+ def initialize
18
+ config # make sure we raise on instantiation if configuration is invalid
19
+ end
20
+
21
+ def database_type
22
+ case database_gem_name
23
+ when /mysql/i then :mysql
24
+ when /sqlite/i then :sqlite
25
+ when /pg/i, /postgres/i then :postgres
26
+ else raise "Unknown database type for Gem name: #{database_gem_name.inspect}"
27
+ end
28
+ end
29
+
30
+ def setup_activerecord!
31
+ require 'active_record'
32
+ require config[:require]
33
+ ::ActiveRecord::Base.establish_connection(config[:config])
34
+
35
+ require 'logger'
36
+ require 'stringio'
37
+ @logs = StringIO.new
38
+ ::ActiveRecord::Base.logger = Logger.new(@logs)
39
+
40
+ if config[:config][:adapter] == 'sqlite3'
41
+ sqlite_version = ::ActiveRecord::Base.connection.send(:sqlite_version).instance_variable_get("@version").inspect rescue "unknown"
42
+ end
43
+ end
44
+
45
+ def table_name(name)
46
+ "flexcols_spec_#{name}"
47
+ end
48
+
49
+ def database_gem_name
50
+ config[:database_gem_name]
51
+ end
52
+
53
+ private
54
+ def config
55
+ config_from_config_file || travis_ci_config_from_environment || invalid_config_file!
56
+ end
57
+
58
+ def config_from_config_file
59
+ return nil unless File.exist?(config_file_path)
60
+ require config_file_path
61
+
62
+ return nil unless defined?(FLEX_COLUMNS_SPEC_DATABASE_CONFIG)
63
+ return nil unless FLEX_COLUMNS_SPEC_DATABASE_CONFIG.kind_of?(Hash)
64
+
65
+ return nil unless FLEX_COLUMNS_SPEC_DATABASE_CONFIG[:require]
66
+ return nil unless FLEX_COLUMNS_SPEC_DATABASE_CONFIG[:database_gem_name]
67
+
68
+ return nil unless FLEX_COLUMNS_SPEC_DATABASE_CONFIG
69
+ FLEX_COLUMNS_SPEC_DATABASE_CONFIG
70
+ end
71
+
72
+ def travis_ci_config_from_environment
73
+ dbtype = (ENV['FLEX_COLUMNS_TRAVIS_CI_DATABASE_TYPE'] || '').strip.downcase
74
+ is_jruby = defined?(RUBY_ENGINE) && RUBY_ENGINE == 'jruby'
75
+
76
+ if is_jruby
77
+ case dbtype
78
+ when 'mysql'
79
+ {
80
+ :require => 'activerecord-jdbcmysql-adapter',
81
+ :database_gem_name => 'activerecord-jdbcmysql-adapter',
82
+ :config => {
83
+ :adapter => 'jdbcmysql',
84
+ :database => 'myapp_test',
85
+ :username => 'travis',
86
+ :encoding => 'utf8'
87
+ }
88
+ }
89
+ when 'postgres', 'postgresql'
90
+ {
91
+ :require => 'activerecord-jdbcpostgresql-adapter',
92
+ :database_gem_name => 'activerecord-jdbcpostgresql-adapter',
93
+ :config => {
94
+ :adapter => 'jdbcpostgresql',
95
+ :database => 'myapp_test',
96
+ :username => 'postgres'
97
+ }
98
+ }
99
+ when 'sqlite'
100
+ {
101
+ :require => 'activerecord-jdbcsqlite3-adapter',
102
+ :database_gem_name => 'activerecord-jdbcsqlite3-adapter',
103
+ :config => {
104
+ :adapter => 'jdbcsqlite3',
105
+ :database => ':memory:'
106
+ }
107
+ }
108
+ when '', nil then nil
109
+ else
110
+ raise "Unknown Travis CI database type: #{dbtype.inspect}"
111
+ end
112
+ else
113
+ case dbtype
114
+ when 'postgres', 'postgresql'
115
+ {
116
+ :require => 'pg',
117
+ :database_gem_name => 'pg',
118
+ :config => {
119
+ :adapter => 'postgresql',
120
+ :database => 'myapp_test',
121
+ :username => 'postgres',
122
+ :min_messages => 'WARNING'
123
+ }
124
+ }
125
+ when 'mysql'
126
+ {
127
+ :require => 'mysql2',
128
+ :database_gem_name => 'mysql2',
129
+ :config => {
130
+ :adapter => 'mysql2',
131
+ :database => 'myapp_test',
132
+ :username => 'travis',
133
+ :encoding => 'utf8'
134
+ }
135
+ }
136
+ when 'sqlite'
137
+ {
138
+ :require => 'sqlite3',
139
+ :database_gem_name => 'sqlite3',
140
+ :config => {
141
+ :adapter => 'sqlite3',
142
+ :database => ':memory:',
143
+ :timeout => 500
144
+ }
145
+ }
146
+ when '', nil then nil
147
+ else
148
+ raise "Unknown Travis CI database type: #{dbtype.inspect}"
149
+ end
150
+ end
151
+ end
152
+
153
+ def config_file_path
154
+ @config_file_path ||= File.expand_path(File.join(File.dirname(__FILE__), '..', '..', '..', 'spec_database_config.rb'))
155
+ end
156
+
157
+ def invalid_config_file!
158
+ raise Errno::ENOENT, %{In order to run specs for FlexColumns, you need to create a file at:
159
+
160
+ #{config_file_path}
161
+
162
+ ...that defines a top-level FLEX_COLUMNS_SPEC_DATABASE_CONFIG hash, with members:
163
+
164
+ :require => 'name_of_adapter_to_require',
165
+ :database_gem_name => 'name_of_gem_for_adapter',
166
+ :config => { ...whatever ActiveRecord::Base.establish_connection should be passed... }
167
+
168
+ Alternatively, if you're running under Travis CI, you can set the environment variable
169
+ FLEX_COLUMNS_TRAVIS_CI_DATABASE_TYPE to 'postgres', 'mysql', or 'sqlite', and it will
170
+ use the correct configuration for testing on Travis CI.}
171
+ end
172
+ end
173
+ end
174
+ end
@@ -0,0 +1,20 @@
1
+ module FlexColumns
2
+ module Helpers
3
+ module ExceptionHelpers
4
+ def capture_exception(required_class = Exception, &block)
5
+ e = nil
6
+ begin
7
+ block.call
8
+ rescue required_class => x
9
+ e = x
10
+ end
11
+
12
+ unless e
13
+ raise "Expected an exception of class #{required_class.inspect}, but none was raised"
14
+ end
15
+
16
+ e
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,47 @@
1
+ require 'active_record'
2
+ require 'active_record/migration'
3
+
4
+ module FlexColumns
5
+ module Helpers
6
+ module SystemHelpers
7
+ def migrate(&block)
8
+ migration_class = Class.new(::ActiveRecord::Migration)
9
+ metaclass = migration_class.class_eval { class << self; self; end }
10
+ metaclass.instance_eval { define_method(:up, &block) }
11
+
12
+ ::ActiveRecord::Migration.suppress_messages do
13
+ migration_class.migrate(:up)
14
+ end
15
+ end
16
+
17
+ def define_model_class(name, table_name, &block)
18
+ model_class = Class.new(::ActiveRecord::Base)
19
+ ::Object.send(:remove_const, name) if ::Object.const_defined?(name)
20
+ ::Object.const_set(name, model_class)
21
+ model_class.table_name = table_name
22
+ model_class.class_eval(&block)
23
+ end
24
+
25
+ def create_standard_system_spec_tables!
26
+ migrate do
27
+ drop_table :flexcols_spec_users rescue nil
28
+ create_table :flexcols_spec_users do |t|
29
+ t.string :name, :null => false
30
+ t.text :user_attributes
31
+ t.text :more_attributes
32
+ end
33
+ end
34
+ end
35
+
36
+ def create_standard_system_spec_models!
37
+ define_model_class(:User, 'flexcols_spec_users') { }
38
+ end
39
+
40
+ def drop_standard_system_spec_tables!
41
+ migrate do
42
+ drop_table :flexcols_spec_users rescue nil
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,245 @@
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
+ context "with a very simple column definition" do
19
+ before :each do
20
+ define_model_class(:User, 'flexcols_spec_users') do
21
+ flex_column :user_attributes do
22
+ field :wants_email
23
+ end
24
+ end
25
+ end
26
+
27
+ it "should be able to serialize and deserialize a very simple example" do
28
+ define_model_class(:UserBackdoor, 'flexcols_spec_users') { }
29
+
30
+ user = ::User.new
31
+ user.name = 'User 1'
32
+ user.user_attributes['wants_email'] = 'sometimes'
33
+ user.user_attributes['wants_email'].should == 'sometimes'
34
+ user.save!
35
+
36
+ user.user_attributes['wants_email'].should == 'sometimes'
37
+
38
+ user2 = ::User.find(user.id)
39
+ user2.user_attributes['wants_email'].should == 'sometimes'
40
+ user2.user_attributes.keys.should == [ :wants_email ]
41
+ end
42
+
43
+ it "should store its data as standard JSON" do
44
+ user = ::User.new
45
+ user.name = 'User 1'
46
+ user.user_attributes['wants_email'] = 'sometimes'
47
+ user.save!
48
+
49
+ bd = ::UserBackdoor.find(user.id)
50
+ bd.should be
51
+ bd.id.should == user.id
52
+
53
+ string = bd.user_attributes
54
+ string.should be
55
+ string.length.should > 0
56
+
57
+ contents = JSON.parse(string)
58
+ contents.class.should == Hash
59
+ contents.keys.should == [ 'wants_email' ]
60
+ contents['wants_email'].should == 'sometimes'
61
+ end
62
+
63
+ it "should not modify that JSON if you don't write to it with a different value, but should if you touch it" do
64
+ define_model_class(:UserBackdoor, 'flexcols_spec_users') { }
65
+
66
+ weirdly_spaced_json = ' { "wants_email" : "boop" } '
67
+
68
+ user_bd = ::UserBackdoor.new
69
+ user_bd.name = 'User 1'
70
+ user_bd.user_attributes = weirdly_spaced_json
71
+ user_bd.save!
72
+
73
+ user = ::User.find(user_bd.id)
74
+ user.name.should == 'User 1'
75
+ user.wants_email.should == 'boop'
76
+ user.wants_email = 'boop'
77
+ user.save!
78
+
79
+ user_bd_again = ::UserBackdoor.find(user_bd.id)
80
+ user_bd_again.name.should == 'User 1'
81
+ user_bd_again.user_attributes.should == weirdly_spaced_json
82
+
83
+ user.user_attributes.touch!
84
+ user.save!
85
+
86
+ user_bd_again = ::UserBackdoor.find(user_bd.id)
87
+ user_bd_again.name.should == 'User 1'
88
+ user_bd_again.user_attributes.should == '{"wants_email":"boop"}'
89
+ end
90
+
91
+ it "should provide access to attributes as methods" do
92
+ user = ::User.new
93
+ user.name = 'User 1'
94
+ user.user_attributes.wants_email = 'sometimes'
95
+ user.user_attributes.wants_email.should == 'sometimes'
96
+ user.user_attributes['wants_email'].should == 'sometimes'
97
+ user.save!
98
+
99
+ user.user_attributes.wants_email.should == 'sometimes'
100
+
101
+ user2 = ::User.find(user.id)
102
+ user2.user_attributes.wants_email.should == 'sometimes'
103
+ user2.user_attributes.keys.should == [ :wants_email ]
104
+ end
105
+
106
+ it "should delegate methods to attributes automatically" do
107
+ user = ::User.new
108
+ user.name = 'User 1'
109
+ user.wants_email = 'sometimes'
110
+ user.wants_email.should == 'sometimes'
111
+ user.user_attributes['wants_email'].should == 'sometimes'
112
+ user.save!
113
+
114
+ user.wants_email.should == 'sometimes'
115
+
116
+ user2 = ::User.find(user.id)
117
+ user2.wants_email.should == 'sometimes'
118
+ user2.user_attributes.keys.should == [ :wants_email ]
119
+ end
120
+
121
+ it "should have a reasonable class name for contents" do
122
+ class_name = ::User.new.user_attributes.class.name
123
+ class_name.should match(/^user::/i)
124
+ class_name.should match(/userattributes/i)
125
+ class_name.should match(/flexcontents/i)
126
+ end
127
+
128
+ it "should let you make flex-column accessors private one-by-one" do
129
+ define_model_class(:User, 'flexcols_spec_users') do
130
+ flex_column :user_attributes do
131
+ field :wants_email, :visibility => :private
132
+ field :another_thing
133
+ end
134
+ end
135
+
136
+ user = ::User.new
137
+ user.user_attributes.respond_to?(:wants_email).should_not be
138
+ lambda { user.user_attributes.wants_email }.should raise_error(NoMethodError)
139
+
140
+ user.user_attributes.send(:wants_email).should be_nil
141
+ user.user_attributes.send("wants_email=", "foobar").should == "foobar"
142
+ user.user_attributes.send(:wants_email).should == "foobar"
143
+
144
+ user.user_attributes.another_thing = 123
145
+ user.user_attributes.another_thing.should == 123
146
+
147
+ user.respond_to?(:wants_email).should_not be
148
+ user.respond_to?(:another_thing).should be
149
+ end
150
+
151
+ it "should let you make flex-column accessors private en masse, and override it one-by-one" do
152
+ define_model_class(:User, 'flexcols_spec_users') do
153
+ flex_column :user_attributes, :visibility => :private do
154
+ field :wants_email
155
+ field :another_thing, :visibility => :public
156
+ end
157
+ end
158
+
159
+ user = ::User.new
160
+ user.user_attributes.respond_to?(:wants_email).should_not be
161
+ lambda { user.user_attributes.wants_email }.should raise_error(NoMethodError)
162
+
163
+ user.user_attributes.send(:wants_email).should be_nil
164
+ user.user_attributes.send("wants_email=", "foobar").should == "foobar"
165
+ user.user_attributes.send(:wants_email).should == "foobar"
166
+
167
+ user.user_attributes.another_thing = 123
168
+ user.user_attributes.another_thing.should == 123
169
+
170
+ user.respond_to?(:wants_email).should_not be
171
+ user.respond_to?(:another_thing).should be
172
+ end
173
+
174
+ it "should return Symbols as Strings, so that saving to the database and reading from it doesn't produce a different result (since Symbols are stored in JSON as Strings)" do
175
+ user = ::User.new
176
+ user.name = 'User 1'
177
+ user.wants_email = :bonko
178
+ user.wants_email.should == 'bonko'
179
+ user.save!
180
+
181
+ user_again = ::User.find(user.id)
182
+ user_again.wants_email.should == 'bonko'
183
+ end
184
+
185
+ it "should allow storing an Array happily" do
186
+ user = ::User.new
187
+ user.name = 'User 1'
188
+ user.wants_email = [ 123, "foo", 47.2, { 'foo' => 'bar' } ]
189
+ user.save!
190
+
191
+ user_again = ::User.find(user.id)
192
+ user_again.wants_email.should == [ 123, 'foo', 47.2, { 'foo' => 'bar' } ]
193
+ end
194
+
195
+ it "should allow storing a Hash happily" do
196
+ user = ::User.new
197
+ user.name = 'User 1'
198
+ user.wants_email = { 'foo' => 47.2, '13' => 'bar', 'baz' => [ 'a', 'b', 'c' ] }
199
+ user.save!
200
+
201
+ user_again = ::User.find(user.id)
202
+ output = user_again.wants_email
203
+ output.class.should == Hash
204
+ output.keys.sort.should == [ '13', 'baz', 'foo' ].sort
205
+ output['13'].should == 'bar'
206
+ output['baz'].should == [ 'a', 'b', 'c' ]
207
+ output['foo'].should == 47.2
208
+ end
209
+
210
+ it "should remove keys entirely when they're set to nil, but not if they're set to false" do
211
+ define_model_class(:User, 'flexcols_spec_users') do
212
+ flex_column :user_attributes do
213
+ field :aaa
214
+ field :bbb
215
+ end
216
+ end
217
+
218
+ ::User.reset_column_information
219
+
220
+ user = ::User.new
221
+ user.name = 'User 1'
222
+ user.aaa = 'aaa1'
223
+ user.bbb = 'bbb1'
224
+ user.save!
225
+
226
+ user_bd = ::UserBackdoor.find(user.id)
227
+ JSON.parse(user_bd.user_attributes).keys.sort.should == %w{aaa bbb}.sort
228
+
229
+ user.aaa = false
230
+ user.save!
231
+
232
+ user_bd = ::UserBackdoor.find(user.id)
233
+ parsed = JSON.parse(user_bd.user_attributes)
234
+ parsed.keys.sort.should == %w{aaa bbb}.sort
235
+ parsed['aaa'].should == false
236
+
237
+ user.aaa = nil
238
+ user.save!
239
+
240
+ user_bd = ::UserBackdoor.find(user.id)
241
+ parsed = JSON.parse(user_bd.user_attributes)
242
+ parsed.keys.sort.should == %w{bbb}.sort
243
+ end
244
+ end
245
+ end