property 1.2.0 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/History.txt CHANGED
@@ -1,3 +1,17 @@
1
+ == 2.0.0
2
+
3
+ * Major enhancements
4
+ * Rewrite of the core engine to remove all the metaclass and anonymous module codes (generated memory leaks).
5
+ * Removed "actions" to create methods in a Role (you have to define them in the host class instead).
6
+ * Not defining accessor methods in Roles (using method missing instead). Accessors in Schema are defined directly in the class.
7
+ * Not checking for redefined methods anymore.
8
+
9
+ == 1.3.0 2010-11-9 (not released)
10
+
11
+ * Major enhancements
12
+ * Removed 'included_in' check (the same property can now be redefined)
13
+ * Added support for field indices (as columns in the owner table).
14
+
1
15
  == 1.2.0 2010-09-26
2
16
 
3
17
  * Major enhancements
data/README.rdoc CHANGED
@@ -14,11 +14,15 @@ changes detections and migrations.
14
14
  == Usage
15
15
 
16
16
  You first need to create a migration to add a 'text' field named 'properties' to
17
- your model. Something like this:
17
+ your model. Choose a text format that is really long (otherwize your data will be truncated and the property will fail to decode => error). Do something like this:
18
18
 
19
19
  class AddPropertyToContact < ActiveRecord::Migration
20
20
  def self.up
21
- add_column :contacts, :properties, :text
21
+ if ActiveRecord::Base.configurations[RAILS_ENV]['adapter'] == 'mysql'
22
+ execute "ALTER TABLE contacts ADD COLUMN properties LONGTEXT"
23
+ else
24
+ add_column :contacts, :properties, :text
25
+ end
22
26
  end
23
27
 
24
28
  def self.down
data/lib/property.rb CHANGED
@@ -2,8 +2,8 @@ require 'property/attribute'
2
2
  require 'property/dirty'
3
3
  require 'property/properties'
4
4
  require 'property/column'
5
+ require 'property/role_module'
5
6
  require 'property/role'
6
- require 'property/stored_role'
7
7
  require 'property/schema'
8
8
  require 'property/declaration'
9
9
  require 'property/db'
@@ -11,6 +11,7 @@ require 'property/index'
11
11
  require 'property/serialization/json'
12
12
  require 'property/core_ext/time'
13
13
  require 'property/base'
14
+ require 'property/stored_role'
14
15
 
15
16
  module Property
16
17
  def self.included(base)
@@ -19,7 +19,7 @@ module Property
19
19
  end
20
20
  end
21
21
 
22
- # This is just a helper module that includes necessary code for property access, but without
22
+ # This is just a helper module that includes the necessary code for property access, but without
23
23
  # the validation/save hooks.
24
24
  module Base
25
25
  def self.included(base)
@@ -92,7 +92,8 @@ module Property
92
92
 
93
93
  private
94
94
  def attributes_with_properties=(attributes, guard_protected_attributes = true)
95
- property_columns = self.properties.columns
95
+ property_columns = self.schema.column_names
96
+
96
97
  properties = {}
97
98
 
98
99
  attributes.keys.each do |k|
@@ -1,7 +1,8 @@
1
1
  module Property
2
2
 
3
- # Property::Declaration module is used to declare property definitions in a Class. The module
4
- # also manages property inheritence in sub-classes.
3
+ # This module is used to manage property definitions (the schema) in a Class. The module
4
+ # also manages property inheritence in sub-classes by linking the schema in the sub-class with
5
+ # the schema in the superclass.
5
6
  module Declaration
6
7
  def self.included(base)
7
8
  base.class_eval do
@@ -10,6 +11,8 @@ module Property
10
11
  end
11
12
  end
12
13
 
14
+ # This is just a helper module that includes the necessary code for property definition, but without
15
+ # the validation/save hooks.
13
16
  module Base
14
17
  def self.included(base)
15
18
  base.class_eval do
@@ -17,6 +20,7 @@ module Property
17
20
  include InstanceMethods
18
21
 
19
22
  class << self
23
+ # Every class has it's own schema.
20
24
  attr_accessor :schema
