Skip to content

Commit

Permalink
First release, created and tested.
Browse files Browse the repository at this point in the history
  • Loading branch information
Reinier de Lange committed Jul 28, 2010
1 parent 1e45a59 commit 20bb562
Show file tree
Hide file tree
Showing 10 changed files with 376 additions and 9 deletions.
2 changes: 1 addition & 1 deletion LICENSE
@@ -1,4 +1,4 @@
Copyright (c) 2009 Reinier de Lange
Copyright (c) 2010 Reinier de Lange

Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
Expand Down
63 changes: 62 additions & 1 deletion README.rdoc
@@ -1,6 +1,67 @@
= dynamic_attributes

Description goes here.
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.

== Requirements ==

* Rails 2.x

== Installation ==

* config.gem 'dynamic_attributes', sudo rake gems:install
* gem install dynamic_attributes

== Usage ==

To add dynamic_attributes to an AR model, take the following steps:

* Create a migration to add a column to serialize the dynamic attributes to:

class AddDynamicAttributesToDynamicModels < ActiveRecord::Migration
def self.up
add_column :dynamic_models, :dynamic_attributes, :text
end

def self.down
remove_column :dynamic_models, :dynamic_attributes
end
end

* Add dynamic_attributes to your AR model:

class DynamicModel < ActiveRecord::Base
has_dynamic_attributes
end

* Now you can add dynamic attributes in several ways. Examples:

- New: DynamicModel.new(:title => 'Hello', :field_summary => 'This is a dynamic attribute')
- Create: DynamicModel.create(:title => 'Hello', :field_summary => 'This is a dynamic attribute')
- Update:
* dynamic_model.update_atribute(:field_summary, 'This is a dynamic attribute')
* dynamic_model.update_atributes(:field_summary => 'This is a dynamic attribute', :description => 'Testing')
- Set manually: dynamic_model.field_summary = 'This is a dynamic attribute'

== Options ==

The has_dynamic_attribute call takes three different options:

* :dynamic_attribute_field
- Defines the database column to serialize to.
* :dynamic_attribute_prefix
- 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.
* :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 the module to store a dynamic attribute even if its value is nil.

By default, the has_dynamic_attributes call without options equals to calling:

has_dynamic_attributes
:dynamic_attribute_field => :dynamic_attributes,
:dynamic_attribute_prefix => 'field_',
:destroy_dynamic_attribute_for_nil => false

Take a look at the code Rdoc for more information!

== Note on Patches/Pull Requests

Expand Down
23 changes: 20 additions & 3 deletions Rakefile
Expand Up @@ -5,12 +5,29 @@ begin
require 'jeweler'
Jeweler::Tasks.new do |gem|
gem.name = "dynamic_attributes"
gem.summary = %Q{TODO: one-line summary of your gem}
gem.description = %Q{TODO: longer description of your gem}
gem.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.}
gem.description = %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. Dynamic attributes can be defined by simply setting an attribute or by passing them on create or update.}
gem.email = "r.j.delange@nedforce.nl"
gem.homepage = "http://github.com/moiristo/dynamic_attributes"
gem.authors = ["Reinier de Lange"]
gem.add_development_dependency "thoughtbot-shoulda", ">= 0"
gem.files = [
"init.rb",
".document",
".gitignore",
"LICENSE",
"README.rdoc",
"Rakefile",
"VERSION",
"lib/dynamic_attributes.rb"
]
gem.test_files = [
"test/helper.rb",
"test/test_dynamic_attributes.rb",
"test/database.yml",
"test/schema.rb"
]
# gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
end
Jeweler::GemcutterTasks.new
Expand Down
52 changes: 52 additions & 0 deletions dynamic_attributes.gemspec
@@ -0,0 +1,52 @@
# Generated by jeweler
# DO NOT EDIT THIS FILE DIRECTLY
# Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command
# -*- encoding: utf-8 -*-

Gem::Specification.new do |s|
s.name = %q{dynamic_attributes}
s.version = "0.0.0.pre1"

