property 0.5.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/.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