21
25
 
22
26
  def schema
@@ -24,12 +28,9 @@ module Property
24
28
  end
25
29
 
26
30
  private
31
+ # Build schema and manage inheritance.
27
32
  def make_schema
28
- schema = Property::Schema.new(self.to_s, self)
29
- if superclass.respond_to?(:schema)
30
- schema.has_role superclass
31
- end
32
- schema
33
+ Property::Schema.new(self.to_s, :class => self)
33
34
  end
34
35
  end
35
36
  end
@@ -40,11 +41,11 @@ module Property
40
41
 
41
42
  # Include a new set of property definitions (Role) into the current class schema.
42
43
  # You can also provide a class to simulate multiple inheritance.
43
- def has_role(role)
44
- schema.has_role role
44
+ def include_role(role)
45
+ schema.include_role role
45
46
  end
46
47
 
47
- # Return true if the current object has all the roles of the given object, class or role.
48
+ # Return true if the current object has all the roles of the given schema or role.
48
49
  def has_role?(role)
49
50
  schema.has_role? role
50
51
  end
@@ -63,7 +64,27 @@ module Property
63
64
  # end
64
65
  # end
65
66
  def property(&block)
66
- schema.role.property(&block)
67
+ schema.property(&block)
68
+ end
69
+
70
+ # Define property methods in a class. This is only triggered when properties are declared directly in the
71
+ # class and not through Role inclusion.
72
+ def define_property_methods(column)
73
+ attr_name = column.name
74
+
75
+ class_eval(%Q{
76
+ def #{attr_name} # def title
77
+ prop['#{attr_name}'] # prop['title']
78
+ end # end
79
+ #
80
+ def #{attr_name}? # def title?
81
+ prop['#{attr_name}'] # prop['title']
82
+ end # end
83
+ #
84
+ def #{attr_name}=(new_value) # def title=(new_value)
85
+ prop['#{attr_name}'] = new_value # prop['title'] = new_value
86
+ end # end
87
+ }, __FILE__, __LINE__)
67
88
  end
68
89
  end # ClassMethods
69
90
 
@@ -76,8 +97,8 @@ module Property
76
97
 
77
98
  # Include a new set of property definitions (Role) into the current instance's schema.
78
99
  # You can also provide a class to simulate multiple inheritance.
79
- def has_role(role)
80
- own_schema.has_role role
100
+ def include_role(role)
101
+ own_schema.include_role role
81
102
  end
82
103
 
83
104
  # Return the list of active roles. The active roles are all the Roles included
@@ -99,12 +120,39 @@ module Property
99
120
  def own_schema
100
121
  @own_schema ||= make_own_schema
101
122
  end
123
+
124
+ # When roles are dynamically added to a model, we use method_missing to mimic property
125
+ # accessors. Since this has a cost, it is better to use 'prop' based accessors in production
126
+ # code (this is mostly helpful for testing/debugging).
127
+ def method_missing(meth, *args, &block)
128
+ method = meth.to_s
129
+ if args.empty?
130
+ if method[-1..-1] == '?'
131
+ # predicate
132
+ key = method[0..-2]
133
+ else
134
+ # reader
135
+ key = method
136
+ end
137
+
138
+ if schema.has_column?(key)
139
+ return prop[key]
140
+ end
141
+ elsif args.size == 1 && method[-1..-1] == '='
142
+ # writer
143
+ key = method[0..-2]
144
+ if schema.has_column?(key)
145
+ return prop[key] = args.first
146
+ end
147
+ end
148
+ # Not a property method
149
+ super
150
+ end
151
+
102
152
  private
153
+ # Create a schema for the instance and inherit from the class
103
154
  def make_own_schema
104
- this = class << self; self; end
105
- schema = Property::Schema.new(nil, this)
106
- schema.has_role self.class
107
- schema
155
+ Property::Schema.new(nil, :superschema => self.class.schema)
108
156
  end
109
157
  end # InsanceMethods
110
158
  end # Declaration
@@ -4,12 +4,14 @@ module Property
4
4
  # also manages property inheritence in sub-classes.
5
5
  module Index