s.required_rubygems_version = Gem::Requirement.new("> 1.3.1") if s.respond_to? :required_rubygems_version=
s.authors = ["Reinier de Lange"]
s.date = %q{2010-07-29}
s.description = %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. Dynamic attributes can be defined by simply setting an attribute or by passing them on create or update.}
s.email = %q{r.j.delange@nedforce.nl}
s.extra_rdoc_files = [
"LICENSE",
"README.rdoc"
]
s.files = [
".document",
".gitignore",
"LICENSE",
"README.rdoc",
"Rakefile",
"VERSION",
"init.rb",
"lib/dynamic_attributes.rb"
]
s.homepage = %q{http://github.com/moiristo/dynamic_attributes}
s.rdoc_options = ["--charset=UTF-8"]
s.require_paths = ["lib"]
s.rubygems_version = %q{1.3.6}
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.}
s.test_files = [
"test/helper.rb",
"test/test_dynamic_attributes.rb",
"test/database.yml",
"test/schema.rb"
]

if s.respond_to? :specification_version then
current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
s.specification_version = 3

if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
else
end
else
end
end

1 change: 1 addition & 0 deletions init.rb
@@ -0,0 +1 @@
require 'dynamic_attributes'
118 changes: 118 additions & 0 deletions lib/dynamic_attributes.rb
@@ -0,0 +1,118 @@

# Adds the has_dynamic_attributes method in ActiveRecord::Base, which can be used to configure the module.
class << ActiveRecord::Base

# Method to call in AR classes in order to be able to define dynamic attributes. The following options can be defined:
#
# * :dynamic_attribute_field - Defines the attribute to which all dynamic attributes will be serialized. Default: :dynamic_attributes.
# * :dynamic_attribute_prefix - Defines the prefix that a dynamic attribute should have. All 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. Default: 'field_'
# * :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
# the module to store a dynamic attribute even if its value is nil.
#
def has_dynamic_attributes(options = { :dynamic_attribute_field => :dynamic_attributes, :dynamic_attribute_prefix => 'field_', :destroy_dynamic_attribute_for_nil => false})
cattr_accessor :dynamic_attribute_field
self.dynamic_attribute_field = options[:dynamic_attribute_field] || :dynamic_attributes
cattr_accessor :dynamic_attribute_prefix
self.dynamic_attribute_prefix = options[:dynamic_attribute_prefix] || 'field_'
cattr_accessor :destroy_dynamic_attribute_for_nil
self.destroy_dynamic_attribute_for_nil = options[:destroy_dynamic_attribute_for_nil] || false

include DynamicAttributes
end
end

# The DynamicAttributes module handles all dynamic attributes.
module DynamicAttributes

# On saving an AR record, the attributes to be persisted are re-evaluated and written to the serialization field.
def before_save
new_dynamic_attributes = {}
self.persisting_dynamic_attributes.uniq.each do |dynamic_attribute|
value = send(dynamic_attribute)
if value.nil? and destroy_dynamic_attribute_for_nil
self.persisting_dynamic_attributes.delete(dynamic_attribute)
singleton_class.send(:remove_method, dynamic_attribute + '=')
else
new_dynamic_attributes[dynamic_attribute] = value
end
end
write_attribute(self.dynamic_attribute_field, new_dynamic_attributes)
end

# After find, populate the dynamic attributes and create accessors
def after_find
(read_attribute(self.dynamic_attribute_field) || {}).each {|att, value| set_dynamic_attribute(att, value); self.destroy_dynamic_attribute_for_nil = false if value.nil? }
end

