property 0.9.1 → 1.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.
@@ -40,12 +40,18 @@ module Property
40
40
  errors = @owner.errors
41
41
  no_errors = true
42
42
 
43
+ original_hash = @original_hash || self
44
+
43
45
  bad_keys = keys - column_names
44
46
  missing_keys = column_names - keys
45
47
  keys_to_validate = keys - bad_keys
46
48
 
47
49
  bad_keys.each do |key|
48
- errors.add("#{key}", 'property is not declared')
50
+ if original_hash[key] == self[key]
51
+ # ignore invalid legacy value
52
+ else
53
+ errors.add("#{key}", 'property not declared')
54
+ end
49
55
  end
50
56
 
51
57
  missing_keys.each do |key|
@@ -0,0 +1,8 @@
1
+ require 'property/error'
2
+
3
+ module Property
4
+ # This error is raised when a role is included into a class and this inclusion
5
+ # hides already defined methods (in superclass).
6
+ class RedefinedMethodError < Property::Error
7
+ end
8
+ end
@@ -0,0 +1,8 @@
1
+ require 'property/error'
2
+
3
+ module Property
4
+ # This error is raised when a role is included into a class and this inclusion
5
+ # hides already defined properties.
6
+ class RedefinedPropertyError < Property::Error
7
+ end
8
+ end
@@ -0,0 +1,30 @@
1
+ require 'property/role_module'
2
+
3
+ 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.
7
+ class Role
8
+ attr_accessor :name
9
+ include RoleModule
10
+
11
+ def self.new(name, &block)
12
+ if name.kind_of?(Hash)
13
+ obj = super(name[:name] || name['name'])
14
+ else
15
+ obj = super(name)
16
+ end
17
+
18
+ if block_given?
19
+ obj.property(&block)
20
+ end
21
+ obj
22
+ end
23
+
24
+ # Initialize a new role with the given name
25
+ def initialize(name)
26
+ self.name = name
27
+ initialize_role_module
28
+ end
29
+ end
30
+ end
@@ -1,27 +1,35 @@
1
+ require 'property/redefined_property_error'
2
+ require 'property/redefined_method_error'
3
+
1
4
  module Property
2
5
  # This class holds a set of property definitions. This is like a Module in ruby:
3
- # by 'including' this behavior in a class or in an instance, you augment the said
4
- # object with the behavior's property definitions.
5
- class Behavior
6
- attr_accessor :name, :included, :accessor_module
7
-
8
- def self.new(name, &block)
9
- obj = super
10
- if block_given?
11
- obj.property(&block)
12
- end
13
- obj
14
- end
15
-
16
- # Initialize a new behavior with the given name
17
- def initialize(name)
18
- @name = name
6
+ # by 'including' this role in a class or in an instance, you augment the said
7
+ # object with the role's property definitions.
8
+ 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
19
27
  @included_in_schemas = []
20
28
  @group_indices = []
21
29
  @accessor_module = build_accessor_module
22
30
  end
23
31
 
24
- # List all property definitiosn for the current behavior
32
+ # List all property definitiosn for the current role
25
33
  def columns
26
34
  @columns ||= {}
27
35
  end
@@ -32,14 +40,16 @@ module Property
32
40
  c.indexed?
33
41
  end.map do |c|
34
42
  if c.index == true
35
- [c.type, c.name]
43
+ [c.type.to_sym, c.name]
44
+ elsif c.index.kind_of?(Proc)
45
+ [c.type.to_sym, c.name, c.index]
36
46
  else
37
- [c.type, c.name, c.index]
47
+ [c.index, c.name]
38
48
  end
39
49
  end + @group_indices
40
50
  end
41
51
 
42
- # Return true if the Behavior contains the given column (property).
52
+ # Return true if the Role contains the given column (property).
43
53
  def has_column?(name)
44
54
  column_names.include?(name)
45
55
  end
@@ -49,12 +59,12 @@ module Property
49
59
  columns.keys
50
60
  end
51
61
 
52
- # Use this method to declare properties into a Behavior.
62
+ # Use this method to declare properties into a Role.
53
63
  # Example:
54
- # @behavior.property.string 'phone', :default => ''
64
+ # @role.property.string 'phone', :default => ''
55
65
  #
56
66
  # You can also use a block:
57
- # @behavior.property do |p|
67
+ # @role.property do |p|
58
68
  # p.string 'phone', 'name', :default => ''
59
69
  # end
60
70
  def property
@@ -65,8 +75,8 @@ module Property
65
75
  end
66
76
 
67
77
  # @internal
68
- # This is called when the behavior is included in a schema
69
- def included(schema)
78
+ # This is called when the role is included in a schema
79
+ def included_in(schema)
70
80
  @included_in_schemas << schema
71
81
  end
72
82
 
@@ -75,9 +85,10 @@ module Property
75
85
  name = column.name
76
86
 
77
87
  if columns[name]
78
- raise TypeError.new("Property '#{name}' is already defined.")
88
+ raise RedefinedPropertyError.new("Property '#{name}' is already defined.")
79
89
  else
80
- verify_not_defined_in_schemas_using_this_behavior(name)
90
+ verify_not_defined_in_schemas_using_this_role(name)
91
+ verify_method_not_defined_in_classes_using_this_role(name)
81
92
  define_property_methods(column) if column.should_create_accessors?
82
93
  columns[column.name] = column
83
94
  end
@@ -89,12 +100,25 @@ module Property
89
100
  @group_indices << [type, nil, proc]
90
101
  end
91
102
 
103
+ # Returns true if the current role is used by the given object. A Role is
104
+ # considered to be used if any of it's attributes is not blank in the object's
105
+ # properties.
106
+ def used_in(object)
107
+ used_keys_in(object) != []
108
+ end
109
+
110
+ # Returns the list of column names in the current role that are used by the
111
+ # given object (value not blank).
112
+ def used_keys_in(object)
113
+ object.properties.keys & column_names
114
+ end
115
+
92
116
  private
93
117
  def build_accessor_module
94
118
  accessor_module = Module.new
95
119
  accessor_module.class_eval do
96
120
  class << self
97
- attr_accessor :behavior
121
+ attr_accessor :role
98
122
 
99
123
  # def string(*args)
100
124
  # options = args.extract_options!
@@ -106,9 +130,9 @@ module Property
106
130
  class_eval <<-EOV
107
131
  def #{column_type}(*args)
108
132
  options = args.extract_options!
109
- column_names = args
133
+ column_names = args.flatten
110
134
  default = options.delete(:default)
111
- column_names.each { |name| behavior.add_column(Property::Column.new(name, default, '#{column_type}', options)) }
135
+ column_names.each { |name| role.add_column(Property::Column.new(name, default, '#{column_type}', options)) }
112
136
  end
113
137
  EOV
114
138
  end
@@ -117,7 +141,7 @@ module Property
117
141
  # p.serialize 'pet', Dog
118
142
  def serialize(name, klass, options = {})
119
143
  Property.validate_property_class(klass)
120
- behavior.add_column(Property::Column.new(name, nil, klass, options))
144
+ role.add_column(Property::Column.new(name, nil, klass, options))
121
145
  end
122
146
 
123
147
  # This is used to create complex indices with the following syntax:
@@ -132,13 +156,13 @@ module Property
132
156
  # The first argument is the type (used to locate the table where the data will be stored) and the block
133
157
  # will be yielded with the record and should return a hash of key => value pairs.
134
158
  def index(type, &block)
135
- behavior.add_index(type, block)
159
+ role.add_index(type, block)
136
160
  end
137
161
 
138
162
  alias actions class_eval
139
163
  end
140
164
  end
141
- accessor_module.behavior = self
165
+ accessor_module.role = self
142
166
  accessor_module
143
167
  end
144
168
 
@@ -209,10 +233,18 @@ module Property
209
233
  accessor_module.class_eval(method_definition, __FILE__, __LINE__)
210
234
  end
211
235
 
212
- def verify_not_defined_in_schemas_using_this_behavior(name)
236
+ def verify_not_defined_in_schemas_using_this_role(name)
213
237
  @included_in_schemas.each do |schema|
214
238
  if schema.columns[name]