6
6
  KEY = Property::Db.connection.quote_column_name('key')
7
+ FIELD_INDEX_REGEXP = %r{\A\.(.*)\Z}
7
8
 
8
9
  def self.included(base)
9
10
  base.class_eval do
10
11
  extend ClassMethods
11
12
  include InstanceMethods
12
- after_save :property_index
13
+ before_save :property_field_index
14
+ after_save :property_index
13
15
  after_destroy :property_index_destroy
14
16
  end
15
17
  end
@@ -25,6 +27,12 @@ module Property
25
27
  module InstanceMethods
26
28
 
27
29
  def rebuild_index!
30
+ property_field_index
31
+
32
+ if changed_without_properties?
33
+ update_without_callbacks # no validation, no callbacks
34
+ end
35
+
28
36
  property_index
29
37
  end
30
38
 
@@ -144,9 +152,21 @@ module Property
144
152
  self.class.connection.execute "DELETE FROM #{index_table_name(group_name)} WHERE #{index_reader_sql(group_name)} AND #{KEY} IN (#{keys.map{|key| connection.quote(key)}.join(',')})"
145
153
  end
146
154
 
155
+ # This method prepares the index
156
+ def property_field_index
157
+ schema.index_groups.each do |group_name, definitions|
158
+ if group_name =~ FIELD_INDEX_REGEXP
159
+ # write attribute in owner
160
+ key = definitions.first.first
161
+ self[$1] = prop[key]
162
+ end
163
+ end
164
+ end
165
+
147
166
  # This method prepares the index
148
167
  def property_index
149
168
  schema.index_groups.each do |group_name, definitions|
169
+ next if group_name =~ FIELD_INDEX_REGEXP
150
170
  cur_indices = {}
151
171
  definitions.each do |key, proc|
152
172
  if key
@@ -69,7 +69,7 @@ module Property
69
69
  end
70
70
 
71
71
  def columns
72
- @columns ||= @owner.schema.columns
72
+ @owner.schema.columns
73
73
  end
74
74
  end
75
75
  end
data/lib/property/role.rb CHANGED
@@ -1,18 +1,24 @@
1
- require 'property/role_module'
1
+ require 'property/redefined_property_error'
2
+ require 'property/redefined_method_error'
2
3
 
3
4
  module Property
4
- # This class holds a set of property definitions. This is like a Module in ruby:
5
- # by 'including' this role in a class or in an instance, you augment the said
6
- # object with the role's property definitions.
5
+ # The Role holds information on a group of property columns. The "Role" is used
6
+ # in the same way as the ruby Module: as a mixin. The Schema class "includes" roles.
7
7
  class Role
8
- attr_accessor :name
9
8
  include RoleModule
10
9
 
11
- def self.new(name, &block)
10
+ # Create a new role. If a block is provided, this block can be used
11
+ # to define properties:
12
+ #
13
+ # Example:
14
+ # @role = Role.new('Poet') do |p|
15
+ # p.string :muse
16
+ # end
17
+ def self.new(name, opts = nil, &block)
12
18
  if name.kind_of?(Hash)
13
- obj = super(name[:name] || name['name'])
19
+ obj = super(name[:name] || name['name'], opts)
14
20
  else
15
- obj = super(name)
21
+ obj = super(name, opts)
16
22
  end
17
23
 
18
24
  if block_given?
@@ -22,8 +28,8 @@ module Property
22
28
  end
23
29
 
24
30
  # Initialize a new role with the given name
25
- def initialize(name)
26
- self.name = name
31
+ def initialize(name, opts = nil)
32
+ @name = name
27
33
  initialize_role_module
28
34
  end
29
35
  end
@@ -1,49 +1,13 @@
1
- require 'property/redefined_property_error'
2
- require 'property/redefined_method_error'
3
-
4
1
  module Property
5
- # This class holds a set of property definitions. This is like a Module in ruby:
6
- # by 'including' this role in a class or in an instance, you augment the said
7
- # object with the role's property definitions.
2
+ # The RoleModule enables a class to hold information on a group of property columns.
3
+ # This enables classes to act in the same way as the ruby Module: as a mixin.
4
+ # The Schema class "includes" roles.
8
5
  module RoleModule
