├── VERSION ├── init.rb ├── .document ├── test ├── database.yml ├── schema.rb ├── helper.rb └── test_dynamic_attributes.rb ├── .gitignore ├── LICENSE ├── dynamic_attributes.gemspec ├── Rakefile ├── README.rdoc └── lib └── dynamic_attributes.rb /VERSION: -------------------------------------------------------------------------------- 1 | 1.2.0 2 | -------------------------------------------------------------------------------- /init.rb: -------------------------------------------------------------------------------- 1 | require 'dynamic_attributes' -------------------------------------------------------------------------------- /.document: -------------------------------------------------------------------------------- 1 | README.rdoc 2 | lib/**/*.rb 3 | bin/* 4 | features/**/*.feature 5 | LICENSE 6 | -------------------------------------------------------------------------------- /test/database.yml: -------------------------------------------------------------------------------- 1 | sqlite: 2 | adapter: sqlite 3 | database: ":memory:" 4 | sqlite3: 5 | adapter: sqlite3 6 | database: ":memory:" -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 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 | 21 | ## PROJECT::SPECIFIC 22 | -------------------------------------------------------------------------------- /test/schema.rb: -------------------------------------------------------------------------------- 1 | ActiveRecord::Schema.define(:version => 0) do 2 | create_table :dynamic_models, :force => true do |t| 3 | t.string :title 4 | t.text :dynamic_attributes 5 | t.text :extra 6 | end 7 | 8 | create_table :dynamic_nested_models, :force => true do |t| 9 | t.string :title 10 | t.text :dynamic_attributes 11 | t.integer :dynamic_model_id 12 | end 13 | end -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2010 Reinier de Lange 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. 21 | -------------------------------------------------------------------------------- /test/helper.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'test/unit' 3 | 4 | $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) 5 | $LOAD_PATH.unshift(File.dirname(__FILE__)) 6 | 7 | gem "activerecord" 8 | require 'fileutils' 9 | require 'active_record' 10 | require 'dynamic_attributes' 11 | require 'pp' 12 | 13 | class DynamicNestedModel < ActiveRecord::Base 14 | has_dynamic_attributes 15 | 16 | belongs_to :dynamic_model 17 | end 18 | 19 | class DynamicModel < ActiveRecord::Base 20 | has_dynamic_attributes :dynamic_attribute_field => :dynamic_attributes, :dynamic_attribute_prefix => 'field_', :destroy_dynamic_attribute_for_nil => false 21 | 22 | has_many :dynamic_nested_models 23 | 24 | accepts_nested_attributes_for :dynamic_nested_models 25 | end 26 | 27 | def load_schema 28 | config = YAML::load(IO.read(File.dirname(__FILE__) + '/database.yml')) 29 | ActiveRecord::Base.logger = ActiveSupport::BufferedLogger.new(File.dirname(__FILE__) + "/debug.log") 30 | db_adapter = ENV['DB'] 31 | # no db passed, try one of these fine config-free DBs before bombing. 32 | db_adapter ||= begin 33 | require 'rubygems' 34 | require 'sqlite' 35 | 'sqlite' 36 | rescue MissingSourceFile 37 | begin 38 | require 'sqlite3' 39 | 'sqlite3' 40 | rescue MissingSourceFile 41 | end 42 | end 43 | 44 | if db_adapter.nil? 45 | raise "No DB Adapter selected. Pass the DB= option to pick one, or install Sqlite or Sqlite3." 46 | end 47 | ActiveRecord::Base.establish_connection(config[db_adapter]) 48 | load(File.dirname(__FILE__) + "/schema.rb") 49 | require File.dirname(__FILE__) + '/../init.rb' 50 | end 51 | -------------------------------------------------------------------------------- /dynamic_attributes.gemspec: -------------------------------------------------------------------------------- 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{dynamic_attributes} 8 | s.version = "1.2.0" 9 | 10 | s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version= 11 | s.authors = ["Reinier de Lange"] 12 | s.date = %q{2010-11-21} 13 | s.description = %q{dynamic_attributes is a gem that lets you dynamically specify attributes on ActiveRecord models, which will be serialized and 14 | deserialized to a given text column. Dynamic attributes can be defined by simply setting an attribute or by passing them on create or update.} 15 | s.email = %q{r.j.delange@nedforce.nl} 16 | s.extra_rdoc_files = [ 17 | "LICENSE", 18 | "README.rdoc" 19 | ] 20 | s.files = [ 21 | ".document", 22 | ".gitignore", 23 | "LICENSE", 24 | "README.rdoc", 25 | "Rakefile", 26 | "VERSION", 27 | "init.rb", 28 | "lib/dynamic_attributes.rb" 29 | ] 30 | s.homepage = %q{http://github.com/moiristo/dynamic_attributes} 31 | s.rdoc_options = ["--charset=UTF-8"] 32 | s.require_paths = ["lib"] 33 | s.rubygems_version = %q{1.3.7} 34 | s.summary = %q{dynamic_attributes is a gem that lets you dynamically specify attributes on ActiveRecord models, which will be serialized and deserialized to a given text column.} 35 | s.test_files = [ 36 | "test/helper.rb", 37 | "test/test_dynamic_attributes.rb", 38 | "test/database.yml", 39 | "test/schema.rb" 40 | ] 41 | 42 | if s.respond_to? :specification_version then 43 | current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION 44 | s.specification_version = 3 45 | 46 | if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then 47 | else 48 | end 49 | else 50 | end 51 | end 52 | 53 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'rake' 3 | 4 | begin 5 | require 'jeweler' 6 | Jeweler::Tasks.new do |gem| 7 | gem.name = "dynamic_attributes" 8 | gem.summary = %Q{dynamic_attributes is a gem that lets you dynamically specify attributes on ActiveRecord models, which will be serialized and 9 | deserialized to a given text column.} 10 | gem.description = %Q{dynamic_attributes is a gem that lets you dynamically specify attributes on ActiveRecord models, which will be serialized and 11 | deserialized to a given text column. Dynamic attributes can be defined by simply setting an attribute or by passing them on create or update.} 12 | gem.email = "r.j.delange@nedforce.nl" 13 | gem.homepage = "http://github.com/moiristo/dynamic_attributes" 14 | gem.authors = ["Reinier de Lange"] 15 | gem.files = [ 16 | "init.rb", 17 | ".document", 18 | ".gitignore", 19 | "LICENSE", 20 | "README.rdoc", 21 | "Rakefile", 22 | "VERSION", 23 | "lib/dynamic_attributes.rb" 24 | ] 25 | gem.test_files = [ 26 | "test/helper.rb", 27 | "test/test_dynamic_attributes.rb", 28 | "test/database.yml", 29 | "test/schema.rb" 30 | ] 31 | # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings 32 | end 33 | Jeweler::GemcutterTasks.new 34 | rescue LoadError 35 | puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler" 36 | end 37 | 38 | require 'rake/testtask' 39 | Rake::TestTask.new(:test) do |test| 40 | test.libs << 'lib' << 'test' 41 | test.pattern = 'test/**/test_*.rb' 42 | test.verbose = true 43 | end 44 | 45 | begin 46 | require 'rcov/rcovtask' 47 | Rcov::RcovTask.new do |test| 48 | test.libs << 'test' 49 | test.pattern = 'test/**/test_*.rb' 50 | test.verbose = true 51 | end 52 | rescue LoadError 53 | task :rcov do 54 | abort "RCov is not available. In order to run rcov, you must: sudo gem install spicycode-rcov" 55 | end 56 | end 57 | 58 | task :test => :check_dependencies 59 | 60 | task :default => :test 61 | 62 | require 'rake/rdoctask' 63 | Rake::RDocTask.new do |rdoc| 64 | version = File.exist?('VERSION') ? File.read('VERSION') : "" 65 | 66 | rdoc.rdoc_dir = 'rdoc' 67 | rdoc.title = "dynamic_attributes #{version}" 68 | rdoc.rdoc_files.include('README*') 69 | rdoc.rdoc_files.include('lib/**/*.rb') 70 | end 71 | -------------------------------------------------------------------------------- /test/test_dynamic_attributes.rb: -------------------------------------------------------------------------------- 1 | require 'helper' 2 | 3 | class TestDynamicAttributes < Test::Unit::TestCase 4 | load_schema 5 | 6 | def setup 7 | DynamicModel.dynamic_attribute_field = :dynamic_attributes 8 | DynamicModel.dynamic_attribute_prefix = 'field_' 9 | DynamicModel.destroy_dynamic_attribute_for_nil = false 10 | 11 | @dynamic_model = DynamicModel.create(:title => "A dynamic model") 12 | end 13 | 14 | def test_should_persist_nothing_by_default 15 | assert_equal [], @dynamic_model.persisting_dynamic_attributes 16 | end 17 | 18 | def test_should_create_manual_dynamic_attribute 19 | @dynamic_model.field_test = 'hello' 20 | assert_equal 'hello', @dynamic_model.field_test 21 | assert @dynamic_model.persisting_dynamic_attributes.include?('field_test') 22 | assert @dynamic_model.dynamic_attributes.keys.include?('field_test') 23 | assert @dynamic_model.has_dynamic_attribute?(:field_test) 24 | end 25 | 26 | def test_should_create_dynamic_attributes_for_hash 27 | assert dynamic_model = DynamicModel.create(:title => 'Title', :field_test1 => 'Hello', :field_test2 => 'World') 28 | assert dynamic_model.persisting_dynamic_attributes.include?('field_test1') 29 | assert dynamic_model.persisting_dynamic_attributes.include?('field_test2') 30 | assert_equal 'Hello', dynamic_model.field_test1 31 | assert_equal 'World', dynamic_model.field_test2 32 | assert dynamic_model.has_dynamic_attribute?(:field_test1) 33 | assert dynamic_model.has_dynamic_attribute?(:field_test2) 34 | end 35 | 36 | def test_should_update_attributes 37 | @dynamic_model.update_attributes(:title => 'Title', :field_test1 => 'Hello', :field_test2 => 'World') 38 | assert @dynamic_model.persisting_dynamic_attributes.include?('field_test1') 39 | assert @dynamic_model.persisting_dynamic_attributes.include?('field_test2') 40 | assert_equal 'Hello', @dynamic_model.field_test1 41 | assert_equal 'World', @dynamic_model.field_test2 42 | 43 | @dynamic_model.reload 44 | assert_equal 'Hello', @dynamic_model.field_test1 45 | assert_equal 'World', @dynamic_model.field_test2 46 | 47 | assert @dynamic_model.has_dynamic_attribute?(:field_test1) 48 | assert @dynamic_model.has_dynamic_attribute?(:field_test2) 49 | end 50 | 51 | def test_should_load_dynamic_attributes_after_find 52 | DynamicModel.update_all("dynamic_attributes = '---\nfield_test: Hi!\n'", :id => @dynamic_model.id) 53 | dynamic_model = DynamicModel.find(@dynamic_model.id) 54 | assert_equal 'Hi!', dynamic_model.field_test 55 | 56 | assert dynamic_model.has_dynamic_attribute?(:field_test) 57 | end 58 | 59 | def test_should_set_dynamic_attribute_to_nil_if_configured 60 | assert @dynamic_model.update_attribute(:field_test,nil) 61 | assert_nil @dynamic_model.field_test 62 | assert @dynamic_model.persisting_dynamic_attributes.include?('field_test') 63 | assert @dynamic_model.dynamic_attributes.include?('field_test') 64 | assert @dynamic_model.has_dynamic_attribute?(:field_test) 65 | 66 | DynamicModel.destroy_dynamic_attribute_for_nil = true 67 | assert @dynamic_model.update_attribute(:field_test,nil) 68 | assert !@dynamic_model.persisting_dynamic_attributes.include?('field_test') 69 | assert !@dynamic_model.dynamic_attributes.include?('field_test') 70 | assert !@dynamic_model.respond_to?('field_test=') 71 | assert !@dynamic_model.has_dynamic_attribute?(:field_test) 72 | end 73 | 74 | def test_should_allow_different_prefix 75 | DynamicModel.dynamic_attribute_prefix = 'what_' 76 | 77 | @dynamic_model.what_test = 'hello' 78 | assert_equal 'hello', @dynamic_model.what_test 79 | assert @dynamic_model.persisting_dynamic_attributes.include?('what_test') 80 | assert @dynamic_model.dynamic_attributes.keys.include?('what_test') 81 | assert @dynamic_model.has_dynamic_attribute?(:what_test) 82 | assert !@dynamic_model.has_dynamic_attribute?(:field_test) 83 | 84 | assert_raises NoMethodError do 85 | @dynamic_model.field_test = 'Fail' 86 | end 87 | end 88 | 89 | def test_should_allow_different_serialization_field 90 | DynamicModel.dynamic_attribute_field = 'extra' 91 | @dynamic_model.update_attributes(:title => 'Title', :field_test1 => 'Hello', :field_test2 => 'World') 92 | assert_equal({}, @dynamic_model.dynamic_attributes || {}) 93 | assert_equal({"field_test1"=>"Hello", "field_test2"=>"World"}, @dynamic_model.extra) 94 | end 95 | 96 | def test_should_set_nested_dynamic_attributes 97 | @dynamic_model.update_attributes(:dynamic_nested_models_attributes => { '0'=> { :title => 'A nested dynamic model', :field_test => 'Hello', :field_test2 => 'World' } }) 98 | assert DynamicNestedModel.any? 99 | 100 | nested_model = @dynamic_model.dynamic_nested_models.first 101 | nested_model.has_dynamic_attribute?(:field_test) 102 | nested_model.has_dynamic_attribute?(:field_test2) 103 | assert_equal 'A nested dynamic model', nested_model.title 104 | assert_equal 'Hello', nested_model.field_test 105 | assert_equal 'World', nested_model.field_test2 106 | end 107 | 108 | end 109 | -------------------------------------------------------------------------------- /README.rdoc: -------------------------------------------------------------------------------- 1 | = dynamic_attributes 2 | 3 | dynamic_attributes is a gem that lets you dynamically specify attributes on ActiveRecord models, which will be serialized and deserialized to a given text column. Example: 4 | 5 | >> dm = DynamicModel.new(:field_test => 'I am a dynamic attribute') 6 | +-------+-------------+--------------------------------------------+ 7 | | title | description | dynamic_attributes | 8 | +-------+-------------+--------------------------------------------+ 9 | | | | {"field_test"=>"I am a dynamic_attribute"} | 10 | +-------+-------------+--------------------------------------------+ 11 | >> dm.field_test 12 | => "I am a dynamic_attribute" 13 | >> dm.field_test2 14 | NoMethodError: undefined method `field_test2' 15 | >> dm.field_test2 = 'I am too!' 16 | => 'I am too!' 17 | >> dm.field_test2 18 | => 'I am too!' 19 | >> dm.save 20 | +-------+-------------+------------------------------------------------------------------------+ 21 | | title | description | dynamic_attributes | 22 | +-------+-------------+------------------------------------------------------------------------+ 23 | | | | {"field_test2"=>"I am too!", "field_test"=>"I am a dynamic_attribute"} | 24 | +-------+-------------+------------------------------------------------------------------------+ 25 | 26 | 27 | 28 | 29 | == Requirements 30 | 31 | * Rails 2.x / 3 (Tested for 2.3.5, 2.3.8 & 3). 32 | 33 | == Installation 34 | 35 | * config.gem 'dynamic_attributes', sudo rake gems:install 36 | * gem install dynamic_attributes 37 | 38 | == Usage 39 | 40 | To add dynamic_attributes to an AR model, take the following steps: 41 | 42 | * Create a migration to add a column to serialize the dynamic attributes to: 43 | 44 | class AddDynamicAttributesToDynamicModels < ActiveRecord::Migration 45 | def self.up 46 | add_column :dynamic_models, :dynamic_attributes, :text 47 | end 48 | 49 | def self.down 50 | remove_column :dynamic_models, :dynamic_attributes 51 | end 52 | end 53 | 54 | * Add dynamic_attributes to your AR model: 55 | 56 | class DynamicModel < ActiveRecord::Base 57 | has_dynamic_attributes 58 | end 59 | 60 | * Now you can add dynamic attributes in several ways. Examples: 61 | 62 | - New: DynamicModel.new(:title => 'Hello', :field_summary => 'This is a dynamic attribute') 63 | - Create: DynamicModel.create(:title => 'Hello', :field_summary => 'This is a dynamic attribute') 64 | - Update: 65 | * dynamic_model.update_attribute(:field_summary, 'This is a dynamic attribute') 66 | * dynamic_model.update_attributes(:field_summary => 'This is a dynamic attribute', :description => 'Testing') 67 | - Set manually: dynamic_model.field_summary = 'This is a dynamic attribute' 68 | 69 | Note that a dynamic attribute should be prefixed (by default with 'field_'), see the Options section for more info. 70 | 71 | * Get Info: 72 | * dynamic_model.has_dynamic_attribute?(dynamic_attribute) 73 | * Returns whether a given attribute is a dynamic attribute. Accepts strings and symbols. 74 | * dynamic_model.read_dynamic_attribute(dynamic_attribute) 75 | * Returns the value for a given dynamic attribute. Returns nil if the attribute does not exist or if the attribute is not a dynamic attribute. 76 | * dynamic_model.persisting_dynamic_attributes 77 | * Returns an array of the dynamic attributes that will be persisted. 78 | * DynamicModel.dynamic_attribute_field 79 | * Returns the serialization attribute. You can access this serialization attribute directly if you need to. 80 | * DynamicModel.dynamic_attribute_prefix 81 | * Returns the method prefix of dynamic attributes, see below for more info. 82 | * DynamicModel.destroy_dynamic_attribute_for_nil 83 | * Returns whether dynamic attributes with null values will be persisted, see below for more info. 84 | 85 | == Options 86 | 87 | The has_dynamic_attribute call takes three different options: 88 | 89 | * :dynamic_attribute_field 90 | - Defines the database column to serialize to. 91 | * :dynamic_attribute_prefix 92 | - Defines the prefix that a dynamic attribute should have. All attribute assignments that start with this prefix will become dynamic attributes. Note that it's not recommended to set this prefix to the empty string; as every method call that falls through to method_missing will become a dynamic attribute. 93 | * :destroy_dynamic_attribute_for_nil 94 | - When set to true, the module will remove a dynamic attribute when its value is set to nil. Defaults to false, causing the module to store a dynamic attribute even if its value is nil. 95 | 96 | By default, the has_dynamic_attributes call without options equals to calling: 97 | 98 | has_dynamic_attributes :dynamic_attribute_field => :dynamic_attributes, :dynamic_attribute_prefix => 'field_', :destroy_dynamic_attribute_for_nil => false 99 | 100 | Take a look at the code Rdoc for more information! 101 | 102 | == Validations 103 | 104 | The validations provided by AR can be used for dynamic attributes as if it is a normal attribute. 105 | 106 | == Contributors 107 | 108 | * Adam H. (terralab) 109 | 110 | == Note on Patches/Pull Requests 111 | 112 | * Fork the project. 113 | * Make your feature addition or bug fix. 114 | * Add tests for it. This is important so I don't break it in a 115 | future version unintentionally. 116 | * Commit, do not mess with rakefile, version, or history. 117 | (if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull) 118 | * Send me a pull request. Bonus points for topic branches. 119 | 120 | == Copyright 121 | 122 | Copyright (c) 2010 Reinier de Lange. See LICENSE for details. 123 | -------------------------------------------------------------------------------- /lib/dynamic_attributes.rb: -------------------------------------------------------------------------------- 1 | 2 | # Adds the has_dynamic_attributes method in ActiveRecord::Base, which can be used to configure the module. 3 | class << ActiveRecord::Base 4 | 5 | # Method to call in AR classes in order to be able to define dynamic attributes. The following options can be defined: 6 | # 7 | # * :dynamic_attribute_field - Defines the attribute to which all dynamic attributes will be serialized. Default: :dynamic_attributes. 8 | # * :dynamic_attribute_prefix - Defines the prefix that a dynamic attribute should have. All assignments that start with this prefix will become 9 | # dynamic attributes. Note that it's not recommended to set this prefix to the empty string; as every method call that falls through to method_missing 10 | # will become a dynamic attribute. Default: 'field_' 11 | # * :destroy_dynamic_attribute_for_nil - When set to true, the module will remove a dynamic attribute when its value is set to nil. Defaults to false, causing 12 | # the module to store a dynamic attribute even if its value is nil. 13 | # 14 | def has_dynamic_attributes(options = { :dynamic_attribute_field => :dynamic_attributes, :dynamic_attribute_prefix => 'field_', :destroy_dynamic_attribute_for_nil => false}) 15 | cattr_accessor :dynamic_attribute_field 16 | self.dynamic_attribute_field = options[:dynamic_attribute_field] || :dynamic_attributes 17 | cattr_accessor :dynamic_attribute_prefix 18 | self.dynamic_attribute_prefix = options[:dynamic_attribute_prefix] || 'field_' 19 | cattr_accessor :destroy_dynamic_attribute_for_nil 20 | self.destroy_dynamic_attribute_for_nil = options[:destroy_dynamic_attribute_for_nil] || false 21 | 22 | include DynamicAttributes 23 | end 24 | end 25 | 26 | # The DynamicAttributes module handles all dynamic attributes. 27 | module DynamicAttributes 28 | 29 | # Overrides the initializer to take dynamic attributes into account 30 | def initialize(attributes = nil) 31 | dynamic_attributes = {} 32 | (attributes ||= {}).each{|att,value| dynamic_attributes[att] = value if att.to_s.starts_with?(self.dynamic_attribute_prefix) } 33 | super(attributes.except(*dynamic_attributes.keys)) 34 | set_dynamic_attributes(dynamic_attributes) 35 | end 36 | 37 | # Returns whether a given attribute is a dynamic attribute 38 | def has_dynamic_attribute?(dynamic_attribute) 39 | return persisting_dynamic_attributes.include?(dynamic_attribute.to_s) 40 | end 41 | 42 | # Reads the value of the given dynamic attribute. 43 | # Returns nil if the attribute does not exist or if it is not a dynamic attribute. 44 | def read_dynamic_attribute(dynamic_attribute) 45 | has_dynamic_attribute?(dynamic_attribute) ? (send(dynamic_attribute) rescue nil) : nil 46 | end 47 | 48 | # On saving an AR record, the attributes to be persisted are re-evaluated and written to the serialization field. 49 | def evaluate_dynamic_attributes 50 | new_dynamic_attributes = {} 51 | self.persisting_dynamic_attributes.uniq.each do |dynamic_attribute| 52 | value = read_dynamic_attribute(dynamic_attribute) 53 | if value.nil? and destroy_dynamic_attribute_for_nil 54 | self.persisting_dynamic_attributes.delete(dynamic_attribute) 55 | singleton_class.send(:remove_method, dynamic_attribute + '=') 56 | else 57 | new_dynamic_attributes[dynamic_attribute] = value 58 | end 59 | end 60 | write_attribute(self.dynamic_attribute_field, new_dynamic_attributes) 61 | end 62 | 63 | # After find, populate the dynamic attributes and create accessors 64 | def populate_dynamic_attributes 65 | (read_attribute(self.dynamic_attribute_field) || {}).each {|att, value| set_dynamic_attribute(att, value); self.destroy_dynamic_attribute_for_nil = false if value.nil? } 66 | end 67 | 68 | # Explicitly define after_find for Rails 2.x 69 | def after_find; populate_dynamic_attributes end 70 | 71 | # Overrides update_attributes to take dynamic attributes into account 72 | def update_attributes(attributes) 73 | set_dynamic_attributes(attributes) 74 | super(attributes) 75 | end 76 | 77 | # Creates an accessor when a non-existing setter with the configured dynamic attribute prefix is detected. Calls super otherwise. 78 | def method_missing(method, *arguments, &block) 79 | (method.to_s =~ /#{self.dynamic_attribute_prefix}(.+)=/) ? set_dynamic_attribute(self.dynamic_attribute_prefix + $1, *arguments.first) : super 80 | end 81 | 82 | # Returns the dynamic attributes that will be persisted to the serialization column. This array can 83 | # be altered to force dynamic attributes to not be saved in the database or to persist other attributes, but 84 | # it is recommended to not change it at all. 85 | def persisting_dynamic_attributes 86 | @persisting_dynamic_attributes ||= [] 87 | end 88 | 89 | # Ensures the configured dynamic attribute field is serialized by AR. 90 | def self.included object 91 | super 92 | object.after_find :populate_dynamic_attributes 93 | object.before_save :evaluate_dynamic_attributes 94 | object.serialize object.dynamic_attribute_field 95 | end 96 | 97 | # Gets the object's singleton class. Backported from Rails 2.3.8 to support older versions of Rails. 98 | def singleton_class 99 | class << self 100 | self 101 | end 102 | end 103 | 104 | # Defines an accessor for the given attribute. It is defined withinin the public scope to ensure the 105 | # accessor is publicly available in ruby 1.9. 106 | def define_accessor_for_attribute(att) 107 | singleton_class.send(:attr_accessor, att) 108 | end 109 | 110 | private 111 | 112 | # Method that is called when a dynamic attribute is added to this model. It adds this attribute to the list 113 | # of attributes that will be persisited, creates an accessor and sets the attribute value. To reflect that the 114 | # attribute has been added, the serialization attribute will also be updated. 115 | def set_dynamic_attribute(att, value = nil) 116 | att = att.to_s 117 | persisting_dynamic_attributes << att 118 | define_accessor_for_attribute(att) 119 | send(att + '=', value) 120 | update_dynamic_attribute(att, value) 121 | end 122 | 123 | # Called on object initialization or when calling update_attributes to convert passed dynamic attributes 124 | # into attributes that will be persisted by calling set_dynamic_attribute if it does not exist already. 125 | # The serialization column will also be updated and the detected dynamic attributes are removed from the passed 126 | # attributes hash. 127 | def set_dynamic_attributes(attributes) 128 | return if attributes.nil? 129 | 130 | attributes.each do |att, value| 131 | if att.to_s.starts_with?(self.dynamic_attribute_prefix) 132 | attributes.delete(att) 133 | unless respond_to?(att.to_s + '=') 134 | set_dynamic_attribute(att, value) 135 | else 136 | send(att.to_s + '=', value); 137 | update_dynamic_attribute(att, value) 138 | end 139 | end 140 | end 141 | end 142 | 143 | # Updates the serialization column with a new attribute and value. 144 | def update_dynamic_attribute(attribute, value) 145 | write_attribute(self.dynamic_attribute_field.to_s, (read_attribute(self.dynamic_attribute_field.to_s) || {}).merge(attribute.to_s => value)) 146 | end 147 | 148 | end 149 | --------------------------------------------------------------------------------