property 0.7.0 → 0.8.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|