9
- attr_accessor :included, :accessor_module
10
-
11
- # We cannot use attr_accessor to define these because we are in a module
12
- # when the module is included in an ActiveRecord class.
13
- #%W{name included accessor_module}.each do |name|
14
- # class_eval %Q{
15
- # def #{name}
16
- # @#{name}
17
- # end
18
- #
19
- # def #{name}=(value)
20
- # @#{name} = value
21
- # end
22
- # }
23
- #end
24
-
25
- # Initialize module (should be called from within including class's initialize method).
26
- def initialize_role_module
27
- @included_in_schemas = []
28
- @group_indices = []
29
- @accessor_module = build_accessor_module
30
- end
31
-
32
- # List all property definitiosn for the current role
33
- def columns
34
- @columns ||= {}
35
- end
36
-
37
- # Return a list of index definitions in the form [type, key, proc_or_nil]
38
- def indices
39
- columns.values.select do |c|
40
- c.indexed?
41
- end.map do |c|
42
- [c.index, c.name, c.index_proc]
43
- end + @group_indices
6
+ def name
7
+ @name
44
8
  end
45
9
 
46
- # Return true if the Role contains the given column (property).
10
+ # Return true if the role contains the given column (property).
47
11
  def has_column?(name)
48
12
  column_names.include?(name)
49
13
  end
@@ -53,52 +17,71 @@ module Property
53
17
  columns.keys
54
18
  end
55
19
 
56
- # Use this method to declare properties into a Role.
20
+ # List all property columns defined for this role
21
+ def columns
22
+ defined_columns
23
+ end
24
+
25
+ # Use this method to declare properties into a Role or Schema.
26
+ #
57
27
  # Example:
58
28
  # @role.property.string 'phone', :default => ''
59
29
  #
30
+ # You can also use the "property" method in the class to access the schema:
31
+ #
32
+ # Example:
33
+ # Page.property.string 'phone', :default => ''
34
+ #
60
35
  # You can also use a block:
61
- # @role.property do |p|
36
+ # Page.property do |p|
62
37
  # p.string 'phone', 'name', :default => ''
63
38
  # end
64
39
  def property
65
40
  if block_given?
66
- yield accessor_module
41
+ yield self
67
42
  end
68
- accessor_module
43
+ self
69
44
  end
70
45
 
71
- # @internal
72
- # This is called when the role is included in a schema
73
- def included_in(schema)
74
- @included_in_schemas << schema
46
+ %w( string text integer float decimal datetime timestamp time date binary boolean ).each do |column_type|
47
+ class_eval <<-EOV
48
+ def #{column_type}(*args)
49
+ options = args.extract_options!
50
+ column_names = args.flatten
51
+ default = options.delete(:default)
52
+ column_names.each { |name| add_column(Property::Column.new(name, default, '#{column_type}', options.merge(:role => self))) }
53
+ end
54
+ EOV
75
55
  end
76
56
 
77
- # @internal
78
- def add_column(column)
79
- name = column.name
80
-
81
- if columns[name]
82
- raise RedefinedPropertyError.new("Property '#{name}' is already defined.")
83
- else
84
- verify_not_defined_in_schemas_using_this_role(name)
85
- verify_method_not_defined_in_classes_using_this_role(name)
86
- define_property_methods(column) if column.should_create_accessors?
87
- columns[column.name] = column
88
- end
57
+ # This is used to serialize a non-native DB type. Use:
58
+ # p.serialize 'pet', Dog
59
+ def serialize(name, klass, options = {})
60
+ Property.validate_property_class(klass)
61
+ add_column(Property::Column.new(name, nil, klass, options.merge(:role => self)))
89
62
  end
90
63
 
91
- # @internal
92
- def add_index(type, proc)
64
+ # This is used to create complex indices with the following syntax:
65
+ #
66
+ # p.index(:text) do |r| # r = record
67
+ # {
68
+ # "high" => "gender:#{r.gender} age:#{r.age} name:#{r.name}",
69
+ # "name_#{r.lang}" => r.name, # multi-lingual index
70
+ # }
71
+ # end
72
+ #
73
+ # The first argument is the type (used to locate the table where the data will be stored) and the block
74
+ # will be yielded with the record and should return a hash of key => value pairs.
75
+ def index(type, &block)
93
76
  # type, key, proc
