has_metadata 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/.document ADDED
@@ -0,0 +1,5 @@
1
+ README.rdoc
2
+ lib/**/*.rb
3
+ bin/*
4
+ features/**/*.feature
5
+ LICENSE
data/.gitignore ADDED
@@ -0,0 +1,26 @@
1
+ ## MAC OS
2
+ .DS_Store
3
+
4
+ ## TEXTMATE
5
+ *.tmproj
6
+ tmtags
7
+
8
+ ## EMACS
9
+ *~
10
+ \#*
11
+ .\#*
12
+
13
+ ## VIM
14
+ *.swp
15
+
16
+ ## PROJECT::GENERAL
17
+ coverage
18
+ rdoc
19
+ pkg
20
+ .bundle
21
+ .rvmrc
22
+ test.sqlite
23
+
24
+ ## PROJECT::DOCUMENTATION
25
+ .yardoc
26
+ doc
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ -cfs
data/Gemfile ADDED
@@ -0,0 +1,14 @@
1
+ source :rubygems
2
+
3
+ # DEPENDENCIES
4
+ gem 'rails', '>= 3.0'
5
+
6
+ # DEVELOPMENT
7
+ gem 'jeweler'
8
+ gem 'yard'
9
+ gem 'RedCloth', require: 'redcloth'
10
+ gem 'sqlite3'
11
+
12
+ # TEST
13
+ gem 'rspec'
14
+
data/Gemfile.lock ADDED
@@ -0,0 +1,103 @@
1
+ GEM
2
+ remote: http://rubygems.org/
3
+ specs:
4
+ RedCloth (4.2.3)
5
+ abstract (1.0.0)
6
+ actionmailer (3.0.1)
7
+ actionpack (= 3.0.1)
8
+ mail (~> 2.2.5)
9
+ actionpack (3.0.1)
10
+ activemodel (= 3.0.1)
11
+ activesupport (= 3.0.1)
12
+ builder (~> 2.1.2)
13
+ erubis (~> 2.6.6)
14
+ i18n (~> 0.4.1)
15
+ rack (~> 1.2.1)
16
+ rack-mount (~> 0.6.12)
17
+ rack-test (~> 0.5.4)
18
+ tzinfo (~> 0.3.23)
19
+ activemodel (3.0.1)
20
+ activesupport (= 3.0.1)
21
+ builder (~> 2.1.2)
22
+ i18n (~> 0.4.1)
23
+ activerecord (3.0.1)
24
+ activemodel (= 3.0.1)
25
+ activesupport (= 3.0.1)
26
+ arel (~> 1.0.0)
27
+ tzinfo (~> 0.3.23)
28
+ activeresource (3.0.1)
29
+ activemodel (= 3.0.1)
30
+ activesupport (= 3.0.1)
31
+ activesupport (3.0.1)
32
+ arel (1.0.1)
33
+ activesupport (~> 3.0.0)
34
+ builder (2.1.2)
35
+ diff-lcs (1.1.2)
36
+ erubis (2.6.6)
37
+ abstract (>= 1.0.0)
38
+ ffi (0.6.3)
39
+ rake (>= 0.8.7)
40
+ gemcutter (0.6.1)
41
+ git (1.2.5)
42
+ i18n (0.4.2)
43
+ jeweler (1.4.0)
44
+ gemcutter (>= 0.1.0)
45
+ git (>= 1.2.5)
46
+ rubyforge (>= 2.0.0)
47
+ json_pure (1.4.6)
48
+ mail (2.2.9)
49
+ activesupport (>= 2.3.6)
50
+ i18n (~> 0.4.1)
51
+ mime-types (~> 1.16)
52
+ treetop (~> 1.4.8)
53
+ mime-types (1.16)
54
+ polyglot (0.3.1)
55
+ rack (1.2.1)
56
+ rack-mount (0.6.13)
57
+ rack (>= 1.0.0)
58
+ rack-test (0.5.6)
59
+ rack (>= 1.0)
60
+ rails (3.0.1)
61
+ actionmailer (= 3.0.1)
62
+ actionpack (= 3.0.1)
63
+ activerecord (= 3.0.1)
64
+ activeresource (= 3.0.1)
65
+ activesupport (= 3.0.1)
66
+ bundler (~> 1.0.0)
67
+ railties (= 3.0.1)
68
+ railties (3.0.1)
69
+ actionpack (= 3.0.1)
70
+ activesupport (= 3.0.1)
71
+ rake (>= 0.8.4)
72
+ thor (~> 0.14.0)
73
+ rake (0.8.7)
74
+ rspec (2.0.1)
75
+ rspec-core (~> 2.0.1)
76
+ rspec-expectations (~> 2.0.1)
77
+ rspec-mocks (~> 2.0.1)
78
+ rspec-core (2.0.1)
79
+ rspec-expectations (2.0.1)
80
+ diff-lcs (>= 1.1.2)
81
+ rspec-mocks (2.0.1)
82
+ rspec-core (~> 2.0.1)
83
+ rspec-expectations (~> 2.0.1)
84
+ rubyforge (2.0.4)
85
+ json_pure (>= 1.1.7)
86
+ sqlite3 (0.1.1)
87
+ ffi (>= 0.6.3)
88
+ thor (0.14.3)
89
+ treetop (1.4.8)
90
+ polyglot (>= 0.3.1)
91
+ tzinfo (0.3.23)
92
+ yard (0.6.1)
93
+
94
+ PLATFORMS
95
+ ruby
96
+
97
+ DEPENDENCIES
98
+ RedCloth
99
+ jeweler
100
+ rails (>= 3.0)
101
+ rspec
102
+ sqlite3
103
+ yard
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2009 Tim Morgan
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.textile ADDED
@@ -0,0 +1,105 @@
1
+ h1. has_metadata -- Keep your tables narrow
2
+
3
+ | *Author* | Tim Morgan |
4
+ | *Version* | 1.0 (Oct 29, 2010) |
5
+ | *License* | Released under the MIT License. |
6
+
7
+ h2. About
8
+
9
+ Wide tables are a problem for big databases. If your @ActiveRecord@ models have
10
+ 10, maybe 15 columns, some of which are @VARCHARs@ or maybe even @TEXTs@, it's
11
+ going to slow your queries down when you start to scale up.
12
+
13
+ The easy solution to this problem is to limit your projections; in other words,
14
+ to only @SELECT@ the columns that you actually need. If you've got a @users@
15
+ table with a giant @about_me@ text column, and you're only trying to look up the
16
+ user's login, then just select the @login@ column.
17
+
18
+ In the long run, though, a superior solution is to just move those
19
+ @about_me@-type columns to a completely different table. This table has just one
20
+ JSON-serialized field, making it schemaless, so it doesn't waste space. Each row
21
+ in this table is associated with a record in another table (@Metadata@ @has_one@
22
+ of your models).
23
+
24
+ This way, when your website gets huge, all of your giant, freeform data is in
25
+ one table that you can shard, or move off to an alternate database, or even a
26
+ NoSQL-type document store, or otherwise manage as you please. Your relational
27
+ tables remain slim and efficient, containing only columns that a) are indexed,
28
+ or b) you need frequent access to.
29
+
30
+ This gem includes a generator that creates the @Metadata@ model, and a module
31
+ that you can include in your models to define which fields have been spun off to
32
+ the metadata record.
33
+
34
+ h2. Installation
35
+
36
+ *Important Note:* This gem is only compatible with Ruby 1.9 and Rails 3.0.
37
+
38
+ Firstly, add the gem to your Rails project's @Gemfile@:
39
+
40
+ <pre><code>
41
+ gem 'has_metadata'
42
+ </code></pre>
43
+
44
+ Next, run the generator, which will add the @Metadata@ model and its migration
45
+ to your application.
46
+
47
+ <pre><code>
48
+ rails generate metadata
49
+ </code></pre>
50
+
51
+ h2. Usage
52
+
53
+ The first thing to think about is what columns to keep in your model. You will
54
+ need to keep any indexed columns, or any columns you perform lookups or other
55
+ SQL queries with. You should also keep any frequently accessed columns,
56
+ especially if they are small (integers or booleans). Good candidates for the
57
+ metadata table are the @TEXT@- and @VARCHAR@-type columns that you only need to
58
+ render a page or two in your app.
59
+
60
+ You'll need to change your model's schema so that it has a @metadata_id@ column
61
+ that will associate the model with its @Metadata@ instance:
62
+
63
+ <pre><code>
64
+ t.belongs_to :metadata
65
+ </code></pre>
66
+
67
+ Next, include the @HasMetadata@ module in your model, and call the
68
+ @has_metadata@ method to define the schema of your metadata. You can get more
69
+ information in the @has_metadata@ documentation, but for starters, here's a
70
+ basic example:
71
+
72
+ <pre><code>
73
+ class User < ActiveRecord::Base
74
+ include HasMetadata
75
+ has_metadata({
76
+ about_me: { type: String, length: { maximum: 512 } },
77
+ birthdate: { type: Date, presence: true },
78
+ zipcode: { type: Number, numericality: { greater_than: 9999, less_than: 10_000_} }
79
+ })
80
+ end
81
+ </code></pre>
82
+
83
+ As you can see, you pass field names mapped to a hash. The hash describes the
84
+ validation that will be performed, and is in the same format as a call to
85
+ @validates@. In addition to the @EachValidator@ keys shown above, you can also
86
+ pass a @type@ key, to constrain the Ruby type that can be assigned to the field.
87
+
88
+ Each of these fields (in this case, @about_me@, @birthdate@, and @zipcode@) can
89
+ be accessed and set as first_level methods on an instance of your model:
90
+
91
+ <pre><code>
92
+ user.about_me #=> "I was born in 1982 in Aberdeen. My father was a carpenter from..."
93
+ </code></pre>
94
+
95
+ ... and thus, used as part of @form_for@ fields:
96
+
97
+ <pre><code>
98
+ form_for user do |f|
99
+ f.text_area :about_me, rows: 5, cols: 80
100
+ end
101
+ </code></pre>
102
+
103
+ The only thing you _can't_ do is use these fields in a query, obviously. You
104
+ can't do something like @User.where(zipcode: 90210)@, because that column
105
+ doesn't exist on the @users@ table.
data/Rakefile ADDED
@@ -0,0 +1,36 @@
1
+ require 'rake'
2
+ begin
3
+ require 'bundler'
4
+ rescue LoadError
5
+ puts "Bundler is not installed; install with `gem install bundler`."
6
+ exit 1
7
+ end
8
+
9
+ Bundler.require :default
10
+
11
+ Jeweler::Tasks.new do |gem|
12
+ gem.name = "has_metadata"
13
+ gem.summary = %Q{Reduce your table width by moving non-indexed columns to a separate metadata table}
14
+ gem.description = %Q{has_metadata lets you move non-indexed and weighty columns off of your big tables by creating a separate metadata table to store all this extra information. Works with Ruby 1.9. and Rails 3.0.}
15
+ gem.email = "git@timothymorgan.info"
16
+ gem.homepage = "http://github.com/riscfuture/has_metadata"
17
+ gem.authors = [ "Tim Morgan" ]
18
+ gem.required_ruby_version = '>= 1.9'
19
+ gem.add_dependency "rails", ">= 3.0"
20
+ end
21
+ Jeweler::GemcutterTasks.new
22
+
23
+ require 'rspec/core/rake_task'
24
+ RSpec::Core::RakeTask.new
25
+
26
+ YARD::Rake::YardocTask.new('doc') do |doc|
27
+ doc.options << "-m" << "textile"
28
+ doc.options << "--protected"
29
+ doc.options << "-r" << "README.textile"
30
+ doc.options << "-o" << "doc"
31
+ doc.options << "--title" << "has_metadata Documentation".inspect
32
+
33
+ doc.files = [ 'lib/**/*', 'README.textile', 'templates/metadata.rb' ]
34
+ end
35
+
36
+ task(default: :spec)
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 1.0.0
@@ -0,0 +1,63 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE DIRECTLY
3
+ # Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command
4
+ # -*- encoding: utf-8 -*-
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = %q{has_metadata}
8
+ s.version = "1.0.0"
9
+
10
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
+ s.authors = ["Tim Morgan"]
12
+ s.date = %q{2010-10-30}
13
+ s.description = %q{has_metadata lets you move non-indexed and weighty columns off of your big tables by creating a separate metadata table to store all this extra information. Works with Ruby 1.9. and Rails 3.0.}
14
+ s.email = %q{git@timothymorgan.info}
15
+ s.extra_rdoc_files = [
16
+ "LICENSE",
17
+ "README.textile"
18
+ ]
19
+ s.files = [
20
+ ".document",
21
+ ".gitignore",
22
+ ".rspec",
23
+ "Gemfile",
24
+ "Gemfile.lock",
25
+ "LICENSE",
26
+ "README.textile",
27
+ "Rakefile",
28
+ "VERSION",
29
+ "has_metadata.gemspec",
30
+ "lib/has_metadata.rb",
31
+ "lib/metadata_generator.rb",
32
+ "spec/has_metadata_spec.rb",
33
+ "spec/metadata_spec.rb",
34
+ "spec/spec_helper.rb",
35
+ "templates/create_metadata.rb",
36
+ "templates/metadata.rb"
37
+ ]
38
+ s.homepage = %q{http://github.com/riscfuture/has_metadata}
39
+ s.rdoc_options = ["--charset=UTF-8"]
40
+ s.require_paths = ["lib"]
41
+ s.required_ruby_version = Gem::Requirement.new(">= 1.9")
42
+ s.rubygems_version = %q{1.3.7}
43
+ s.summary = %q{Reduce your table width by moving non-indexed columns to a separate metadata table}
44
+ s.test_files = [
45
+ "spec/has_metadata_spec.rb",
46
+ "spec/metadata_spec.rb",
47
+ "spec/spec_helper.rb"
48
+ ]
49
+
50
+ if s.respond_to? :specification_version then
51
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
52
+ s.specification_version = 3
53
+
54
+ if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
55
+ s.add_runtime_dependency(%q<rails>, [">= 3.0"])
56
+ else
57
+ s.add_dependency(%q<rails>, [">= 3.0"])
58
+ end
59
+ else
60
+ s.add_dependency(%q<rails>, [">= 3.0"])
61
+ end
62
+ end
63
+
@@ -0,0 +1,118 @@
1
+ require 'metadata_generator'
2
+
3
+ # @private
4
+ class Object
5
+
6
+ # Creates a deep copy of this object.
7
+ #
8
+ # @raise [TypeError] If the object cannot be deep-copied. All objects that can
9
+ # be marshalled can be deep-copied.
10
+
11
+ def deep_clone
12
+ Marshal.load Marshal.dump(self)
13
+ end
14
+ end
15
+
16
+
17
+ # Provides the {ClassMethods#has_metadata} method to subclasses of @ActiveRecord::Base@.
18
+
19
+ module HasMetadata
20
+ extend ActiveSupport::Concern
21
+
22
+ # Class methods that are added to your model.
23
+
24
+ module ClassMethods
25
+
26
+ # Defines a set of fields whose values exist in the associated {Metadata}
27
+ # record. Each key in the @fields@ hash is the name of a metadata field, and
28
+ # the value is a set of options to pass to the @validates@ method. If you do
29
+ # not want to perform any validation on a field, simply pass @true@ as its
30
+ # key value.
31
+ #
32
+ # In addition to the normal @validates@ keys, you can also include a @:type@
33
+ # key to restrict values to certain classes.
34
+ #
35
+ # @param [Hash<Symbol, Hash>] fields A mapping of field names to their validation options (and/or the @:type@ key).
36
+ #
37
+ # @example Three metadata fields, one basic, one validated, and one type-checked.
38
+ # has_metadata(optional: true, required: { presence: true }, number: { type: Fixnum })
39
+
40
+ def has_metadata(fields)
41
+ belongs_to :metadata, dependent: :destroy
42
+ accepts_nested_attributes_for :metadata
43
+ #after_save :save_metadata, if: :metadata_changed?
44
+ class_inheritable_hash :metadata_fields
45
+ self.metadata_fields = fields.deep_clone
46
+
47
+ #define_method(:save_metadata) { metadata.save! }
48
+ #define_method(:metadata_changed?) { metadata and metadata.changed? }
49
+
50
+ fields.each do |name, options|
51
+ delegate name, to: :metadata!
52
+ delegate :"#{name}=", to: :metadata!
53
+
54
+ if options.kind_of?(Hash) then
55
+ type = options.delete(:type)
56
+ validate do |obj|
57
+ errors.add(name, :incorrect_type) unless
58
+ metadata_typecast(obj.send(name), type).kind_of?(type) or
59
+ ((options[:allow_nil] and obj.send(name).nil?) or (options[:allow_blank] and obj.send(name).blank?))
60
+ end if type
61
+ validates(name, options) unless options.empty? or (options.keys - [ :allow_nil, :allow_blank ]).empty?
62
+ end
63
+ end
64
+ end
65
+ end
66
+
67
+ # Instance methods that are added to your model.
68
+
69
+ module InstanceMethods
70
+
71
+ # @private
72
+ def metadata_typecast(value, type)
73
+ if value.kind_of?(String) then
74
+ if type == Integer or type == Fixnum then return value.to_i
75
+ elsif type == Float then return value.to_f end
76
+ end
77
+ return value
78
+ end
79
+
80
+ # @private
81
+ def assign_multiparameter_attributes(pairs)
82
+ fake_attributes = pairs.select { |(field, _)| self.class.metadata_fields.include? field[0, field.index('(')].to_sym }
83
+
84
+ fake_attributes.group_by { |(field, _)| field[0, field.index('(')] }.each do |field_name, parts|
85
+ options = self.class.metadata_fields[field_name.to_sym]
86
+ if options[:type] then
87
+ args = parts.each_with_object([]) do |(part_name, value), ary|
88
+ part_ann = part_name[part_name.index('(') + 1, part_name.length]
89
+ index = part_ann.to_i - 1
90
+ raise "Out-of-bounds multiparameter argument index" unless index >= 0
91
+ ary[index] = if value.blank? then nil
92
+ elsif part_ann.ends_with?('i)') then value.to_i
93
+ elsif part_ann.ends_with?('f)') then value.to_f
94
+ else value end
95
+ end
96
+ args.compact!
97
+ send :"#{field_name}=", options[:type].new(*args) unless args.empty?
98
+ else
99
+ raise "#{field_name} has no type and cannot be used for multiparameter assignment"
100
+ end
101
+ end
102
+
103
+ super(pairs - fake_attributes)
104
+ end
105
+
106
+ # @return [Metadata] An existing associated {Metadata} instance, or new, saved one if none was found.
107
+
108
+ def metadata!
109
+ if instance_variables.include?(:@metadata) then
110
+ metadata.set_fields self.class.metadata_fields
111
+ else
112
+ Metadata.transaction do
113
+ (metadata || create_metadata).set_fields self.class.metadata_fields
114
+ end
115
+ end
116
+ end
117
+ end
118
+ end
@@ -0,0 +1,23 @@
1
+ require 'rails/generators'
2
+ require 'rails/generators'
3
+ require 'rails/generators/migration'
4
+
5
+ # @private
6
+ class MetadataGenerator < Rails::Generators::Base
7
+ include Rails::Generators::Migration
8
+
9
+ source_root "#{File.dirname __FILE__}/../templates"
10
+
11
+ def self.next_migration_number(dirname)
12
+ if ActiveRecord::Base.timestamped_migrations then
13
+ Time.now.utc.strftime "%Y%m%d%H%M%S"
14
+ else
15
+ "%.3d" % (current_migration_number(dirname) + 1)
16
+ end
17
+ end
18
+
19
+ def copy_files
20
+ copy_file "metadata.rb", "app/models/metadata.rb"
21
+ migration_template "create_metadata.rb", "db/migrate/create_metadata.rb"
22
+ end
23
+ end
@@ -0,0 +1,125 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
2
+
3
+ module SpecSupport
4
+ class ConstructorTester
5
+ attr_reader :args
6
+ def initialize(*args) @args = args end
7
+ end
8
+
9
+ class HasMetadataTester < ActiveRecord::Base
10
+ include HasMetadata
11
+ set_table_name 'users'
12
+ has_metadata({
13
+ untyped: {},
14
+ can_be_nil: { type: Date, allow_nil: true },
15
+ can_be_blank: { type: Date, allow_blank: true },
16
+ number: { numericality: true },
17
+ multiparam: { type: SpecSupport::ConstructorTester }
18
+ })
19
+ end
20
+ end
21
+
22
+ describe HasMetadata do
23
+ describe "#has_metadata" do
24
+ it "should add a :metadata association" do
25
+ SpecSupport::HasMetadataTester.reflect_on_association(:metadata).macro.should eql(:belongs_to)
26
+ end
27
+
28
+ it "should set the model to accept nested attributes for :metadata" do
29
+ SpecSupport::HasMetadataTester.nested_attributes_options[:metadata].should_not be_nil
30
+ end
31
+
32
+ it "should make a getter for each field" do
33
+ SpecSupport::HasMetadataTester.new.should respond_to(:untyped)
34
+ SpecSupport::HasMetadataTester.new.should respond_to(:multiparam)
35
+ SpecSupport::HasMetadataTester.new.should respond_to(:number)
36
+ end
37
+
38
+ context "getters" do
39
+ before :each do
40
+ @object = SpecSupport::HasMetadataTester.new
41
+ @metadata = @object.metadata!
42
+ end
43
+
44
+ it "should return a field in the metadata object" do
45
+ @metadata.data[:untyped] = 'bar'
46
+ @object.untyped.should eql('bar')
47
+ end
48
+
49
+ it "should return nil if there is no associated metadata" do
50
+ @object.stub!(:metadata).and_return(nil)
51
+ ivars = @object.instance_variables - [ :@metadata ]
52
+ @object.stub!(:instance_variables).and_return(ivars)
53
+
54
+ @object.untyped.should be_nil
55
+ end
56
+ end
57
+
58
+ it "should make a setter for each field" do
59
+ SpecSupport::HasMetadataTester.new.should respond_to(:untyped=)
60
+ SpecSupport::HasMetadataTester.new.should respond_to(:multiparam=)
61
+ SpecSupport::HasMetadataTester.new.should respond_to(:number=)
62
+ end
63
+
64
+ context "setters" do
65
+ before :each do
66
+ @object = SpecSupport::HasMetadataTester.new
67
+ @metadata = @object.metadata!
68
+ end
69
+
70
+ it "should set the value in the metadata object" do
71
+ @object.untyped = 'foo'
72
+ @metadata.data[:untyped].should eql('foo')
73
+ end
74
+
75
+ it "should create the metadata object if it doesn't exist" do
76
+ @object.stub!(:metadata).and_return(nil)
77
+ ivars = @object.instance_variables - [ :@metadata ]
78
+ @object.stub!(:instance_variables).and_return(ivars)
79
+ Metadata.should_receive(:new).once.and_return(@metadata)
80
+
81
+ @object.untyped = 'foo'
82
+ @metadata.data[:untyped].should eql('foo')
83
+ end
84
+
85
+ it "should enforce a type if given" do
86
+ @object.multiparam = 'not correct'
87
+ @object.should_not be_valid
88
+ @object.errors[:multiparam].should_not be_empty
89
+ end
90
+
91
+ it "should not enforce a type if :allow_nil is given" do
92
+ @object.can_be_nil = nil
93
+ @object.valid? #@object.should be_valid
94
+ @object.errors[:can_be_nil].should be_empty
95
+ end
96
+
97
+ it "should not enforce a type if :allow_blank is given" do
98
+ @object.can_be_blank = ""
99
+ @object.valid? #@object.should be_valid
100
+ @object.errors[:can_be_blank].should be_empty
101
+ end
102
+
103
+ it "should enforce other validations as given" do
104
+ @object.number = 'not number'
105
+ @object.should_not be_valid
106
+ @object.errors[:number].should_not be_empty
107
+ end
108
+
109
+ it "should mass-assign a multiparameter attribute" do
110
+ @object.attributes = { 'multiparam(1)' => 'foo', 'multiparam(2)' => '1' }
111
+ @object.multiparam.args.should eql([ 'foo', '1' ])
112
+ end
113
+
114
+ it "should compact blank multiparameter parts" do
115
+ @object.attributes = { 'multiparam(1)' => '', 'multiparam(2)' => 'foo' }
116
+ @object.multiparam.args.should eql([ 'foo' ])
117
+ end
118
+
119
+ it "should typecast multiparameter parts" do
120
+ @object.attributes = { 'multiparam(1i)' => '1982', 'multiparam(2f)' => '10.5' }
121
+ @object.multiparam.args.should eql([ 1982, 10.5 ])
122
+ end
123
+ end
124
+ end
125
+ end
@@ -0,0 +1,13 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
2
+
3
+ describe Metadata do
4
+ describe ".new" do
5
+ it "should initialize data to an empty hash" do
6
+ Metadata.new.data.should eql({})
7
+ end
8
+
9
+ it "should initialize data to the value given in the initializer" do
10
+ Metadata.new(data: { foo: 'bar' }).data.should eql(foo: 'bar')
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,23 @@
1
+ Bundler.require :default, :test
2
+ require 'active_support'
3
+ require 'active_record'
4
+
5
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
6
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
7
+
8
+ require 'has_metadata'
9
+
10
+ ActiveRecord::Base.establish_connection(
11
+ adapter: 'sqlite3',
12
+ database: 'test.sqlite'
13
+ )
14
+ require "#{File.dirname __FILE__}/../templates/metadata"
15
+
16
+ RSpec.configure do |config|
17
+ config.before(:each) do
18
+ Metadata.connection.execute "DROP TABLE IF EXISTS metadata"
19
+ Metadata.connection.execute "CREATE TABLE metadata (id INTEGER PRIMARY KEY ASC, data TEXT)"
20
+ Metadata.connection.execute "DROP TABLE IF EXISTS users"
21
+ Metadata.connection.execute "CREATE TABLE users (id INTEGER PRIMARY KEY ASC)"
22
+ end
23
+ end
@@ -0,0 +1,11 @@
1
+ class CreateMetadatas < ActiveRecord::Migration
2
+ def self.up
3
+ create_table :metadata do |t|
4
+ t.text :data, null: false
5
+ end
6
+ end
7
+
8
+ def self.down
9
+ drop_table :metadatas
10
+ end
11
+ end
@@ -0,0 +1,42 @@
1
+ # Stores information about a model that doesn't need to be in that model's
2
+ # table. Each row in the @metadata@ table stores a schemaless, serialized hash
3
+ # of data associated with a model instance. Any model can have an associated row
4
+ # in the @metadata@ table by using the {HasMetadata} module.
5
+ #
6
+ # h2. Properties
7
+ #
8
+ # | @data@ | A hash of this metadata's contents (YAML serialized in the database). |
9
+
10
+ class Metadata < ActiveRecord::Base
11
+ set_table_name 'metadata'
12
+ serialize :data, Hash
13
+
14
+ after_initialize :initialize_data
15
+ before_save :nullify_empty_fields
16
+
17
+ validates :data,
18
+ presence: true
19
+
20
+ # @private
21
+ def set_fields(fields)
22
+ return self if @fields_set
23
+ @fields_set = true
24
+
25
+ fields.each do |name, _|
26
+ singleton_class.send(:define_method, name) { data[name] }
27
+ singleton_class.send(:define_method, :"#{name}=") { |value| data[name] = value }
28
+ end
29
+
30
+ self
31
+ end
32
+
33
+ private
34
+
35
+ def initialize_data
36
+ self.data ||= Hash.new
37
+ end
38
+
39
+ def nullify_empty_fields
40
+ data.each { |key, value| data[key] = nil if data[key].blank? }
41
+ end
42
+ end
metadata ADDED
@@ -0,0 +1,97 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: has_metadata
3
+ version: !ruby/object:Gem::Version
4
+ prerelease: false
5
+ segments:
6
+ - 1
7
+ - 0
8
+ - 0
9
+ version: 1.0.0
10
+ platform: ruby
11
+ authors:
12
+ - Tim Morgan
13
+ autorequire:
14
+ bindir: bin
15
+ cert_chain: []
16
+
17
+ date: 2010-10-30 00:00:00 -07:00
18
+ default_executable:
19
+ dependencies:
20
+ - !ruby/object:Gem::Dependency
21
+ name: rails
22
+ requirement: &id001 !ruby/object:Gem::Requirement
23
+ none: false
24
+ requirements:
25
+ - - ">="
26
+ - !ruby/object:Gem::Version
27
+ segments:
28
+ - 3
29
+ - 0
30
+ version: "3.0"
31
+ type: :runtime
32
+ prerelease: false
33
+ version_requirements: *id001
34
+ description: has_metadata lets you move non-indexed and weighty columns off of your big tables by creating a separate metadata table to store all this extra information. Works with Ruby 1.9. and Rails 3.0.
35
+ email: git@timothymorgan.info
36
+ executables: []
37
+
38
+ extensions: []
39
+
40
+ extra_rdoc_files:
41
+ - LICENSE
42
+ - README.textile
43
+ files:
44
+ - .document
45
+ - .gitignore
46
+ - .rspec
47
+ - Gemfile
48
+ - Gemfile.lock
49
+ - LICENSE
50
+ - README.textile
51
+ - Rakefile
52
+ - VERSION
53
+ - has_metadata.gemspec
54
+ - lib/has_metadata.rb
55
+ - lib/metadata_generator.rb
56
+ - spec/has_metadata_spec.rb
57
+ - spec/metadata_spec.rb
58
+ - spec/spec_helper.rb
59
+ - templates/create_metadata.rb
60
+ - templates/metadata.rb
61
+ has_rdoc: true
62
+ homepage: http://github.com/riscfuture/has_metadata
63
+ licenses: []
64
+
65
+ post_install_message:
66
+ rdoc_options:
67
+ - --charset=UTF-8
68
+ require_paths:
69
+ - lib
70
+ required_ruby_version: !ruby/object:Gem::Requirement
71
+ none: false
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ segments:
76
+ - 1
77
+ - 9
78
+ version: "1.9"
79
+ required_rubygems_version: !ruby/object:Gem::Requirement
80
+ none: false
81
+ requirements:
82
+ - - ">="
83
+ - !ruby/object:Gem::Version
84
+ segments:
85
+ - 0
86
+ version: "0"
87
+ requirements: []
88
+
89
+ rubyforge_project:
90
+ rubygems_version: 1.3.7
91
+ signing_key:
92
+ specification_version: 3
93
+ summary: Reduce your table width by moving non-indexed columns to a separate metadata table
94
+ test_files:
95
+ - spec/has_metadata_spec.rb
96
+ - spec/metadata_spec.rb
97
+ - spec/spec_helper.rb