has_metadata 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.
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