94
- @group_indices << [type, nil, proc]
77
+ @group_indices << [type, nil, block]
95
78
  end
96
79
 
97
- # Returns true if the current role is used by the given object. A Role is
98
- # considered to be used if any of it's attributes is not blank in the object's
80
+ # Returns true if the role is used by the given object. A role is
81
+ # considered to be used if any of it's defined columns is not blank in the object's
99
82
  # properties.
100
83
  def used_in(object)
101
- used_keys_in(object) != []
84
+ object.properties.keys & defined_columns.keys != []
102
85
  end
103
86
 
104
87
  # Returns the list of column names in the current role that are used by the
@@ -107,140 +90,44 @@ module Property
107
90
  object.properties.keys & column_names
108
91
  end
109
92
 
110
- private
111
- def build_accessor_module
112
- accessor_module = Module.new
113
- accessor_module.class_eval do
114
- class << self
115
- attr_accessor :role
116
-
117
- # def string(*args)
118
- # options = args.extract_options!
119
- # column_names = args
120
- # default = options.delete(:default)
121
- # column_names.each { |name| column(name, default, 'string', options) }
122
- # end
123
- %w( string text integer float decimal datetime timestamp time date binary boolean ).each do |column_type|
124
- class_eval <<-EOV
125
- def #{column_type}(*args)
126
- options = args.extract_options!
127
- column_names = args.flatten
128
- default = options.delete(:default)
129
- column_names.each { |name| role.add_column(Property::Column.new(name, default, '#{column_type}', options.merge(:role => role))) }
130
- end
131
- EOV
132
- end
133
-
134
- # This is used to serialize a non-native DB type. Use:
135
- # p.serialize 'pet', Dog
136
- def serialize(name, klass, options = {})
137
- Property.validate_property_class(klass)
138
- role.add_column(Property::Column.new(name, nil, klass, options.merge(:role => role)))
139
- end
140
-
141
- # This is used to create complex indices with the following syntax:
142
- #
143
- # p.index(:text) do |r| # r = record
144
- # {
145
- # "high" => "gender:#{r.gender} age:#{r.age} name:#{r.name}",
146
- # "name_#{r.lang}" => r.name, # multi-lingual index
147
- # }
148
- # end
149
- #
150
- # The first argument is the type (used to locate the table where the data will be stored) and the block
151
- # will be yielded with the record and should return a hash of key => value pairs.
152
- def index(type, &block)
153
- role.add_index(type, block)
154
- end
155
-
156
- alias actions class_eval
157
- end
158
- end
159
- accessor_module.role = self
160
- accessor_module
161
- end
162
-
163
- def define_property_methods(column)
164
- name = column.name
165
-
166
- #if create_time_zone_conversion_attribute?(name, column)
167
- # define_read_property_method_for_time_zone_conversion(name)
168
- #else
169
- define_read_property_method(name.to_sym, name, column)
170
- #end
171
-
172
- #if create_time_zone_conversion_attribute?(name, column)
173
- # define_write_property_method_for_time_zone_conversion(name)
174
- #else
175
- define_write_property_method(name.to_sym)
176
- #end
177
-
178
- define_question_property_method(name)
179
- end
180
-
181
- # Define a property reader method. Cope with nil column.
182
- def define_read_property_method(symbol, attr_name, column)
183
- # Unlike rails, we do not cast on read
184
- evaluate_attribute_property_method attr_name, "def #{symbol}; prop['#{attr_name}']; end"
185
- end
186
-
187
- # Defined for all +datetime+ and +timestamp+ attributes when +time_zone_aware_attributes+ are enabled.
188
- # This enhanced read method automatically converts the UTC time stored in the database to the time zone stored in Time.zone.
189
- # def define_read_property_method_for_time_zone_conversion(attr_name)
190
- # method_body = <<-EOV
191
- # def #{attr_name}(reload = false)
192
- # cached = @attributes_cache['#{attr_name}']
193
- # return cached if cached && !reload
194
- # time = properties['#{attr_name}']
195
- # @attributes_cache['#{attr_name}'] = time.acts_like?(:time) ? time.in_time_zone : time
196
- # end
197
- # EOV
198
- # evaluate_attribute_property_method attr_name, method_body
199
- # end
200
-
201
- # Defines a predicate method <tt>attr_name?</tt>.
202
- def define_question_property_method(attr_name)
203
- evaluate_attribute_property_method attr_name, "def #{attr_name}?; prop['#{attr_name}']; end", "#{attr_name}?"
204
- end
205
-
206
- def define_write_property_method(attr_name)
207
- evaluate_attribute_property_method attr_name, "def #{attr_name}=(new_value);prop['#{attr_name}'] = new_value; end", "#{attr_name}="
208
- end
93
+ # Return a list of index definitions in the form [type, key, proc_or_nil]
94
+ def indices
95
+ columns.values.select do |c|
96
+ c.indexed?
97
+ end.map do |c|
98
+ [c.index, c.name, c.index_proc]
99
+ end + @group_indices
100
+ end
209
101
 