215
- raise TypeError.new("Property '#{name}' is already defined in #{schema.name}.")
239
+ raise RedefinedPropertyError.new("Property '#{name}' is already defined in #{schema.name}.")
240
+ end
241
+ end
242
+ end
243
+
244
+ def verify_method_not_defined_in_classes_using_this_role(name)
245
+ @included_in_schemas.each do |schema|
246
+ if schema.binding.superclass.method_defined?(name)
247
+ raise RedefinedMethodError.new("Method '#{name}' is already defined in #{schema.binding.superclass} or ancestors.")
216
248
  end
217
249
  end
218
250
  end
@@ -4,49 +4,73 @@ module Property
4
4
  # to validate content and type_cast during write operations.
5
5
  #
6
6
  # The properties are not directly defined in the schema. They are stored in a
7
- # Behavior instance which checks that the database is in sync with the properties
7
+ # Role instance which checks that the database is in sync with the properties
8
8
  # defined.
9
9
  class Schema
10
- attr_reader :behaviors, :behavior, :binding
10
+ attr_reader :roles, :role, :binding
11
11
 
12
12
  # Create a new Schema. If a class_name is provided, the schema automatically
13
- # creates a default Behavior to store definitions.
13
+ # creates a default Role to store definitions.
14
14
  def initialize(class_name, binding)
15
15
  @binding = binding
16
- @behaviors = []
16
+ @roles = []
17
17
  if class_name
18
- @behavior = Behavior.new(class_name)
19
- include_behavior @behavior
20
- @behaviors << @behavior
18
+ @role = Role.new(class_name)
19
+ include_role @role
20
+ @roles << @role
21
21
  end
22
22
  end
23
23
 
24
24
  # Return an identifier for the schema to help locate property redefinition errors.
25
25
  def name
26
- @behavior ? @behavior.name : @binding.to_s
26
+ @role ? @role.name : @binding.to_s
27
+ end
28
+
29
+ # Return true if the current schema has all the roles of the given object, class or role.
30
+ def has_role?(thing)
31
+ roles = self.roles.flatten
32
+ test_roles = thing.class < RoleModule ? [thing] : thing.schema.roles.flatten
33
+ test_roles.each do |role|
34
+ return false unless roles.include?(role)
35
+ end
36
+ true
27
37
  end
28
38
 
29
39
  # If the parameter is a class, the schema will inherit the property definitions
30
- # from the class. If the parameter is a Behavior, the properties from that
31
- # behavior will be included. Any new columns added to a behavior or any new
32
- # behaviors included in a class will be dynamically added to the sub-classes (just like
40
+ # from the class. If the parameter is a Role, the properties from that
41
+ # role will be included. Any new columns added to a role or any new
42
+ # roles included in a class will be dynamically added to the sub-classes (just like
33
43
  # Ruby class inheritance, module inclusion works).
34
44
  # If you ...
35
- def behave_like(thing)
45
+ def has_role(thing)
36
46
  if thing.kind_of?(Class)
37
47
  if thing.respond_to?(:schema) && thing.schema.kind_of?(Schema)
38
- thing.schema.behaviors.flatten.each do |behavior|
39
- include_behavior behavior
48
+ schema_class = thing.schema.binding
49
+ if @binding.ancestors.include?(schema_class)
50
+ check_super_methods = false
51
+ else
52
+ check_super_methods = true
40
53
  end
41
- self.behaviors << thing.schema.behaviors
54
+ thing.schema.roles.flatten.each do |role|
55
+ include_role role, check_super_methods
56
+ end
57
+ self.roles << thing.schema.roles
42
58
  else
43
- raise TypeError.new("expected Behavior or class with schema, found #{thing}")
59
+ raise TypeError.new("expected Role or class with schema, found #{thing}")
44
60
  end
45
- elsif thing.kind_of?(Behavior)
46
- include_behavior thing
47
- self.behaviors << thing
61
+ elsif thing.kind_of?(RoleModule)
62
+ include_role thing
63
+ self.roles << thing
48
64
  else
