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