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.
- data/.gitignore +1 -0
- data/History.txt +19 -3
- data/README.rdoc +51 -6
- data/lib/property.rb +4 -2
- data/lib/property/attribute.rb +14 -7
- data/lib/property/base.rb +16 -0
- data/lib/property/column.rb +13 -1
- data/lib/property/declaration.rb +48 -26
- data/lib/property/error.rb +4 -0
- data/lib/property/index.rb +104 -29
- data/lib/property/properties.rb +7 -1
- data/lib/property/redefined_method_error.rb +8 -0
- data/lib/property/redefined_property_error.rb +8 -0
- data/lib/property/role.rb +30 -0
- data/lib/property/{behavior.rb → role_module.rb} +67 -35
- data/lib/property/schema.rb +76 -32
- data/lib/property/stored_column.rb +30 -0
- data/lib/property/stored_role.rb +101 -0
- data/property.gemspec +20 -5
- data/test/fixtures.rb +39 -1
- data/test/shoulda_macros/index.rb +95 -0
- data/test/shoulda_macros/role.rb +237 -0
- data/test/shoulda_macros/serialization.rb +1 -1
- data/test/test_helper.rb +2 -0
- data/test/unit/property/base_test.rb +80 -0
- data/test/unit/property/declaration_test.rb +89 -30
- data/test/unit/property/index_foreign_test.rb +2 -2
- data/test/unit/property/index_simple_test.rb +17 -4
- data/test/unit/property/role_test.rb +78 -0
- data/test/unit/property/stored_role_test.rb +84 -0
- data/test/unit/property/validation_test.rb +26 -4
- metadata +22 -7
- data/test/unit/property/behavior_test.rb +0 -146
data/lib/property/properties.rb
CHANGED
@@ -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
|
-
|
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,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
|
4
|
-
# object with the
|
5
|
-
|
6
|
-
attr_accessor :
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
end
|
15
|
-
|
16
|
-
#
|
17
|
-
|
18
|
-
|
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
|
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.
|
47
|
+
[c.index, c.name]
|
38
48
|
end
|
39
49
|
end + @group_indices
|
40
50
|
end
|
41
51
|
|
42
|
-
# Return true if the
|
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
|
62
|
+
# Use this method to declare properties into a Role.
|
53
63
|
# Example:
|
54
|
-
# @
|
64
|
+
# @role.property.string 'phone', :default => ''
|
55
65
|
#
|
56
66
|
# You can also use a block:
|
57
|
-
# @
|
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
|
69
|
-
def
|
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
|
88
|
+
raise RedefinedPropertyError.new("Property '#{name}' is already defined.")
|
79
89
|
else
|
80
|
-
|
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 :
|
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|
|
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
|
-
|
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
|
-
|
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.
|
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
|
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
|
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
|
data/lib/property/schema.rb
CHANGED
@@ -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
|
-
#
|
7
|
+
# Role instance which checks that the database is in sync with the properties
|
8
8
|
# defined.
|
9
9
|
class Schema
|
10
|
-
attr_reader :
|
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
|
13
|
+
# creates a default Role to store definitions.
|
14
14
|
def initialize(class_name, binding)
|
15
15
|
@binding = binding
|
16
|
-
@
|
16
|
+
@roles = []
|
17
17
|
if class_name
|
18
|
-
@
|
19
|
-
|
20
|
-
@
|
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
|
-
@
|
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
|
31
|
-
#
|
32
|
-
#
|
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
|
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.
|
39
|
-
|
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
|
-
|
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
|
59
|
+
raise TypeError.new("expected Role or class with schema, found #{thing}")
|
44
60
|
end
|
45
|
-
elsif thing.kind_of?(
|
46
|
-
|
47
|
-
self.
|
61
|
+
elsif thing.kind_of?(RoleModule)
|
62
|
+
include_role thing
|
63
|
+
self.roles << thing
|
48
64
|
else
|
49
|
-
raise TypeError.new("expected
|
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
|
-
[@
|
62
|
-
return true if
|
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
|
91
|
+
# Return column definitions from all included roles.
|
68
92
|
def columns
|
69
93
|
columns = {}
|
70
|
-
@
|
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
|
-
@
|
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
|
89
|
-
return if
|
90
|
-
|
91
|
-
|
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
|
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
|