property 0.7.0 → 0.8.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 +7 -0
- data/README.rdoc +22 -0
- data/lib/property.rb +14 -1
- data/lib/property/behavior.rb +167 -0
- data/lib/property/column.rb +21 -1
- data/lib/property/declaration.rb +42 -185
- data/lib/property/properties.rb +3 -13
- data/lib/property/schema.rb +79 -0
- data/lib/property/serialization/json.rb +5 -5
- data/lib/property/serialization/marshal.rb +0 -10
- data/lib/property/serialization/yaml.rb +0 -11
- data/property.gemspec +6 -2
- data/test/fixtures.rb +19 -0
- data/test/shoulda_macros/serialization.rb +7 -12
- data/test/test_helper.rb +1 -0
- data/test/unit/property/attribute_test.rb +26 -2
- data/test/unit/property/behavior_test.rb +119 -0
- data/test/unit/property/declaration_test.rb +63 -23
- data/test/unit/property/validation_test.rb +7 -0
- data/test/unit/serialization/json_test.rb +25 -0
- metadata +6 -2
data/History.txt
CHANGED
data/README.rdoc
CHANGED
@@ -46,3 +46,25 @@ And set them with:
|
|
46
46
|
@contact.prop['name'] = 'Gandhi'
|
47
47
|
@contact.name = 'Gandhi'
|
48
48
|
|
49
|
+
== Behaviors
|
50
|
+
|
51
|
+
Properties would not be really fun if you could not add new properties to your instances depending
|
52
|
+
on some flags. First define the behaviors:
|
53
|
+
|
54
|
+
@a_picture = Property::Behavior.new do |p|
|
55
|
+
p.integer :width, :default => :get_width
|
56
|
+
p.integer :height, :default => :get_height
|
57
|
+
p.string 'camera'
|
58
|
+
p.string 'location'
|
59
|
+
end
|
60
|
+
|
61
|
+
And then, either when creating new pictures or updating them, you need to include the behavior:
|
62
|
+
|
63
|
+
@model.behave_like @a_picture
|
64
|
+
|
65
|
+
The model now has the picture's properties defined, with accessors like @model.camera and default
|
66
|
+
values will be fetched on save.
|
67
|
+
|
68
|
+
Note that you do not need to include a behavior just to read the data as long as you use the 'prop'
|
69
|
+
accessor.
|
70
|
+
|
data/lib/property.rb
CHANGED
@@ -2,16 +2,29 @@ require 'property/attribute'
|
|
2
2
|
require 'property/dirty'
|
3
3
|
require 'property/properties'
|
4
4
|
require 'property/column'
|
5
|
+
require 'property/behavior'
|
6
|
+
require 'property/schema'
|
5
7
|
require 'property/declaration'
|
6
8
|
require 'property/serialization/json'
|
7
9
|
require 'property/core_ext/time'
|
8
10
|
|
9
11
|
module Property
|
10
|
-
VERSION = '0.
|
12
|
+
VERSION = '0.8.0'
|
11
13
|
|
12
14
|
def self.included(base)
|
13
15
|
base.class_eval do
|
14
16
|
include ::Property::Attribute
|
15
17
|
end
|
16
18
|
end
|
19
|
+
|
20
|
+
def self.validators
|
21
|
+
@@validators ||= []
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.validate_property_class(type)
|
25
|
+
@@validators.each do |validator|
|
26
|
+
return false unless validator.validate(type)
|
27
|
+
end
|
28
|
+
true
|
29
|
+
end
|
17
30
|
end
|
@@ -0,0 +1,167 @@
|
|
1
|
+
module Property
|
2
|
+
# 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)
|
9
|
+
obj = super
|
10
|
+
if block_given?
|
11
|
+
yield obj
|
12
|
+
end
|
13
|
+
obj
|
14
|
+
end
|
15
|
+
|
16
|
+
# Initialize a new behavior with the given name
|
17
|
+
def initialize(name)
|
18
|
+
@name = name
|
19
|
+
@included_in_schemas = []
|
20
|
+
@accessor_module = Module.new
|
21
|
+
end
|
22
|
+
|
23
|
+
# List all property definitiosn for the current behavior
|
24
|
+
def columns
|
25
|
+
@columns ||= {}
|
26
|
+
end
|
27
|
+
|
28
|
+
# Return the list of column names.
|
29
|
+
def column_names
|
30
|
+
columns.keys
|
31
|
+
end
|
32
|
+
|
33
|
+
# Use this method to declare properties into a Behavior.
|
34
|
+
# Example:
|
35
|
+
# @behavior.property.string 'phone', :default => ''
|
36
|
+
#
|
37
|
+
# You can also use a block:
|
38
|
+
# @behavior.property do |p|
|
39
|
+
# p.string 'phone', 'name', :default => ''
|
40
|
+
# end
|
41
|
+
def property
|
42
|
+
if block_given?
|
43
|
+
yield self
|
44
|
+
end
|
45
|
+
self
|
46
|
+
end
|
47
|
+
|
48
|
+
# def string(*args)
|
49
|
+
# options = args.extract_options!
|
50
|
+
# column_names = args
|
51
|
+
# default = options.delete(:default)
|
52
|
+
# column_names.each { |name| column(name, default, 'string', options) }
|
53
|
+
# end
|
54
|
+
%w( string text integer float decimal datetime timestamp time date binary boolean ).each do |column_type|
|
55
|
+
class_eval <<-EOV
|
56
|
+
def #{column_type}(*args)
|
57
|
+
options = args.extract_options!
|
58
|
+
column_names = args
|
59
|
+
default = options.delete(:default)
|
60
|
+
column_names.each { |name| add_column(Property::Column.new(name, default, '#{column_type}', options)) }
|
61
|
+
end
|
62
|
+
EOV
|
63
|
+
end
|
64
|
+
|
65
|
+
# This is used to serialize a non-native DB type. Use:
|
66
|
+
# p.serialize 'pet', Dog
|
67
|
+
def serialize(name, klass, options = {})
|
68
|
+
Property.validate_property_class(klass)
|
69
|
+
add_column(Property::Column.new(name, nil, klass, options))
|
70
|
+
end
|
71
|
+
|
72
|
+
# @internal
|
73
|
+
# This is called when the behavior is included in a schema
|
74
|
+
def included(schema)
|
75
|
+
@included_in_schemas << schema
|
76
|
+
end
|
77
|
+
|
78
|
+
private
|
79
|
+
|
80
|
+
def define_property_methods(column)
|
81
|
+
name = column.name
|
82
|
+
|
83
|
+
#if create_time_zone_conversion_attribute?(name, column)
|
84
|
+
# define_read_property_method_for_time_zone_conversion(name)
|
85
|
+
#else
|
86
|
+
define_read_property_method(name.to_sym, name, column)
|
87
|
+
#end
|
88
|
+
|
89
|
+
#if create_time_zone_conversion_attribute?(name, column)
|
90
|
+
# define_write_property_method_for_time_zone_conversion(name)
|
91
|
+
#else
|
92
|
+
define_write_property_method(name.to_sym)
|
93
|
+
#end
|
94
|
+
|
95
|
+
define_question_property_method(name)
|
96
|
+
end
|
97
|
+
|
98
|
+
# Define a property reader method. Cope with nil column.
|
99
|
+
def define_read_property_method(symbol, attr_name, column)
|
100
|
+
# Unlike rails, we do not cast on read
|
101
|
+
evaluate_attribute_property_method attr_name, "def #{symbol}; prop['#{attr_name}']; end"
|
102
|
+
end
|
103
|
+
|
104
|
+
# Defined for all +datetime+ and +timestamp+ attributes when +time_zone_aware_attributes+ are enabled.
|
105
|
+
# This enhanced read method automatically converts the UTC time stored in the database to the time zone stored in Time.zone.
|
106
|
+
# def define_read_property_method_for_time_zone_conversion(attr_name)
|
107
|
+
# method_body = <<-EOV
|
108
|
+
# def #{attr_name}(reload = false)
|
109
|
+
# cached = @attributes_cache['#{attr_name}']
|
110
|
+
# return cached if cached && !reload
|
111
|
+
# time = properties['#{attr_name}']
|
112
|
+
# @attributes_cache['#{attr_name}'] = time.acts_like?(:time) ? time.in_time_zone : time
|
113
|
+
# end
|
114
|
+
# EOV
|
115
|
+
# evaluate_attribute_property_method attr_name, method_body
|
116
|
+
# end
|
117
|
+
|
118
|
+
# Defines a predicate method <tt>attr_name?</tt>.
|
119
|
+
def define_question_property_method(attr_name)
|
120
|
+
evaluate_attribute_property_method attr_name, "def #{attr_name}?; prop['#{attr_name}']; end", "#{attr_name}?"
|
121
|
+
end
|
122
|
+
|
123
|
+
def define_write_property_method(attr_name)
|
124
|
+
evaluate_attribute_property_method attr_name, "def #{attr_name}=(new_value);prop['#{attr_name}'] = new_value; end", "#{attr_name}="
|
125
|
+
end
|
126
|
+
|
127
|
+
# Defined for all +datetime+ and +timestamp+ attributes when +time_zone_aware_attributes+ are enabled.
|
128
|
+
# This enhanced write method will automatically convert the time passed to it to the zone stored in Time.zone.
|
129
|
+
# def define_write_property_method_for_time_zone_conversion(attr_name)
|
130
|
+
# method_body = <<-EOV
|
131
|
+
# def #{attr_name}=(time)
|
132
|
+
# unless time.acts_like?(:time)
|
133
|
+
# time = time.is_a?(String) ? Time.zone.parse(time) : time.to_time rescue time
|
134
|
+
# end
|
135
|
+
# time = time.in_time_zone rescue nil if time
|
136
|
+
# prop['#{attr_name}'] = time
|
137
|
+
# end
|
138
|
+
# EOV
|
139
|
+
# evaluate_attribute_property_method attr_name, method_body, "#{attr_name}="
|
140
|
+
# end
|
141
|
+
|
142
|
+
# Evaluate the definition for an attribute related method
|
143
|
+
def evaluate_attribute_property_method(attr_name, method_definition, method_name=attr_name)
|
144
|
+
accessor_module.class_eval(method_definition, __FILE__, __LINE__)
|
145
|
+
end
|
146
|
+
|
147
|
+
def add_column(column)
|
148
|
+
name = column.name
|
149
|
+
|
150
|
+
if columns[name]
|
151
|
+
raise TypeError.new("Property '#{name}' is already defined.")
|
152
|
+
else
|
153
|
+
verify_not_defined_in_schemas_using_this_behavior(name)
|
154
|
+
define_property_methods(column) if column.should_create_accessors?
|
155
|
+
columns[column.name] = column
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
def verify_not_defined_in_schemas_using_this_behavior(name)
|
160
|
+
@included_in_schemas.each do |schema|
|
161
|
+
if schema.columns[name]
|
162
|
+
raise TypeError.new("Property '#{name}' is already defined in #{schema.name}.")
|
163
|
+
end
|
164
|
+
end
|
165
|
+
end
|
166
|
+
end
|
167
|
+
end
|
data/lib/property/column.rb
CHANGED
@@ -11,11 +11,16 @@ module Property
|
|
11
11
|
def initialize(name, default, type, options={})
|
12
12
|
name = name.to_s
|
13
13
|
extract_property_options(options)
|
14
|
+
if type.kind_of?(Class)
|
15
|
+
@klass = type
|
16
|
+
end
|
14
17
|
super(name, default, type, options)
|
15
18
|
end
|
16
19
|
|
17
20
|
def validate(value, errors)
|
18
|
-
|
21
|
+
if @klass && !value.kind_of?(@klass)
|
22
|
+
errors.add(name, "cannot cast #{value.class} to #{@klass}")
|
23
|
+
end
|
19
24
|
end
|
20
25
|
|
21
26
|
def should_create_accessors?
|
@@ -36,6 +41,21 @@ module Property
|
|
36
41
|
end
|
37
42
|
end
|
38
43
|
|
44
|
+
def klass
|
45
|
+
@klass || super
|
46
|
+
end
|
47
|
+
|
48
|
+
def type_cast(value)
|
49
|
+
if type == :string
|
50
|
+
value = value.to_s
|
51
|
+
value.blank? ? nil : value
|
52
|
+
elsif @klass
|
53
|
+
value
|
54
|
+
else
|
55
|
+
super
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
39
59
|
private
|
40
60
|
def extract_property_options(options)
|
41
61
|
@indexed = options.delete(:indexed)
|
data/lib/property/declaration.rb
CHANGED
@@ -10,90 +10,34 @@ module Property
|
|
10
10
|
include InstanceMethods
|
11
11
|
|
12
12
|
class << self
|
13
|
-
attr_accessor :
|
14
|
-
attr_accessor :property_definition_proxy
|
15
|
-
end
|
16
|
-
|
17
|
-
validate :properties_validation, :if => :properties
|
18
|
-
end
|
19
|
-
end
|
20
|
-
|
21
|
-
class DefinitionProxy
|
22
|
-
def initialize(klass)
|
23
|
-
@klass = klass
|
24
|
-
end
|
25
|
-
|
26
|
-
def column(name, default, type, options)
|
27
|
-
if columns[name.to_s]
|
28
|
-
raise TypeError.new("Property '#{name}' is already defined.")
|
29
|
-
else
|
30
|
-
add_column(Property::Column.new(name, default, type, options))
|
31
|
-
end
|
32
|
-
end
|
13
|
+
attr_accessor :schema
|
33
14
|
|
34
|
-
|
35
|
-
|
36
|
-
@klass.define_property_methods(column) if column.should_create_accessors?
|
37
|
-
end
|
38
|
-
|
39
|
-
# If someday we find the need to insert other native classes directly in the DB, we
|
40
|
-
# could use this:
|
41
|
-
# p.serialize MyClass, xxx, xxx
|
42
|
-
# def serialize(klass, name, options={})
|
43
|
-
# if @klass.super_property_columns[name.to_s]
|
44
|
-
# raise TypeError.new("Property '#{name}' is already defined in a superclass.")
|
45
|
-
# elsif !@klass.validate_property_class(type)
|
46
|
-
# raise TypeError.new("Custom type '#{type}' cannot be serialized.")
|
47
|
-
# else
|
48
|
-
# # Find a way to insert the type (maybe with 'serialize'...)
|
49
|
-
# # (@klass.own_property_columns ||= {})[name] = Property::Column.new(name, type, options)
|
50
|
-
# end
|
51
|
-
# end
|
52
|
-
|
53
|
-
# def string(*args)
|
54
|
-
# options = args.extract_options!
|
55
|
-
# column_names = args
|
56
|
-
# default = options.delete(:default)
|
57
|
-
# column_names.each { |name| column(name, default, 'string', options) }
|
58
|
-
# end
|
59
|
-
%w( string text integer float decimal datetime timestamp time date binary boolean ).each do |column_type|
|
60
|
-
class_eval <<-EOV
|
61
|
-
def #{column_type}(*args)
|
62
|
-
options = args.extract_options!
|
63
|
-
column_names = args
|
64
|
-
default = options.delete(:default)
|
65
|
-
column_names.each { |name| column(name, default, '#{column_type}', options) }
|
15
|
+
def schema
|
16
|
+
@schema ||= make_schema
|
66
17
|
end
|
67
|
-
EOV
|
68
|
-
end
|
69
18
|
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
19
|
+
private
|
20
|
+
def make_schema
|
21
|
+
schema = Property::Schema.new(self.to_s, self)
|
22
|
+
if superclass.respond_to?(:schema)
|
23
|
+
schema.behave_like superclass
|
24
|
+
end
|
25
|
+
schema
|
26
|
+
end
|
77
27
|
end
|
78
28
|
|
79
|
-
|
80
|
-
|
81
|
-
class InstanceDefinitionProxy < DefinitionProxy
|
82
|
-
def initialize(instance)
|
83
|
-
@properties = instance.prop
|
84
|
-
end
|
85
|
-
|
86
|
-
def add_column(column)
|
87
|
-
columns[column.name] = column
|
88
|
-
end
|
89
|
-
|
90
|
-
def columns
|
91
|
-
@properties.columns
|
29
|
+
validate :properties_validation, :if => :properties
|
92
30
|
end
|
93
31
|
end
|
94
32
|
|
95
33
|
module ClassMethods
|
96
34
|
|
35
|
+
# Include a new set of property definitions (Behavior) into the current class schema.
|
36
|
+
# You can also provide a class to simulate multiple inheritance.
|
37
|
+
def behave_like(behavior)
|
38
|
+
schema.behave_like behavior
|
39
|
+
end
|
40
|
+
|
97
41
|
# Use this class method to declare properties that will be used in your models.
|
98
42
|
# Example:
|
99
43
|
# property.string 'phone', :default => ''
|
@@ -103,131 +47,44 @@ module Property
|
|
103
47
|
# p.string 'phone', 'name', :default => ''
|
104
48
|
# end
|
105
49
|
def property
|
106
|
-
|
107
|
-
if block_given?
|
108
|
-
yield proxy
|
109
|
-
end
|
110
|
-
proxy
|
111
|
-
end
|
112
|
-
|
113
|
-
# @internal.
|
114
|
-
# If you need the list of columns (including instance columns), you should use
|
115
|
-
# properties.columns
|
116
|
-
#
|
117
|
-
# Return the list of all properties defined for the current class, including the properties
|
118
|
-
# defined in the parent class.
|
119
|
-
def property_columns
|
120
|
-
super_property_columns.merge(self.own_property_columns || {})
|
121
|
-
end
|
122
|
-
|
123
|
-
def property_column_names
|
124
|
-
property_columns.keys
|
125
|
-
end
|
126
|
-
|
127
|
-
def super_property_columns
|
128
|
-
if superclass.respond_to?(:property_columns)
|
129
|
-
superclass.property_columns
|
130
|
-
else
|
131
|
-
{}
|
132
|
-
end
|
133
|
-
end
|
134
|
-
|
135
|
-
def define_property_methods(column)
|
136
|
-
name = column.name
|
137
|
-
unless instance_method_already_implemented?(name)
|
138
|
-
if create_time_zone_conversion_attribute?(name, column)
|
139
|
-
define_read_property_method_for_time_zone_conversion(name)
|
140
|
-
else
|
141
|
-
define_read_property_method(name.to_sym, name, column)
|
142
|
-
end
|
143
|
-
end
|
50
|
+
setter = schema.behavior
|
144
51
|
|
145
|
-
|
146
|
-
|
147
|
-
define_write_property_method_for_time_zone_conversion(name)
|
148
|
-
else
|
149
|
-
define_write_property_method(name.to_sym)
|
150
|
-
end
|
52
|
+
if block_given?
|
53
|
+
yield setter
|
151
54
|
end
|
152
55
|
|
153
|
-
|
154
|
-
define_question_property_method(name)
|
155
|
-
end
|
56
|
+
setter
|
156
57
|
end
|
157
|
-
|
158
|
-
private
|
159
|
-
# Define a property reader method. Cope with nil column.
|
160
|
-
def define_read_property_method(symbol, attr_name, column)
|
161
|
-
# Unlike rails, we do not cast on read
|
162
|
-
evaluate_attribute_property_method attr_name, "def #{symbol}; prop['#{attr_name}']; end"
|
163
|
-
end
|
164
|
-
|
165
|
-
# Defined for all +datetime+ and +timestamp+ attributes when +time_zone_aware_attributes+ are enabled.
|
166
|
-
# This enhanced read method automatically converts the UTC time stored in the database to the time zone stored in Time.zone.
|
167
|
-
def define_read_property_method_for_time_zone_conversion(attr_name)
|
168
|
-
method_body = <<-EOV
|
169
|
-
def #{attr_name}(reload = false)
|
170
|
-
cached = @attributes_cache['#{attr_name}']
|
171
|
-
return cached if cached && !reload
|
172
|
-
time = properties['#{attr_name}']
|
173
|
-
@attributes_cache['#{attr_name}'] = time.acts_like?(:time) ? time.in_time_zone : time
|
174
|
-
end
|
175
|
-
EOV
|
176
|
-
evaluate_attribute_property_method attr_name, method_body
|
177
|
-
end
|
178
|
-
|
179
|
-
# Defines a predicate method <tt>attr_name?</tt>.
|
180
|
-
def define_question_property_method(attr_name)
|
181
|
-
evaluate_attribute_property_method attr_name, "def #{attr_name}?; prop['#{attr_name}']; end", "#{attr_name}?"
|
182
|
-
end
|
183
|
-
|
184
|
-
def define_write_property_method(attr_name)
|
185
|
-
evaluate_attribute_property_method attr_name, "def #{attr_name}=(new_value);prop['#{attr_name}'] = new_value; end", "#{attr_name}="
|
186
|
-
end
|
187
|
-
|
188
|
-
# Defined for all +datetime+ and +timestamp+ attributes when +time_zone_aware_attributes+ are enabled.
|
189
|
-
# This enhanced write method will automatically convert the time passed to it to the zone stored in Time.zone.
|
190
|
-
def define_write_property_method_for_time_zone_conversion(attr_name)
|
191
|
-
method_body = <<-EOV
|
192
|
-
def #{attr_name}=(time)
|
193
|
-
unless time.acts_like?(:time)
|
194
|
-
time = time.is_a?(String) ? Time.zone.parse(time) : time.to_time rescue time
|
195
|
-
end
|
196
|
-
time = time.in_time_zone rescue nil if time
|
197
|
-
prop['#{attr_name}'] = time
|
198
|
-
end
|
199
|
-
EOV
|
200
|
-
evaluate_attribute_property_method attr_name, method_body, "#{attr_name}="
|
201
|
-
end
|
202
|
-
|
203
|
-
# Evaluate the definition for an attribute related method
|
204
|
-
def evaluate_attribute_property_method(attr_name, method_definition, method_name=attr_name)
|
205
|
-
class_eval(method_definition, __FILE__, __LINE__)
|
206
|
-
end
|
207
58
|
end # ClassMethods
|
208
59
|
|
209
60
|
module InstanceMethods
|
61
|
+
# Instance's schema (can be different from the instance's class schema if behaviors have been
|
62
|
+
# added to the instance.
|
63
|
+
def schema
|
64
|
+
@own_schema || self.class.schema
|
65
|
+
end
|
210
66
|
|
211
|
-
#
|
212
|
-
#
|
213
|
-
|
214
|
-
|
215
|
-
# You can also use a block:
|
216
|
-
# @obj.property do |p|
|
217
|
-
# p.string 'phone', 'name', :default => ''
|
218
|
-
# end
|
219
|
-
def property
|
220
|
-
proxy = @instance_definition_proxy ||= InstanceDefinitionProxy.new(self)
|
221
|
-
if block_given?
|
222
|
-
yield proxy
|
223
|
-
end
|
224
|
-
proxy
|
67
|
+
# Include a new set of property definitions (Behavior) into the current instance's schema.
|
68
|
+
# You can also provide a class to simulate multiple inheritance.
|
69
|
+
def behave_like(behavior)
|
70
|
+
own_schema.behave_like behavior
|
225
71
|
end
|
226
72
|
|
227
73
|
protected
|
228
74
|
def properties_validation
|
229
75
|
properties.validate
|
230
76
|
end
|
77
|
+
|
78
|
+
def own_schema
|
79
|
+
@own_schema ||= make_own_schema
|
80
|
+
end
|
81
|
+
private
|
82
|
+
def make_own_schema
|
83
|
+
this = class << self; self; end
|
84
|
+
schema = Property::Schema.new(nil, this)
|
85
|
+
schema.behave_like self.class
|
86
|
+
schema
|
87
|
+
end
|
231
88
|
end # InsanceMethods
|
232
89
|
end # Declaration
|
233
90
|
end # Property
|
data/lib/property/properties.rb
CHANGED
@@ -20,7 +20,7 @@ module Property
|
|
20
20
|
delete(key)
|
21
21
|
end
|
22
22
|
else
|
23
|
-
super(key, column.type_cast(value
|
23
|
+
super(key, column.type_cast(value))
|
24
24
|
end
|
25
25
|
else
|
26
26
|
super
|
@@ -56,24 +56,14 @@ module Property
|
|
56
56
|
end
|
57
57
|
|
58
58
|
keys_to_validate.each do |key|
|
59
|
-
|
60
|
-
column = columns[key]
|
61
|
-
if value.blank?
|
62
|
-
if column.has_default?
|
63
|
-
self[key] = column.default_for(@owner)
|
64
|
-
else
|
65
|
-
delete(key)
|
66
|
-
end
|
67
|
-
else
|
68
|
-
columns[key].validate(self[key], errors)
|
69
|
-
end
|
59
|
+
columns[key].validate(self[key], errors)
|
70
60
|
end
|
71
61
|
|
72
62
|
bad_keys.empty?
|
73
63
|
end
|
74
64
|
|
75
65
|
def columns
|
76
|
-
@columns ||= @owner.
|
66
|
+
@columns ||= @owner.schema.columns
|
77
67
|
end
|
78
68
|
end
|
79
69
|
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
|
2
|
+
module Property
|
3
|
+
# This class holds all the properties of a given class or instance. It is used
|
4
|
+
# to validate content and type_cast during write operations.
|
5
|
+
#
|
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
|
8
|
+
# defined.
|
9
|
+
class Schema
|
10
|
+
attr_reader :behaviors, :behavior, :binding
|
11
|
+
|
12
|
+
# Create a new Schema. If a class_name is provided, the schema automatically
|
13
|
+
# creates a default Behavior to store definitions.
|
14
|
+
def initialize(class_name, binding)
|
15
|
+
@binding = binding
|
16
|
+
@behaviors = []
|
17
|
+
if class_name
|
18
|
+
@behavior = Behavior.new(class_name)
|
19
|
+
include_behavior @behavior
|
20
|
+
@behaviors << @behavior
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
# Return an identifier for the schema to help locate property redefinition errors.
|
25
|
+
def name
|
26
|
+
@behavior ? @behavior.name : @binding.to_s
|
27
|
+
end
|
28
|
+
|
29
|
+
# 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
|
33
|
+
# Ruby class inheritance, module inclusion works).
|
34
|
+
# If you ...
|
35
|
+
def behave_like(thing)
|
36
|
+
if thing.kind_of?(Class)
|
37
|
+
if thing.respond_to?(:schema) && thing.schema.kind_of?(Schema)
|
38
|
+
thing.schema.behaviors.flatten.each do |behavior|
|
39
|
+
include_behavior behavior
|
40
|
+
end
|
41
|
+
self.behaviors << thing.schema.behaviors
|
42
|
+
else
|
43
|
+
raise TypeError.new("expected Behavior or class with schema, found #{thing}")
|
44
|
+
end
|
45
|
+
elsif thing.kind_of?(Behavior)
|
46
|
+
include_behavior thing
|
47
|
+
self.behaviors << thing
|
48
|
+
else
|
49
|
+
raise TypeError.new("expected Behavior or class with schema, found #{thing.class}")
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
# Return the list of column names.
|
54
|
+
def column_names
|
55
|
+
columns.keys
|
56
|
+
end
|
57
|
+
|
58
|
+
# Return column definitions from all included behaviors.
|
59
|
+
def columns
|
60
|
+
columns = {}
|
61
|
+
@behaviors.flatten.uniq.each do |b|
|
62
|
+
columns.merge!(b.columns)
|
63
|
+
end
|
64
|
+
columns
|
65
|
+
end
|
66
|
+
|
67
|
+
private
|
68
|
+
def include_behavior(behavior)
|
69
|
+
return if behaviors.include?(behavior)
|
70
|
+
columns = self.columns
|
71
|
+
common_keys = behavior.column_names & columns.keys
|
72
|
+
if !common_keys.empty?
|
73
|
+
raise TypeError.new("Cannot include behavior #{behavior.name}. Duplicate definitions: #{common_keys.join(', ')}")
|
74
|
+
end
|
75
|
+
behavior.included(self)
|
76
|
+
@binding.send(:include, behavior.accessor_module)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
@@ -5,11 +5,11 @@ module Property
|
|
5
5
|
# provide 'self.create_json' and 'to_json' methods for the classes you want
|
6
6
|
# to serialize.
|
7
7
|
module JSON
|
8
|
-
module
|
8
|
+
module Validator
|
9
9
|
NATIVE_TYPES = [Hash, Array, Integer, Float, String, TrueClass, FalseClass, NilClass]
|
10
10
|
|
11
|
-
#
|
12
|
-
def
|
11
|
+
# Should raise an exception if the type is not serializable.
|
12
|
+
def self.validate(klass)
|
13
13
|
if NATIVE_TYPES.include?(klass) ||
|
14
14
|
(klass.respond_to?('json_create') && klass.instance_methods.include?('to_json'))
|
15
15
|
true
|
@@ -20,10 +20,10 @@ module Property
|
|
20
20
|
end
|
21
21
|
|
22
22
|
def self.included(base)
|
23
|
-
|
23
|
+
Property.validators << Validator
|
24
24
|
end
|
25
25
|
|
26
|
-
# Encode properties with
|
26
|
+
# Encode properties with JSON
|
27
27
|
def encode_properties(properties)
|
28
28
|
properties.to_json
|
29
29
|
end
|
@@ -7,16 +7,6 @@ module Property
|
|
7
7
|
# * no corruption risk if the version of Marshal changes
|
8
8
|
# * it can be accessed by other languages then ruby
|
9
9
|
module Marshal
|
10
|
-
module ClassMethods
|
11
|
-
# Returns true if the given class can be serialized with Marshal
|
12
|
-
def validate_property_class(klass)
|
13
|
-
true
|
14
|
-
end
|
15
|
-
end
|
16
|
-
|
17
|
-
def self.included(base)
|
18
|
-
base.extend ClassMethods
|
19
|
-
end
|
20
10
|
|
21
11
|
# Encode properties with Marhsal
|
22
12
|
def encode_properties(properties)
|
@@ -3,17 +3,6 @@ module Property
|
|
3
3
|
# Use YAML to encode properties. This method is the slowest of all
|
4
4
|
# and you should use JSON if you haven't got good reasons not to.
|
5
5
|
module YAML
|
6
|
-
module ClassMethods
|
7
|
-
# Returns true if the given class can be serialized with YAML
|
8
|
-
def validate_property_class(klass)
|
9
|
-
true
|
10
|
-
end
|
11
|
-
end
|
12
|
-
|
13
|
-
def self.included(base)
|
14
|
-
base.extend ClassMethods
|
15
|
-
end
|
16
|
-
|
17
6
|
# Encode properties with YAML
|
18
7
|
def encode_properties(properties)
|
19
8
|
::YAML.dump(properties)
|
data/property.gemspec
CHANGED
@@ -5,11 +5,11 @@
|
|
5
5
|
|
6
6
|
Gem::Specification.new do |s|
|
7
7
|
s.name = %q{property}
|
8
|
-
s.version = "0.
|
8
|
+
s.version = "0.8.0"
|
9
9
|
|
10
10
|
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
11
|
s.authors = ["Renaud Kern", "Gaspard Bucher"]
|
12
|
-
s.date = %q{2010-02-
|
12
|
+
s.date = %q{2010-02-12}
|
13
13
|
s.description = %q{Wrap model properties into a single database column and declare properties from within the model.}
|
14
14
|
s.email = %q{gaspard@teti.ch}
|
15
15
|
s.extra_rdoc_files = [
|
@@ -24,11 +24,13 @@ Gem::Specification.new do |s|
|
|
24
24
|
"generators/property/property_generator.rb",
|
25
25
|
"lib/property.rb",
|
26
26
|
"lib/property/attribute.rb",
|
27
|
+
"lib/property/behavior.rb",
|
27
28
|
"lib/property/column.rb",
|
28
29
|
"lib/property/core_ext/time.rb",
|
29
30
|
"lib/property/declaration.rb",
|
30
31
|
"lib/property/dirty.rb",
|
31
32
|
"lib/property/properties.rb",
|
33
|
+
"lib/property/schema.rb",
|
32
34
|
"lib/property/serialization/json.rb",
|
33
35
|
"lib/property/serialization/marshal.rb",
|
34
36
|
"lib/property/serialization/yaml.rb",
|
@@ -37,6 +39,7 @@ Gem::Specification.new do |s|
|
|
37
39
|
"test/shoulda_macros/serialization.rb",
|
38
40
|
"test/test_helper.rb",
|
39
41
|
"test/unit/property/attribute_test.rb",
|
42
|
+
"test/unit/property/behavior_test.rb",
|
40
43
|
"test/unit/property/declaration_test.rb",
|
41
44
|
"test/unit/property/dirty_test.rb",
|
42
45
|
"test/unit/property/validation_test.rb",
|
@@ -55,6 +58,7 @@ Gem::Specification.new do |s|
|
|
55
58
|
"test/shoulda_macros/serialization.rb",
|
56
59
|
"test/test_helper.rb",
|
57
60
|
"test/unit/property/attribute_test.rb",
|
61
|
+
"test/unit/property/behavior_test.rb",
|
58
62
|
"test/unit/property/declaration_test.rb",
|
59
63
|
"test/unit/property/dirty_test.rb",
|
60
64
|
"test/unit/property/validation_test.rb",
|
data/test/fixtures.rb
CHANGED
@@ -24,6 +24,25 @@ class Version < ActiveRecord::Base
|
|
24
24
|
end
|
25
25
|
end
|
26
26
|
|
27
|
+
# To test custom class serialization
|
28
|
+
class Dog
|
29
|
+
attr_accessor :name, :toy
|
30
|
+
def self.json_create(data)
|
31
|
+
Dog.new(data['name'], data['toy'])
|
32
|
+
end
|
33
|
+
def initialize(name, toy)
|
34
|
+
@name, @toy = name, toy
|
35
|
+
end
|
36
|
+
def to_json(*args)
|
37
|
+
{ 'json_class' => self.class.to_s,
|
38
|
+
'name' => @name, 'toy' => @toy
|
39
|
+
}.to_json(*args)
|
40
|
+
end
|
41
|
+
def ==(other)
|
42
|
+
other.kind_of?(Dog) && @name == other.name && @toy == other.toy
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
27
46
|
begin
|
28
47
|
class PropertyMigration < ActiveRecord::Migration
|
29
48
|
def self.down
|
@@ -2,17 +2,6 @@ class Test::Unit::TestCase
|
|
2
2
|
|
3
3
|
def self.should_encode_and_decode_properties
|
4
4
|
klass = self.name.gsub(/Test$/,'').constantize
|
5
|
-
context klass do
|
6
|
-
should 'respond to validate_property_class' do
|
7
|
-
assert klass.respond_to? :validate_property_class
|
8
|
-
end
|
9
|
-
|
10
|
-
[Property::Properties, String, Integer, Float].each do |a_class|
|
11
|
-
should "accept to serialize #{a_class}" do
|
12
|
-
assert klass.validate_property_class(a_class)
|
13
|
-
end
|
14
|
-
end
|
15
|
-
end
|
16
5
|
|
17
6
|
context "Instance of #{klass}" do
|
18
7
|
setup do
|
@@ -29,7 +18,13 @@ class Test::Unit::TestCase
|
|
29
18
|
|
30
19
|
context 'with Properties' do
|
31
20
|
setup do
|
32
|
-
@properties = Property::Properties[
|
21
|
+
@properties = Property::Properties[
|
22
|
+
'string' => 'bar',
|
23
|
+
'serialized' => Dog.new('Pavlov', 'Freud'),
|
24
|
+
'datetime' => Time.utc(2010, 02, 12, 21, 31, 25),
|
25
|
+
'float' => 4.3432,
|
26
|
+
'integer' => 4
|
27
|
+
]
|
33
28
|
end
|
34
29
|
|
35
30
|
should 'encode Properties in string' do
|
data/test/test_helper.rb
CHANGED
@@ -1,6 +1,4 @@
|
|
1
1
|
require 'test_helper'
|
2
|
-
require 'fixtures'
|
3
|
-
require 'benchmark'
|
4
2
|
|
5
3
|
class AttributeTest < Test::Unit::TestCase
|
6
4
|
|
@@ -215,6 +213,32 @@ class AttributeTest < Test::Unit::TestCase
|
|
215
213
|
assert_equal @now, subject.prop['mydatetime']
|
216
214
|
end
|
217
215
|
end
|
216
|
+
|
217
|
+
|
218
|
+
context 'a saved serialized class' do
|
219
|
+
setup do
|
220
|
+
@dog = Dog.new('Pavlov', 'Freud')
|
221
|
+
end
|
222
|
+
|
223
|
+
subject do
|
224
|
+
klass = Class.new(ActiveRecord::Base) do
|
225
|
+
include Property
|
226
|
+
set_table_name :dummies
|
227
|
+
property.serialize 'myserialized', Dog
|
228
|
+
end
|
229
|
+
|
230
|
+
obj = klass.create('myserialized' => @dog)
|
231
|
+
klass.find(obj)
|
232
|
+
end
|
233
|
+
|
234
|
+
should 'find class back' do
|
235
|
+
assert_kind_of Dog, subject.prop['myserialized']
|
236
|
+
end
|
237
|
+
|
238
|
+
should 'find same value' do
|
239
|
+
assert_equal @dog, subject.prop['myserialized']
|
240
|
+
end
|
241
|
+
end
|
218
242
|
end
|
219
243
|
|
220
244
|
context 'Setting attributes' do
|
@@ -0,0 +1,119 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
require 'fixtures'
|
3
|
+
|
4
|
+
class BehaviorTest < Test::Unit::TestCase
|
5
|
+
|
6
|
+
context 'A Behavior' do
|
7
|
+
subject { Property::Behavior.new('Foobar') }
|
8
|
+
|
9
|
+
should 'allow string columns' do
|
10
|
+
subject.property.string('weapon')
|
11
|
+
column = subject.columns['weapon']
|
12
|
+
assert_equal 'weapon', column.name
|
13
|
+
assert_equal String, column.klass
|
14
|
+
assert_equal :string, column.type
|
15
|
+
end
|
16
|
+
|
17
|
+
should 'treat symbol keys as strings' do
|
18
|
+
subject.property.string(:weapon)
|
19
|
+
column = subject.columns['weapon']
|
20
|
+
assert_equal 'weapon', column.name
|
21
|
+
assert_equal String, column.klass
|
22
|
+
assert_equal :string, column.type
|
23
|
+
end
|
24
|
+
|
25
|
+
should 'allow integer columns' do
|
26
|
+
subject.property.integer('indestructible')
|
27
|
+
column = subject.columns['indestructible']
|
28
|
+
assert_equal 'indestructible', column.name
|
29
|
+
assert_equal Fixnum, column.klass
|
30
|
+
assert_equal :integer, column.type
|
31
|
+
end
|
32
|
+
|
33
|
+
should 'allow float columns' do
|
34
|
+
subject.property.float('boat')
|
35
|
+
column = subject.columns['boat']
|
36
|
+
assert_equal 'boat', column.name
|
37
|
+
assert_equal Float, column.klass
|
38
|
+
assert_equal :float, column.type
|
39
|
+
end
|
40
|
+
|
41
|
+
should 'allow datetime columns' do
|
42
|
+
subject.property.datetime('time_weapon')
|
43
|
+
column = subject.columns['time_weapon']
|
44
|
+
assert_equal 'time_weapon', column.name
|
45
|
+
assert_equal Time, column.klass
|
46
|
+
assert_equal :datetime, column.type
|
47
|
+
end
|
48
|
+
|
49
|
+
should 'allow default value option' do
|
50
|
+
subject.property.integer('force', :default => 10)
|
51
|
+
column = subject.columns['force']
|
52
|
+
assert_equal 10, column.default
|
53
|
+
end
|
54
|
+
|
55
|
+
should 'allow indexed option' do
|
56
|
+
subject.property.string('rolodex', :indexed => true)
|
57
|
+
column = subject.columns['rolodex']
|
58
|
+
assert column.indexed?
|
59
|
+
end
|
60
|
+
end # A Behavior
|
61
|
+
|
62
|
+
context 'Adding a behavior' do
|
63
|
+
setup do
|
64
|
+
@poet = Property::Behavior.new('Poet') do |p|
|
65
|
+
p.string 'poem'
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
context 'to a class' do
|
70
|
+
setup do
|
71
|
+
@parent = Class.new(ActiveRecord::Base) do
|
72
|
+
set_table_name :dummies
|
73
|
+
include Property
|
74
|
+
property.string 'name'
|
75
|
+
end
|
76
|
+
|
77
|
+
@klass = Class.new(@parent)
|
78
|
+
end
|
79
|
+
|
80
|
+
should 'propagate definitions to child' do
|
81
|
+
@parent.behave_like @poet
|
82
|
+
assert_equal %w{name poem}, @klass.schema.column_names.sort
|
83
|
+
end
|
84
|
+
|
85
|
+
should 'raise an exception if class contains same definitions' do
|
86
|
+
@parent.property.string 'poem'
|
87
|
+
assert_raise(TypeError) { @parent.behave_like @poet }
|
88
|
+
end
|
89
|
+
|
90
|
+
should 'not raise an exception on double inclusion' do
|
91
|
+
@parent.behave_like @poet
|
92
|
+
assert_nothing_raised { @parent.behave_like @poet }
|
93
|
+
end
|
94
|
+
|
95
|
+
should 'add accessor methods to child' do
|
96
|
+
subject = @klass.new
|
97
|
+
assert_raises(NoMethodError) { subject.poem = 'Poe'}
|
98
|
+
@parent.behave_like @poet
|
99
|
+
|
100
|
+
assert_nothing_raised { subject.poem = 'Poe'}
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
context 'to a parent class' do
|
105
|
+
end
|
106
|
+
|
107
|
+
context 'to an instance' do
|
108
|
+
subject { Developer.new }
|
109
|
+
|
110
|
+
setup do
|
111
|
+
subject.behave_like @poet
|
112
|
+
end
|
113
|
+
|
114
|
+
should 'merge property definitions' do
|
115
|
+
assert_equal %w{age first_name language last_name poem}, subject.schema.column_names.sort
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
@@ -9,29 +9,31 @@ class DeclarationTest < Test::Unit::TestCase
|
|
9
9
|
@klass = Developer
|
10
10
|
end
|
11
11
|
|
12
|
-
should 'inherit property
|
13
|
-
assert_equal %w{age first_name language last_name}, @klass.
|
12
|
+
should 'inherit property columns from parent class' do
|
13
|
+
assert_equal %w{age first_name language last_name}, @klass.schema.column_names.sort
|
14
14
|
end
|
15
15
|
|
16
16
|
should 'not back-propagate definitions to parent' do
|
17
|
-
assert !@klass.superclass.
|
17
|
+
assert !@klass.superclass.schema.columns.include?('language')
|
18
18
|
end
|
19
19
|
|
20
|
-
should 'inherit
|
20
|
+
should 'inherit new definitions in parent' do
|
21
21
|
class ParentClass < ActiveRecord::Base
|
22
22
|
include Property
|
23
23
|
property.string 'name'
|
24
24
|
end
|
25
|
+
|
25
26
|
@klass = Class.new(ParentClass) do
|
26
27
|
property.integer 'age'
|
27
28
|
end
|
28
|
-
|
29
|
+
|
30
|
+
assert_equal %w{age name}, @klass.schema.column_names.sort
|
29
31
|
|
30
32
|
ParentClass.class_eval do
|
31
33
|
property.string 'first_name'
|
32
34
|
end
|
33
35
|
|
34
|
-
assert_equal %w{age first_name name}, @klass.
|
36
|
+
assert_equal %w{age first_name name}, @klass.schema.column_names.sort
|
35
37
|
end
|
36
38
|
|
37
39
|
should 'not be allowed to overwrite a property from the parent class' do
|
@@ -62,7 +64,7 @@ class DeclarationTest < Test::Unit::TestCase
|
|
62
64
|
|
63
65
|
should 'create Property::Column definitions' do
|
64
66
|
subject.property.string('weapon')
|
65
|
-
assert_kind_of Property::Column, subject.
|
67
|
+
assert_kind_of Property::Column, subject.schema.columns['weapon']
|
66
68
|
end
|
67
69
|
|
68
70
|
should 'create ruby accessors' do
|
@@ -84,7 +86,7 @@ class DeclarationTest < Test::Unit::TestCase
|
|
84
86
|
|
85
87
|
should 'allow string columns' do
|
86
88
|
subject.property.string('weapon')
|
87
|
-
column = subject.
|
89
|
+
column = subject.schema.columns['weapon']
|
88
90
|
assert_equal 'weapon', column.name
|
89
91
|
assert_equal String, column.klass
|
90
92
|
assert_equal :string, column.type
|
@@ -92,7 +94,7 @@ class DeclarationTest < Test::Unit::TestCase
|
|
92
94
|
|
93
95
|
should 'treat symbol keys as strings' do
|
94
96
|
subject.property.string(:weapon)
|
95
|
-
column = subject.
|
97
|
+
column = subject.schema.columns['weapon']
|
96
98
|
assert_equal 'weapon', column.name
|
97
99
|
assert_equal String, column.klass
|
98
100
|
assert_equal :string, column.type
|
@@ -100,7 +102,7 @@ class DeclarationTest < Test::Unit::TestCase
|
|
100
102
|
|
101
103
|
should 'allow integer columns' do
|
102
104
|
subject.property.integer('indestructible')
|
103
|
-
column = subject.
|
105
|
+
column = subject.schema.columns['indestructible']
|
104
106
|
assert_equal 'indestructible', column.name
|
105
107
|
assert_equal Fixnum, column.klass
|
106
108
|
assert_equal :integer, column.type
|
@@ -108,7 +110,7 @@ class DeclarationTest < Test::Unit::TestCase
|
|
108
110
|
|
109
111
|
should 'allow float columns' do
|
110
112
|
subject.property.float('boat')
|
111
|
-
column = subject.
|
113
|
+
column = subject.schema.columns['boat']
|
112
114
|
assert_equal 'boat', column.name
|
113
115
|
assert_equal Float, column.klass
|
114
116
|
assert_equal :float, column.type
|
@@ -116,41 +118,67 @@ class DeclarationTest < Test::Unit::TestCase
|
|
116
118
|
|
117
119
|
should 'allow datetime columns' do
|
118
120
|
subject.property.datetime('time_weapon')
|
119
|
-
column = subject.
|
121
|
+
column = subject.schema.columns['time_weapon']
|
120
122
|
assert_equal 'time_weapon', column.name
|
121
123
|
assert_equal Time, column.klass
|
122
124
|
assert_equal :datetime, column.type
|
123
125
|
end
|
126
|
+
|
127
|
+
should 'allow serialized columns' do
|
128
|
+
Dog = Struct.new(:name, :toy) do
|
129
|
+
def self.json_create(data)
|
130
|
+
Dog.new(data['name'], data['toy'])
|
131
|
+
end
|
132
|
+
def to_json(*args)
|
133
|
+
{ 'json_class' => self.class.to_s,
|
134
|
+
'name' => @name, 'toy' => @toy
|
135
|
+
}.to_json(*args)
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
subject.property.serialize('pet', Dog)
|
140
|
+
column = subject.schema.columns['pet']
|
141
|
+
assert_equal 'pet', column.name
|
142
|
+
assert_equal Dog, column.klass
|
143
|
+
assert_equal nil, column.type
|
144
|
+
end
|
124
145
|
|
125
146
|
should 'allow default value option' do
|
126
147
|
subject.property.integer('force', :default => 10)
|
127
|
-
column = subject.
|
148
|
+
column = subject.schema.columns['force']
|
128
149
|
assert_equal 10, column.default
|
129
150
|
end
|
130
151
|
|
131
152
|
should 'allow indexed option' do
|
132
153
|
subject.property.string('rolodex', :indexed => true)
|
133
|
-
column = subject.
|
154
|
+
column = subject.schema.columns['rolodex']
|
134
155
|
assert column.indexed?
|
135
156
|
end
|
136
157
|
|
137
|
-
context '
|
158
|
+
context 'through a Behavior on an instance' do
|
138
159
|
setup do
|
139
160
|
@instance = subject.new
|
140
|
-
@
|
141
|
-
|
161
|
+
@poet = Property::Behavior.new('Poet')
|
162
|
+
@poet.property do |p|
|
163
|
+
p.string 'poem'
|
142
164
|
end
|
165
|
+
|
166
|
+
@instance.behave_like @poet
|
143
167
|
end
|
144
168
|
|
145
169
|
should 'behave like any other property column' do
|
146
|
-
@instance.attributes = {'
|
170
|
+
@instance.attributes = {'poem' => 'hello'}
|
171
|
+
@instance.poem = 'shazam'
|
147
172
|
assert @instance.save
|
148
173
|
@instance = subject.find(@instance.id)
|
149
|
-
assert_equal Hash['
|
174
|
+
assert_equal Hash['poem' => 'shazam'], @instance.prop
|
150
175
|
end
|
151
176
|
|
152
177
|
should 'not affect instance class' do
|
153
|
-
assert !subject.
|
178
|
+
assert !subject.schema.column_names.include?('poem')
|
179
|
+
assert_raise(NoMethodError) do
|
180
|
+
subject.new.poem = 'not a poet'
|
181
|
+
end
|
154
182
|
end
|
155
183
|
end
|
156
184
|
end
|
@@ -162,12 +190,24 @@ class DeclarationTest < Test::Unit::TestCase
|
|
162
190
|
end
|
163
191
|
|
164
192
|
should 'return empty Hash if no property columsn are declared' do
|
165
|
-
assert_equal Hash[], Dummy.
|
193
|
+
assert_equal Hash[], Dummy.schema.columns
|
166
194
|
end
|
167
195
|
|
168
196
|
should 'return list of property columns from class' do
|
169
|
-
assert_kind_of Hash, Employee.
|
170
|
-
assert_kind_of Property::Column, Employee.
|
197
|
+
assert_kind_of Hash, Employee.schema.columns
|
198
|
+
assert_kind_of Property::Column, Employee.schema.columns['first_name']
|
199
|
+
end
|
200
|
+
end
|
201
|
+
|
202
|
+
context 'On a class with a schema' do
|
203
|
+
subject { Developer }
|
204
|
+
|
205
|
+
should 'raise an exception if we ask to behave like a class without schema' do
|
206
|
+
assert_raise(TypeError) { subject.behave_like String }
|
207
|
+
end
|
208
|
+
|
209
|
+
should 'raise an exception if we ask to behave like an object' do
|
210
|
+
assert_raise(TypeError) { subject.behave_like 'me' }
|
171
211
|
end
|
172
212
|
end
|
173
213
|
end
|
@@ -9,6 +9,7 @@ class ValidationTest < Test::Unit::TestCase
|
|
9
9
|
include Property
|
10
10
|
property.float 'boat'
|
11
11
|
property.string 'bird_name'
|
12
|
+
property.serialize 'dog', Dog
|
12
13
|
end
|
13
14
|
|
14
15
|
subject { Pirate.create }
|
@@ -39,6 +40,12 @@ class ValidationTest < Test::Unit::TestCase
|
|
39
40
|
assert_kind_of String, subject.prop['bird_name']
|
40
41
|
assert_equal '1337', subject.prop['bird_name']
|
41
42
|
end
|
43
|
+
|
44
|
+
should 'show an error for serialized types' do
|
45
|
+
subject.update_attributes('dog' => 'Medor')
|
46
|
+
assert !subject.valid?
|
47
|
+
assert_equal 'cannot cast String to Dog', subject.errors['dog']
|
48
|
+
end
|
42
49
|
end
|
43
50
|
|
44
51
|
context 'to a blank value' do
|
@@ -9,4 +9,29 @@ class MyJSONTest < Test::Unit::TestCase
|
|
9
9
|
|
10
10
|
should_encode_and_decode_properties
|
11
11
|
|
12
|
+
context 'JSON validator' do
|
13
|
+
subject { Property::Serialization::JSON::Validator }
|
14
|
+
|
15
|
+
should 'respond to validate' do
|
16
|
+
assert subject.respond_to? :validate
|
17
|
+
end
|
18
|
+
|
19
|
+
[Property::Properties, String, Integer, Float].each do |a_class|
|
20
|
+
should "accept to serialize #{a_class}" do
|
21
|
+
assert subject.validate(a_class)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
context 'on a class with properties as custom type' do
|
27
|
+
subject do
|
28
|
+
Class.new(ActiveRecord::Base) do
|
29
|
+
include Property
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
should 'raise an exception if we try to encode an invalid class' do
|
34
|
+
assert_raise(TypeError) { subject.property.serialize 'not_json', Regexp }
|
35
|
+
end
|
36
|
+
end
|
12
37
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: property
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.8.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Renaud Kern
|
@@ -10,7 +10,7 @@ autorequire:
|
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
12
|
|
13
|
-
date: 2010-02-
|
13
|
+
date: 2010-02-12 00:00:00 +01:00
|
14
14
|
default_executable:
|
15
15
|
dependencies:
|
16
16
|
- !ruby/object:Gem::Dependency
|
@@ -50,11 +50,13 @@ files:
|
|
50
50
|
- generators/property/property_generator.rb
|
51
51
|
- lib/property.rb
|
52
52
|
- lib/property/attribute.rb
|
53
|
+
- lib/property/behavior.rb
|
53
54
|
- lib/property/column.rb
|
54
55
|
- lib/property/core_ext/time.rb
|
55
56
|
- lib/property/declaration.rb
|
56
57
|
- lib/property/dirty.rb
|
57
58
|
- lib/property/properties.rb
|
59
|
+
- lib/property/schema.rb
|
58
60
|
- lib/property/serialization/json.rb
|
59
61
|
- lib/property/serialization/marshal.rb
|
60
62
|
- lib/property/serialization/yaml.rb
|
@@ -63,6 +65,7 @@ files:
|
|
63
65
|
- test/shoulda_macros/serialization.rb
|
64
66
|
- test/test_helper.rb
|
65
67
|
- test/unit/property/attribute_test.rb
|
68
|
+
- test/unit/property/behavior_test.rb
|
66
69
|
- test/unit/property/declaration_test.rb
|
67
70
|
- test/unit/property/dirty_test.rb
|
68
71
|
- test/unit/property/validation_test.rb
|
@@ -102,6 +105,7 @@ test_files:
|
|
102
105
|
- test/shoulda_macros/serialization.rb
|
103
106
|
- test/test_helper.rb
|
104
107
|
- test/unit/property/attribute_test.rb
|
108
|
+
- test/unit/property/behavior_test.rb
|
105
109
|
- test/unit/property/declaration_test.rb
|
106
110
|
- test/unit/property/dirty_test.rb
|
107
111
|
- test/unit/property/validation_test.rb
|