49
- raise TypeError.new("expected Behavior or class with schema, found #{thing.class}")
65
+ raise TypeError.new("expected Role or class with schema, found #{thing.class}")
66
+ end
67
+ end
68
+
69
+ # Return the list of active roles. The active roles are all the Roles included
70
+ # in the current object for which properties have been defined (not blank).
71
+ def used_roles_in(object)
72
+ roles.flatten.uniq.reject do |role|
73
+ !role.used_in(object)
50
74
  end
51
75
  end
52
76
 
@@ -58,16 +82,16 @@ module Property
58
82
  # Return true if the schema has a property with the given name.
59
83
  def has_column?(name)
60
84
  name = name.to_s
61
- [@behaviors].flatten.each do |behavior|
62
- return true if behavior.has_column?(name)
85
+ [@roles].flatten.each do |role|
86
+ return true if role.has_column?(name)
63
87
  end
64
88
  false
65
89
  end
66
90
 
67
- # Return column definitions from all included behaviors.
91
+ # Return column definitions from all included roles.
68
92
  def columns
69
93
  columns = {}
70
- @behaviors.flatten.uniq.each do |b|
94
+ @roles.flatten.uniq.each do |b|
71
95
  columns.merge!(b.columns)
72
96
  end
73
97
  columns
@@ -76,7 +100,7 @@ module Property
76
100
  # Return a hash with indexed types as keys and index definitions as values.
77
101
  def index_groups
78
102
  index_groups = {}
79
- @behaviors.flatten.uniq.each do |b|
103
+ @roles.flatten.uniq.each do |b|
80
104
  b.indices.each do |list|
81
105
  (index_groups[list.first] ||= []) << list[1..-1]
82
106
  end
@@ -85,15 +109,35 @@ module Property
85
109
  end
86
110
 
87
111
  private
88
- def include_behavior(behavior)
89
- return if behaviors.include?(behavior)
90
- columns = self.columns
91
- common_keys = behavior.column_names & columns.keys
112
+ def include_role(role, check_methods = true)
113
+ return if roles.flatten.include?(role)
114
+
115
+ stored_column_names = role.column_names
116
+
117
+ check_duplicate_property_definitions(role, stored_column_names)
118
+ check_duplicate_method_definitions(role, stored_column_names) if check_methods
119
+
120
+ role.included_in(self)
121
+ @binding.send(:include, role.accessor_module)
122
+ end
123
+
124
+ def check_duplicate_property_definitions(role, keys)
125
+ common_keys = keys & self.columns.keys
92
126
  if !common_keys.empty?
93
- raise TypeError.new("Cannot include behavior #{behavior.name}. Duplicate definitions: #{common_keys.join(', ')}")
127
+ raise RedefinedPropertyError.new("Cannot include role '#{role.name}' in '#{name}'. Duplicate definitions: #{common_keys.join(', ')}")
94
128
  end
95
- behavior.included(self)
96
- @binding.send(:include, behavior.accessor_module)
97
129
  end
130
+
131
+ def check_duplicate_method_definitions(role, keys)
132
+ common_keys = []
133
+ keys.each do |k|
134
+ common_keys << k if @binding.superclass.method_defined?(k)
135
+ end
136
+
137
+ if !common_keys.empty?
138
+ raise RedefinedMethodError.new("Cannot include role '#{role.name}' in '#{@binding}'. Would hide methods in superclass: #{common_keys.join(', ')}")
139
+ end
140
+ end
141
+
98
142
  end
99
143
  end
@@ -0,0 +1,30 @@
1
+ module Property
2
+ # This module should be inserted in an ActiveRecord class that stores a
3
+ # single property definition in the database and is used with StoredRole.
4
+ module StoredColumn
5
+ def self.included(base)
6
+ base.before_validation :set_index
7
+ end
8
+
9
+ # Default values not currently supported.
10
+ def default
11
+ nil
12
+ end
13
+
14
+ # No supported options yet.
15
+ def options
16
+ {:index => (index.blank? ? nil : index)}
17
+ end
18
+
19
+ private
20
+ def set_index
21
+ if index == true
22
+ self.index = ptype.to_s
23
+ elsif index.blank?
24
+ self.index = nil
25
+ else
26
+ self.index = self.index.to_s
27
+ end
28
+ end
29
+ end
30
+ end