property 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,2 @@
1
+ .DS_Store
2
+ coverage
data/History.txt ADDED
@@ -0,0 +1,10 @@
1
+ == 0.4.0 2010-02-02
2
+
3
+ * 1 major enhancement
4
+ * initial plugin code
5
+
6
+ == 0.5.0 2010-02-11
7
+
8
+ * 2 major enhancement
9
+ * changed plugin into gem
10
+ * using Rails columns to handle defaults and type casting
data/MIT-LICENSE ADDED
@@ -0,0 +1,19 @@
1
+ Copyright (c) 2010 Gaspard Bucher (http://teti.ch)
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in
11
+ all copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ THE SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,48 @@
1
+ == DESCRIPTION:
2
+
3
+ Wrap model properties into a single database column and declare properties from within the model.
4
+
5
+ website: http://zenadmin.org/635
6
+ license: MIT
7
+
8
+ == Status: Beta
9
+
10
+ The gem works fine, even though it still needs some more features like property definition
11
+ changes detections and migrations.
12
+
13
+ == Usage
14
+
15
+ You first need to create a migration to add a 'text' field named 'properties' to
16
+ your model. Something like this:
17
+
18
+ class AddPropertyToContact < ActiveRecord::Migration
19
+ def self.up
20
+ add_column :contacts, :properties, :text
21
+ end
22
+
23
+ def self.down
24
+ remove_column :contacts, :properties
25
+ end
26
+ end
27
+
28
+ Once your database is ready, you need to declare the property columns:
29
+
30
+ class Contact < ActiveRecord::Base
31
+ include Property
32
+ property do |p|
33
+ p.string 'first_name', 'name', 'phone'
34
+ p.datetime 'contacted_at', :default => Proc.new {Time.now}
35
+ end
36
+ end
37
+
38
+ You can now read property values with:
39
+
40
+ @contact.prop['first_name']
41
+ @contact.first_name
42
+
43
+ And set them with:
44
+
45
+ @contact.update_attributes('first_name' => 'Mahatma')
46
+ @contact.prop['name'] = 'Gandhi'
47
+ @contact.name = 'Gandhi'
48
+
data/Rakefile ADDED
@@ -0,0 +1,51 @@
1
+ require 'pathname'
2
+ $LOAD_PATH.unshift((Pathname(__FILE__).dirname + 'lib').expand_path)
3
+
4
+ require 'property'
5
+ require 'rake'
6
+ require 'rake/testtask'
7
+
8
+ Rake::TestTask.new(:test) do |test|
9
+ test.libs << 'lib' << 'test'
10
+ test.pattern = 'test/**/**_test.rb'
11
+ test.verbose = true
12
+ end
13
+
14
+ begin
15
+ require 'rcov/rcovtask'
16
+ Rcov::RcovTask.new do |test|
17
+ test.libs << 'test' << 'lib'
18
+ test.pattern = 'test/**/**_test.rb'
19
+ test.verbose = true
20
+ test.rcov_opts = ['-T', '--exclude-only', '"test\/,^\/"']
21
+ end
22
+ rescue LoadError
23
+ task :rcov do
24
+ abort "RCov is not available. In order to run rcov, you must: sudo gem install rcov"
25
+ end
26
+ end
27
+
28
+ task :default => :test
29
+
30
+
31
+ # GEM management
32
+ begin
33
+ require 'jeweler'
34
+ Jeweler::Tasks.new do |gemspec|
35
+ gemspec.name = 'property'
36
+ gemspec.summary = 'model properties wrap into a single database column'
37
+ gemspec.description = "Wrap model properties into a single database column and declare properties from within the model."
38
+ gemspec.email = "gaspard@teti.ch"
39
+ gemspec.homepage = "http://zenadmin.org/635"
40
+ gemspec.authors = ['Renaud Kern', 'Gaspard Bucher']
41
+ gemspec.version = Property::VERSION
42
+ gemspec.rubyforge_project = 'property'
43
+
44
+ # Gem dependecies
45
+ gemspec.add_development_dependency('shoulda')
46
+ gemspec.add_dependency('active_record')
47
+ end
48
+ rescue LoadError
49
+ puts "Jeweler not available. Gem packaging tasks not available."
50
+ end
51
+ #
@@ -0,0 +1,12 @@
1
+ class PropretyGenerator < MigrationGenerator
2
+
3
+ def initialize(runtime_args, runtime_options={})
4
+ super(["property=rety_migration"])
5
+ end
6
+
7
+ def manifest
8
+ record do |m|
9
+ m.migration_template( 'migration.rb', 'db/migrate')
10
+ end
11
+ end
12
+ end
data/lib/property.rb ADDED
@@ -0,0 +1,16 @@
1
+ require 'property/attribute'
2
+ require 'property/dirty'
3
+ require 'property/properties'
4
+ require 'property/column'
5
+ require 'property/declaration'
6
+ require 'property/serialization/json'
7
+
8
+ module Property
9
+ VERSION = '0.5.0'
10
+
11
+ def self.included(base)
12
+ base.class_eval do
13
+ include ::Property::Attribute
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,89 @@
1
+ module Property
2
+ # The Property::Attribute module is included in ActiveRecord model for CRUD operations
3
+ # on properties. These ared stored in a table field called 'properties' and are accessed
4
+ # with #properties or #prop and properties= methods.
5
+ #
6
+ # The properties are encoded et decoded with a serialization tool than you can change by including
7
+ # a Serialization module that should implement 'encode_properties' and 'decode_properties'.
8
+ # The default is to use Marshal through Property::Serialization::Marshal.
9
+ #
10
+ # The attributes= method filters native attributes and properties in order to store
11
+ # them apart.
12
+ #
13
+ module Attribute
14
+
15
+ def self.included(base)
16
+ base.class_eval do
17
+ include InstanceMethods
18
+ include Serialization::JSON
19
+ include Declaration
20
+ include Dirty
21
+
22
+ before_save :dump_properties
23
+
24
+ alias_method_chain :attributes=, :properties
25
+ end
26
+ end
27
+
28
+ module InstanceMethods
29
+ def properties
30
+ @properties ||= load_properties
31
+ end
32
+
33
+ alias_method :prop, :properties
34
+
35
+ # Define a set of properties. This acts like 'attributes=': it merges the current
36
+ # properties with the list of provided key/values. Note that unlike 'attributes=',
37
+ # the keys must be provided as strings, not symbols. For efficiency reasons and
38
+ # simplification of the API, we do not convert from symbols.
39
+ def properties=(new_properties)
40
+ return if new_properties.nil?
41
+ properties.merge!(new_properties)
42
+ end
43
+
44
+ alias_method :prop=, :properties=
45
+
46
+ # Force a reload of the properties from the ones stored in the database.
47
+ def reload_properties!
48
+ @properties = load_properties
49
+ end
50
+
51
+ private
52
+ def attributes_with_properties=(attributes, guard_protected_attributes = true)
53
+ columns = self.class.column_names
54
+ properties = {}
55
+
56
+ attributes.keys.each do |k|
57
+ if !respond_to?("#{k}=") && !columns.include?(k)
58
+ properties[k] = attributes.delete(k)
59
+ end
60
+ end
61
+
62
+ self.properties = properties
63
+ self.attributes_without_properties = attributes
64
+ end
65
+
66
+ def load_properties
67
+ raw_data = read_attribute('properties')
68
+ prop = raw_data ? decode_properties(raw_data) : Properties.new
69
+ # We need to set the owner to access property definitions and enable
70
+ # type casting on write.
71
+ prop.owner = self
72
+ prop
73
+ end
74
+
75
+ def dump_properties
76
+ if @properties
77
+ @properties.compact!
78
+ if !@properties.empty?
79
+ write_attribute('properties', encode_properties(@properties))
80
+ else
81
+ write_attribute('properties', nil)
82
+ end
83
+ end
84
+ @properties.clear_changes!
85
+ true
86
+ end
87
+ end # InstanceMethods
88
+ end # Attribute
89
+ end # Property
@@ -0,0 +1,35 @@
1
+ require 'active_record'
2
+ ActiveRecord.load_all!
3
+
4
+ module Property
5
+ # The Column class is used to hold information about a Property declaration,
6
+ # such as name, type and options. It is also used to typecast from strings to
7
+ # the proper type (date, integer, float, etc).
8
+ class Column < ::ActiveRecord::ConnectionAdapters::Column
9
+
10
+ def initialize(name, default, type, options={})
11
+ name = name.to_s
12
+ extract_property_options(options)
13
+ super(name, default, type, options)
14
+ end
15
+
16
+ def validate(value, errors)
17
+ if !value.kind_of?(klass)
18
+ if value.nil?
19
+ default
20
+ else
21
+ errors.add("#{name}", "invalid data type. Received #{value.class}, expected #{klass}.")
22
+ nil
23
+ end
24
+ end
25
+ end
26
+
27
+ def indexed?
28
+ @indexed
29
+ end
30
+
31
+ def extract_property_options(options)
32
+ @indexed = options.delete(:indexed)
33
+ end
34
+ end # Column
35
+ end # Property
@@ -0,0 +1,120 @@
1
+ module Property
2
+
3
+ # Property::Declaration module is used to declare property definitions in a Class. The module
4
+ # also manages property inheritence in sub-classes.
5
+ module Declaration
6
+
7
+ def self.included(base)
8
+ base.class_eval do
9
+ extend ClassMethods
10
+ include InstanceMethods
11
+
12
+ class << self
13
+ attr_accessor :own_property_columns
14
+ attr_accessor :property_definition_proxy
15
+ end
16
+
17
+ validate :properties_validation, :if => :properties
18
+ end
19
+ end
20
+
21
+ module ClassMethods
22
+ class DefinitionProxy
23
+ def initialize(klass)
24
+ @klass = klass
25
+ end
26
+
27
+ def column(name, default, type, options)
28
+ if columns[name.to_s]
29
+ raise TypeError.new("Property '#{name}' is already defined.")
30
+ else
31
+ own_columns[name] = Property::Column.new(name, default, type, options)
32
+ end
33
+ end
34
+
35
+ # If someday we find the need to insert other native classes directly in the DB, we
36
+ # could use this:
37
+ # p.serialize MyClass, xxx, xxx
38
+ # def serialize(klass, name, options={})
39
+ # if @klass.super_property_columns[name.to_s]
40
+ # raise TypeError.new("Property '#{name}' is already defined in a superclass.")
41
+ # elsif !@klass.validate_property_class(type)
42
+ # raise TypeError.new("Custom type '#{type}' cannot be serialized.")
43
+ # else
44
+ # # Find a way to insert the type (maybe with 'serialize'...)
45
+ # # (@klass.own_property_columns ||= {})[name] = Property::Column.new(name, type, options)
46
+ # end
47
+ # end
48
+
49
+ # def string(*args)
50
+ # options = args.extract_options!
51
+ # column_names = args
52
+ # default = options.delete(:default)
53
+ # column_names.each { |name| column(name, default, 'string', options) }
54
+ # end
55
+ %w( string text integer float decimal datetime timestamp time date binary boolean ).each do |column_type|
56
+ class_eval <<-EOV
57
+ def #{column_type}(*args)
58
+ options = args.extract_options!
59
+ column_names = args
60
+ default = options.delete(:default)
61
+ column_names.each { |name| column(name, default, '#{column_type}', options) }
62
+ end
63
+ EOV
64
+ end
65
+
66
+ private
67
+ def own_columns
68
+ @klass.own_property_columns ||= {}
69
+ end
70
+
71
+ def columns
72
+ @klass.property_columns
73
+ end
74
+
75
+ end
76
+
77
+ # Use this class method to declare properties that will be used in your models. Note
78
+ # that you must provide string keys. Example:
79
+ # property.string 'phone', :default => ''
80
+ #
81
+ # You can also use a block:
82
+ # property do |p|
83
+ # p.string 'phone', 'name', :default => ''
84
+ # end
85
+ def property
86
+ proxy = self.property_definition_proxy ||= DefinitionProxy.new(self)
87
+ if block_given?
88
+ yield proxy
89
+ end
90
+ proxy
91
+ end
92
+
93
+ # Return the list of all properties defined for the current class, including the properties
94
+ # defined in the parent class.
95
+ def property_columns
96
+ super_property_columns.merge(self.own_property_columns || {})
97
+ end
98
+
99
+ def property_column_names
100
+ property_columns.keys
101
+ end
102
+
103
+ def super_property_columns
104
+ if superclass.respond_to?(:property_columns)
105
+ superclass.property_columns
106
+ else
107
+ {}
108
+ end
109
+ end
110
+ end # ClassMethods
111
+
112
+ module InstanceMethods
113
+
114
+ protected
115
+ def properties_validation
116
+ properties.validate
117
+ end
118
+ end # InsanceMethods
119
+ end # Declaration
120
+ end # Property
@@ -0,0 +1,98 @@
1
+ module Property
2
+ # This module implement ActiveRecord::Dirty functionalities with Property attributes. It
3
+ # enables the usual 'changed?' and 'changes' to include property changes. Unlike dirty,
4
+ # 'foo_changed?' and 'foo_was' are not defined in the model and should be replaced by
5
+ # #prop.foo_changed? and prop.foo_was.
6
+ #
7
+ # If you need to find the property changes only, you can use #prop.changes or prop.changed?
8
+ #
9
+ module Dirty
10
+
11
+ private
12
+
13
+ def self.included(base)
14
+ base.class_eval do
15
+ alias_method_chain :changed?, :properties
16
+ alias_method_chain :changed, :properties
17
+ alias_method_chain :changes, :properties
18
+ end
19
+ end
20
+
21
+ def changed_with_properties?
22
+ changed_without_properties? || properties.changed?
23
+ end
24
+
25
+ def changed_with_properties
26
+ changed_without_properties + properties.changed
27
+ end
28
+
29
+ def changes_with_properties
30
+ changes_without_properties.merge properties.changes
31
+ end
32
+
33
+ end # Dirty
34
+
35
+ # This module implements ActiveRecord::Dirty functionalities for the properties hash.
36
+ module DirtyProperties
37
+ CHANGED_REGEXP = %r{(.+)_changed\?$}
38
+ WAS_REGEXP = %r{(.+)_was$}
39
+
40
+ def []=(key, value)
41
+ @original_hash ||= self.dup
42
+ super
43
+ end
44
+
45
+ def delete(key)
46
+ @original_hash ||= self.dup
47
+ super
48
+ end
49
+
50
+ def merge!(other_hash)
51
+ @original_hash ||= self.dup
52
+ super
53
+ end
54
+
55
+ def changed?
56
+ !changes.empty?
57
+ end
58
+
59
+ def changed
60
+ changes.keys
61
+ end
62
+
63
+ def changes
64
+ return {} unless @original_hash
65
+ compact!
66
+ changes = {}
67
+
68
+ # look for updated value
69
+ each do |key, new_value|
70
+ if new_value != (old_value = @original_hash[key])
71
+ changes[key] = [old_value, new_value]
72
+ end
73
+ end
74
+
75
+ # look for deleted value
76
+ (@original_hash.keys - keys).each do |key|
77
+ changes[key] = [@original_hash[key], nil]
78
+ end
79
+
80
+ changes
81
+ end
82
+
83
+ # This method should be called to reset dirty information before dump
84
+ def clear_changes!
85
+ remove_instance_variable(:@original_hash) if defined?(@original_hash)
86
+ end
87
+
88
+ def method_missing(method, *args)
89
+ if method.to_s =~ CHANGED_REGEXP
90
+ !changes[$1].nil?
91
+ elsif method.to_s =~ WAS_REGEXP
92
+ (@original_hash || self)[$1]
93
+ else
94
+ super
95
+ end
96
+ end
97
+ end
98
+ end # Property