property 1.2.0 → 2.0.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/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