210
- # Defined for all +datetime+ and +timestamp+ attributes when +time_zone_aware_attributes+ are enabled.
211
- # This enhanced write method will automatically convert the time passed to it to the zone stored in Time.zone.
212
- # def define_write_property_method_for_time_zone_conversion(attr_name)
213
- # method_body = <<-EOV
214
- # def #{attr_name}=(time)
215
- # unless time.acts_like?(:time)
216
- # time = time.is_a?(String) ? Time.zone.parse(time) : time.to_time rescue time
217
- # end
218
- # time = time.in_time_zone rescue nil if time
219
- # prop['#{attr_name}'] = time
220
- # end
221
- # EOV
222
- # evaluate_attribute_property_method attr_name, method_body, "#{attr_name}="
223
- # end
102
+ def inspect
103
+ # "#<#{self.class}:#{sprintf("0x%x", object_id)} #{@name.inspect} @klass = #{@klass.inspect} @defined_columns = #{@defined_columns.inspect}>"
104
+ "#<#{self.class}:#{sprintf("0x%x", object_id)} #{column_names.inspect}>"
105
+ end
224
106
 
225
- # Evaluate the definition for an attribute related method
226
- def evaluate_attribute_property_method(attr_name, method_definition, method_name=attr_name)
227
- accessor_module.class_eval(method_definition, __FILE__, __LINE__)
107
+ protected
108
+ def initialize_role_module
109
+ @group_indices = []
228
110
  end
229
111
 
230
- def verify_not_defined_in_schemas_using_this_role(name)
231
- @included_in_schemas.each do |schema|
232
- if schema.columns[name]
233
- raise RedefinedPropertyError.new("Property '#{name}' is already defined in #{schema.name}.")
234
- end
235
- end
112
+ # List all property columns defined for this role
113
+ def defined_columns
114
+ @defined_columns ||= {}
236
115
  end
237
116
 
238
- def verify_method_not_defined_in_classes_using_this_role(name)
239
- @included_in_schemas.each do |schema|
240
- if schema.binding.superclass.method_defined?(name)
241
- raise RedefinedMethodError.new("Method '#{name}' is already defined in #{schema.binding.superclass} or ancestors.")
117
+ # @internal
118
+ def add_column(column)
119
+ name = column.name
120
+ # we do not use self.defined_columns because this triggers the load_columns_from_db in StoredRole (= inf loop).
121
+ defined_columns = (@defined_columns ||= {})
122
+
123
+ if defined_columns[name]
124
+ raise RedefinedPropertyError.new("Property '#{name}' is already defined.")
125
+ else
126
+ defined_columns[column.name] = column
127
+ if @klass && column.should_create_accessors?
128
+ @klass.define_property_methods(column)
242
129
  end
243
130
  end
244
131
  end
245
- end
246
- end
132
+ end # RoleModule
133
+ end # Property