# Creates an accessor when a non-existing setter with the configured dynamic attribute prefix is detected. Calls super otherwise.
def method_missing(method, *arguments, &block)
(method.to_s =~ /#{self.dynamic_attribute_prefix}(.+)=/) ? set_dynamic_attribute(self.dynamic_attribute_prefix + $1, *arguments.first) : super
end

# Overrides the initializer to take dynamic attributes into account
def initialize(attributes = nil)
dynamic_attributes = {}
attributes.each{|att,value| dynamic_attributes[att] = value if att.to_s.starts_with?(self.dynamic_attribute_prefix) }
super(attributes.except(*dynamic_attributes.keys))
set_dynamic_attributes(dynamic_attributes)
end

# Overrides update_attributes to take dynamic attributes into account
def update_attributes(attributes)
set_dynamic_attributes(attributes)
super(attributes)
end

# Returns the dynamic attributes that will be persisted to the serialization column. This array can
# be altered to force dynamic attributes to not be saved in the database or to persist other attributes, but
# it is recommended to not change it at all.
def persisting_dynamic_attributes
@persisting_dynamic_attributes ||= []
end

# Ensures the configured dynamic attribute field is serialized by AR.
def self.included object
object.serialize object.dynamic_attribute_field
end

private

# Method that is called when a dynamic attribute is added to this model. It adds this attribute to the list
# of attributes that will be persisited, creates an accessor and sets the attribute value. To reflect that the
# attribute has been added, the serialization attribute will also be updated.
def set_dynamic_attribute(att, value)
att = att.to_s
persisting_dynamic_attributes << att
singleton_class.send(:attr_accessor, att)
send(att + '=', value)
update_dynamic_attribute(att, value)
end

# Called on object initialization or when calling update_attributes to convert passed dynamic attributes
# into attributes that will be persisted by calling set_dynamic_attribute if it does not exist already.
# The serialization column will also be updated and the detected dynamic attributes are removed from the passed
# attributes hash.
def set_dynamic_attributes(attributes)
return if attributes.nil?

attributes.each do |att, value|
if att.to_s.starts_with?(self.dynamic_attribute_prefix)
attributes.delete(att)
unless respond_to?(att.to_s + '=')
set_dynamic_attribute(att, value)
else
send(att.to_s + '=', value);
update_dynamic_attribute(att, value)
end
end
end
end

# Updates the serialization column with a new attribute and value.
def update_dynamic_attribute(attribute, value)
write_attribute(self.dynamic_attribute_field.to_s, (read_attribute(self.dynamic_attribute_field.to_s) || {}).merge(attribute.to_s => value))
end

end
6 changes: 6 additions & 0 deletions test/database.yml
@@ -0,0 +1,6 @@
sqlite:
adapter: sqlite
database: ":memory:"
sqlite3:
adapter: sqlite3
database: ":memory:"
33 changes: 31 additions & 2 deletions test/helper.rb
@@ -1,10 +1,39 @@
require 'rubygems'
require 'test/unit'
require 'shoulda'

$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
$LOAD_PATH.unshift(File.dirname(__FILE__))

gem "activerecord"
require 'active_record'
require 'dynamic_attributes'
require 'pp'

class Test::Unit::TestCase
class DynamicModel < ActiveRecord::Base
has_dynamic_attributes :dynamic_attribute_field => :dynamic_attributes, :dynamic_attribute_prefix => 'field_', :destroy_dynamic_attribute_for_nil => false
end

def load_schema
config = YAML::load(IO.read(File.dirname(__FILE__) + '/database.yml'))
ActiveRecord::Base.logger = Logger.new(File.dirname(__FILE__) + "/debug.log")
db_adapter = ENV['DB']
# no db passed, try one of these fine config-free DBs before bombing.
db_adapter ||= begin
require 'rubygems'
require 'sqlite'
'sqlite'
rescue MissingSourceFile
begin
require 'sqlite3'
'sqlite3'
rescue MissingSourceFile
end
end

if db_adapter.nil?
raise "No DB Adapter selected. Pass the DB= option to pick one, or install Sqlite or Sqlite3."
end
ActiveRecord::Base.establish_connection(config[db_adapter])
load(File.dirname(__FILE__) + "/schema.rb")
require File.dirname(__FILE__) + '/../init.rb'
end
7 changes: 7 additions & 0 deletions test/schema.rb
@@ -0,0 +1,7 @@
ActiveRecord::Schema.define(:version => 0) do
create_table :dynamic_models, :force => true do |t|
t.string :title
t.text :dynamic_attributes
t.text :extra
end
end

0 comments on commit 20bb562

Please sign in to comment.