property 0.6.0 → 0.7.0
Sign up to get free protection for your applications and to get access to all the features.
- data/History.txt +6 -0
- data/lib/property.rb +2 -1
- data/lib/property/attribute.rb +1 -1
- data/lib/property/column.rb +18 -2
- data/lib/property/core_ext/time.rb +19 -0
- data/lib/property/declaration.rb +88 -58
- data/lib/property/properties.rb +17 -9
- data/property.gemspec +2 -1
- data/test/unit/property/attribute_test.rb +92 -2
- data/test/unit/property/declaration_test.rb +21 -0
- data/test/unit/property/validation_test.rb +28 -2
- metadata +2 -1
data/History.txt
CHANGED
data/lib/property.rb
CHANGED
@@ -4,9 +4,10 @@ require 'property/properties'
|
|
4
4
|
require 'property/column'
|
5
5
|
require 'property/declaration'
|
6
6
|
require 'property/serialization/json'
|
7
|
+
require 'property/core_ext/time'
|
7
8
|
|
8
9
|
module Property
|
9
|
-
VERSION = '0.
|
10
|
+
VERSION = '0.7.0'
|
10
11
|
|
11
12
|
def self.included(base)
|
12
13
|
base.class_eval do
|
data/lib/property/attribute.rb
CHANGED
@@ -50,7 +50,7 @@ module Property
|
|
50
50
|
|
51
51
|
private
|
52
52
|
def attributes_with_properties=(attributes, guard_protected_attributes = true)
|
53
|
-
property_columns = self.
|
53
|
+
property_columns = self.properties.columns
|
54
54
|
properties = {}
|
55
55
|
|
56
56
|
attributes.keys.each do |k|
|
data/lib/property/column.rb
CHANGED
@@ -26,8 +26,24 @@ module Property
|
|
26
26
|
@indexed
|
27
27
|
end
|
28
28
|
|
29
|
-
def
|
30
|
-
|
29
|
+
def default_for(owner)
|
30
|
+
if default.kind_of?(Proc)
|
31
|
+
default.call
|
32
|
+
elsif default.kind_of?(Symbol)
|
33
|
+
owner.send(default)
|
34
|
+
else
|
35
|
+
default
|
36
|
+
end
|
31
37
|
end
|
38
|
+
|
39
|
+
private
|
40
|
+
def extract_property_options(options)
|
41
|
+
@indexed = options.delete(:indexed)
|
42
|
+
end
|
43
|
+
|
44
|
+
def extract_default(default)
|
45
|
+
(default.kind_of?(Proc) || default.kind_of?(Symbol)) ? default : type_cast(default)
|
46
|
+
end
|
47
|
+
|
32
48
|
end # Column
|
33
49
|
end # Property
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# We provide our own 'to_json' version because the default is just an alias for 'to_s' and we
|
2
|
+
# do not get the correct type back.
|
3
|
+
#
|
4
|
+
# In order to keep speed up, we have done some compromizes: all time values are considered to
|
5
|
+
# be UTC: we do not encode the timezone. We also ignore micro seconds.
|
6
|
+
class Time
|
7
|
+
JSON_REGEXP = /\A(\d{4})-(\d\d)-(\d\d) (\d\d):(\d\d):(\d\d)\z/
|
8
|
+
JSON_FORMAT = "%Y-%m-%d %H:%M:%S"
|
9
|
+
|
10
|
+
def self.json_create(serialized)
|
11
|
+
if serialized['data'] =~ JSON_REGEXP
|
12
|
+
Time.utc $1.to_i, $2.to_i, $3.to_i, $4.to_i, $5.to_i, $6.to_i
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def to_json(*args)
|
17
|
+
{ 'json_class' => self.class.name, 'data' => strftime(JSON_FORMAT) }.to_json(*args)
|
18
|
+
end
|
19
|
+
end
|
data/lib/property/declaration.rb
CHANGED
@@ -18,66 +18,84 @@ module Property
|
|
18
18
|
end
|
19
19
|
end
|
20
20
|
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
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))
|
25
31
|
end
|
32
|
+
end
|
26
33
|
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
+
def add_column(column)
|
35
|
+
own_columns[column.name] = column
|
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) }
|
34
66
|
end
|
67
|
+
EOV
|
68
|
+
end
|
69
|
+
|
70
|
+
private
|
71
|
+
def own_columns
|
72
|
+
@klass.own_property_columns ||= {}
|
35
73
|
end
|
36
74
|
|
37
|
-
|
38
|
-
|
39
|
-
# p.serialize MyClass, xxx, xxx
|
40
|
-
# def serialize(klass, name, options={})
|
41
|
-
# if @klass.super_property_columns[name.to_s]
|
42
|
-
# raise TypeError.new("Property '#{name}' is already defined in a superclass.")
|
43
|
-
# elsif !@klass.validate_property_class(type)
|
44
|
-
# raise TypeError.new("Custom type '#{type}' cannot be serialized.")
|
45
|
-
# else
|
46
|
-
# # Find a way to insert the type (maybe with 'serialize'...)
|
47
|
-
# # (@klass.own_property_columns ||= {})[name] = Property::Column.new(name, type, options)
|
48
|
-
# end
|
49
|
-
# end
|
50
|
-
|
51
|
-
# def string(*args)
|
52
|
-
# options = args.extract_options!
|
53
|
-
# column_names = args
|
54
|
-
# default = options.delete(:default)
|
55
|
-
# column_names.each { |name| column(name, default, 'string', options) }
|
56
|
-
# end
|
57
|
-
%w( string text integer float decimal datetime timestamp time date binary boolean ).each do |column_type|
|
58
|
-
class_eval <<-EOV
|
59
|
-
def #{column_type}(*args)
|
60
|
-
options = args.extract_options!
|
61
|
-
column_names = args
|
62
|
-
default = options.delete(:default)
|
63
|
-
column_names.each { |name| column(name, default, '#{column_type}', options) }
|
64
|
-
end
|
65
|
-
EOV
|
75
|
+
def columns
|
76
|
+
@klass.property_columns
|
66
77
|
end
|
67
78
|
|
68
|
-
|
69
|
-
def own_columns
|
70
|
-
@klass.own_property_columns ||= {}
|
71
|
-
end
|
79
|
+
end
|
72
80
|
|
73
|
-
|
74
|
-
|
75
|
-
|
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
|
76
89
|
|
90
|
+
def columns
|
91
|
+
@properties.columns
|
77
92
|
end
|
93
|
+
end
|
78
94
|
|
79
|
-
|
80
|
-
|
95
|
+
module ClassMethods
|
96
|
+
|
97
|
+
# Use this class method to declare properties that will be used in your models.
|
98
|
+
# Example:
|
81
99
|
# property.string 'phone', :default => ''
|
82
100
|
#
|
83
101
|
# You can also use a block:
|
@@ -92,6 +110,10 @@ module Property
|
|
92
110
|
proxy
|
93
111
|
end
|
94
112
|
|
113
|
+
# @internal.
|
114
|
+
# If you need the list of columns (including instance columns), you should use
|
115
|
+
# properties.columns
|
116
|
+
#
|
95
117
|
# Return the list of all properties defined for the current class, including the properties
|
96
118
|
# defined in the parent class.
|
97
119
|
def property_columns
|
@@ -180,20 +202,28 @@ module Property
|
|
180
202
|
|
181
203
|
# Evaluate the definition for an attribute related method
|
182
204
|
def evaluate_attribute_property_method(attr_name, method_definition, method_name=attr_name)
|
183
|
-
|
184
|
-
class_eval(method_definition, __FILE__, __LINE__)
|
185
|
-
rescue SyntaxError => err
|
186
|
-
if logger
|
187
|
-
logger.warn "Exception occurred during method compilation."
|
188
|
-
logger.warn "Maybe #{attr_name} is not a valid Ruby identifier?"
|
189
|
-
logger.warn err.message
|
190
|
-
end
|
191
|
-
end
|
205
|
+
class_eval(method_definition, __FILE__, __LINE__)
|
192
206
|
end
|
193
207
|
end # ClassMethods
|
194
208
|
|
195
209
|
module InstanceMethods
|
196
210
|
|
211
|
+
# Use this method to declare properties *for the current* instance.
|
212
|
+
# Example:
|
213
|
+
# @obj.property.string 'phone', :default => ''
|
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
|
225
|
+
end
|
226
|
+
|
197
227
|
protected
|
198
228
|
def properties_validation
|
199
229
|
properties.validate
|
data/lib/property/properties.rb
CHANGED
@@ -14,7 +14,7 @@ module Property
|
|
14
14
|
def []=(key, value)
|
15
15
|
if column = columns[key]
|
16
16
|
if value.blank?
|
17
|
-
if default = column.
|
17
|
+
if default = column.default_for(@owner)
|
18
18
|
super(key, default)
|
19
19
|
else
|
20
20
|
delete(key)
|
@@ -36,8 +36,7 @@ module Property
|
|
36
36
|
end
|
37
37
|
|
38
38
|
def validate
|
39
|
-
|
40
|
-
column_names = @owner.class.property_column_names
|
39
|
+
column_names = columns.keys
|
41
40
|
errors = @owner.errors
|
42
41
|
no_errors = true
|
43
42
|
|
@@ -52,20 +51,29 @@ module Property
|
|
52
51
|
missing_keys.each do |key|
|
53
52
|
column = columns[key]
|
54
53
|
if column.has_default?
|
55
|
-
self[key] = column.
|
54
|
+
self[key] = column.default_for(@owner)
|
56
55
|
end
|
57
56
|
end
|
58
57
|
|
59
58
|
keys_to_validate.each do |key|
|
60
|
-
|
59
|
+
value = self[key]
|
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
|
61
70
|
end
|
62
71
|
|
63
72
|
bad_keys.empty?
|
64
73
|
end
|
65
74
|
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
end
|
75
|
+
def columns
|
76
|
+
@columns ||= @owner.class.property_columns
|
77
|
+
end
|
70
78
|
end
|
71
79
|
end
|
data/property.gemspec
CHANGED
@@ -5,7 +5,7 @@
|
|
5
5
|
|
6
6
|
Gem::Specification.new do |s|
|
7
7
|
s.name = %q{property}
|
8
|
-
s.version = "0.
|
8
|
+
s.version = "0.7.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"]
|
@@ -25,6 +25,7 @@ Gem::Specification.new do |s|
|
|
25
25
|
"lib/property.rb",
|
26
26
|
"lib/property/attribute.rb",
|
27
27
|
"lib/property/column.rb",
|
28
|
+
"lib/property/core_ext/time.rb",
|
28
29
|
"lib/property/declaration.rb",
|
29
30
|
"lib/property/dirty.rb",
|
30
31
|
"lib/property/properties.rb",
|
@@ -97,11 +97,11 @@ class AttributeTest < Test::Unit::TestCase
|
|
97
97
|
subject { Version.new }
|
98
98
|
|
99
99
|
setup do
|
100
|
-
subject.properties={'foo'=>'bar'
|
100
|
+
subject.properties={'foo'=>'bar'}
|
101
101
|
end
|
102
102
|
|
103
103
|
should 'be accessible with :properties method' do
|
104
|
-
assert_equal Hash['foo'=>'bar'
|
104
|
+
assert_equal Hash['foo'=>'bar'], subject.properties
|
105
105
|
end
|
106
106
|
|
107
107
|
should 'be accessible with native methods' do
|
@@ -127,6 +127,96 @@ class AttributeTest < Test::Unit::TestCase
|
|
127
127
|
|
128
128
|
end
|
129
129
|
|
130
|
+
context 'Retrieving' do
|
131
|
+
context 'a saved string' do
|
132
|
+
subject do
|
133
|
+
klass = Class.new(ActiveRecord::Base) do
|
134
|
+
include Property
|
135
|
+
set_table_name :dummies
|
136
|
+
property.string 'mystring'
|
137
|
+
end
|
138
|
+
|
139
|
+
obj = klass.create('mystring' => 'some data')
|
140
|
+
klass.find(obj)
|
141
|
+
end
|
142
|
+
|
143
|
+
should 'find a string' do
|
144
|
+
assert_kind_of String, subject.prop['mystring']
|
145
|
+
end
|
146
|
+
|
147
|
+
should 'find same value' do
|
148
|
+
assert_equal 'some data', subject.prop['mystring']
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
context 'a saved integer' do
|
153
|
+
subject do
|
154
|
+
klass = Class.new(ActiveRecord::Base) do
|
155
|
+
include Property
|
156
|
+
set_table_name :dummies
|
157
|
+
property.integer 'myinteger'
|
158
|
+
end
|
159
|
+
|
160
|
+
obj = klass.create('myinteger' => 789)
|
161
|
+
klass.find(obj)
|
162
|
+
end
|
163
|
+
|
164
|
+
should 'find an integer' do
|
165
|
+
assert_kind_of Fixnum, subject.prop['myinteger']
|
166
|
+
end
|
167
|
+
|
168
|
+
should 'find same value' do
|
169
|
+
assert_equal 789, subject.prop['myinteger']
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
context 'a saved float' do
|
174
|
+
subject do
|
175
|
+
klass = Class.new(ActiveRecord::Base) do
|
176
|
+
include Property
|
177
|
+
set_table_name :dummies
|
178
|
+
property.float 'myfloat'
|
179
|
+
end
|
180
|
+
|
181
|
+
obj = klass.create('myfloat' => 78.9)
|
182
|
+
klass.find(obj)
|
183
|
+
end
|
184
|
+
|
185
|
+
should 'find an float' do
|
186
|
+
assert_kind_of Float, subject.prop['myfloat']
|
187
|
+
end
|
188
|
+
|
189
|
+
should 'find same value' do
|
190
|
+
assert_equal 78.9, subject.prop['myfloat']
|
191
|
+
end
|
192
|
+
end
|
193
|
+
|
194
|
+
context 'a saved datetime' do
|
195
|
+
setup do
|
196
|
+
@now = Time.utc(2010,02,11,17,50,18)
|
197
|
+
end
|
198
|
+
|
199
|
+
subject do
|
200
|
+
klass = Class.new(ActiveRecord::Base) do
|
201
|
+
include Property
|
202
|
+
set_table_name :dummies
|
203
|
+
property.datetime 'mydatetime'
|
204
|
+
end
|
205
|
+
|
206
|
+
obj = klass.create('mydatetime' => @now)
|
207
|
+
klass.find(obj)
|
208
|
+
end
|
209
|
+
|
210
|
+
should 'find an datetime' do
|
211
|
+
assert_kind_of Time, subject.prop['mydatetime']
|
212
|
+
end
|
213
|
+
|
214
|
+
should 'find same value' do
|
215
|
+
assert_equal @now, subject.prop['mydatetime']
|
216
|
+
end
|
217
|
+
end
|
218
|
+
end
|
219
|
+
|
130
220
|
context 'Setting attributes' do
|
131
221
|
subject { Version.new }
|
132
222
|
|
@@ -54,6 +54,7 @@ class DeclarationTest < Test::Unit::TestCase
|
|
54
54
|
|
55
55
|
context 'Property declaration' do
|
56
56
|
Superhero = Class.new(ActiveRecord::Base) do
|
57
|
+
set_table_name :dummies
|
57
58
|
include Property
|
58
59
|
end
|
59
60
|
|
@@ -132,6 +133,26 @@ class DeclarationTest < Test::Unit::TestCase
|
|
132
133
|
column = subject.property_columns['rolodex']
|
133
134
|
assert column.indexed?
|
134
135
|
end
|
136
|
+
|
137
|
+
context 'in an instance singleton' do
|
138
|
+
setup do
|
139
|
+
@instance = subject.new
|
140
|
+
@instance.property do |p|
|
141
|
+
p.string 'instance_only'
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
should 'behave like any other property column' do
|
146
|
+
@instance.attributes = {'instance_only' => 'hello'}
|
147
|
+
assert @instance.save
|
148
|
+
@instance = subject.find(@instance.id)
|
149
|
+
assert_equal Hash['instance_only' => 'hello'], @instance.prop
|
150
|
+
end
|
151
|
+
|
152
|
+
should 'not affect instance class' do
|
153
|
+
assert !subject.property_column_names.include?('instance_only')
|
154
|
+
end
|
155
|
+
end
|
135
156
|
end
|
136
157
|
|
137
158
|
context 'Property columns' do
|
@@ -62,21 +62,47 @@ class ValidationTest < Test::Unit::TestCase
|
|
62
62
|
|
63
63
|
context 'On a class with default property values' do
|
64
64
|
Cat = Class.new(ActiveRecord::Base) do
|
65
|
+
attr_accessor :encoding
|
66
|
+
|
65
67
|
set_table_name 'dummies'
|
66
68
|
include Property::Attribute
|
67
69
|
property do |p|
|
68
70
|
p.string 'eat', :default => 'mouse'
|
69
71
|
p.string 'name'
|
72
|
+
p.datetime 'seen_at', :default => Proc.new { Time.now }
|
73
|
+
p.string 'encoding', :default => :get_encoding
|
74
|
+
end
|
75
|
+
|
76
|
+
def get_encoding
|
77
|
+
@encoding
|
70
78
|
end
|
71
79
|
end
|
72
80
|
|
73
|
-
should 'insert default values' do
|
81
|
+
should 'insert default literal values' do
|
74
82
|
subject = Cat.create
|
75
83
|
subject.reload
|
76
84
|
assert_equal 'mouse', subject.prop['eat']
|
77
85
|
end
|
78
86
|
|
79
|
-
should '
|
87
|
+
should 'call procs to get default if missing' do
|
88
|
+
subject = Cat.create
|
89
|
+
assert_kind_of Time, subject.prop['seen_at']
|
90
|
+
end
|
91
|
+
|
92
|
+
should 'call procs to get default if empty' do
|
93
|
+
subject = Cat.new('seen_at' => '')
|
94
|
+
assert_kind_of Time, subject.prop['seen_at']
|
95
|
+
end
|
96
|
+
|
97
|
+
should 'call owner methods to get default' do
|
98
|
+
subject = Cat.new
|
99
|
+
subject.encoding = 'yooupla/boom'
|
100
|
+
assert subject.save
|
101
|
+
|
102
|
+
assert_equal 'yooupla/boom', subject.prop['encoding']
|
103
|
+
end
|
104
|
+
|
105
|
+
should 'accept other values' do
|
80
106
|
subject = Cat.create('eat' => 'birds')
|
81
107
|
subject.reload
|
82
108
|
assert_equal 'birds', subject.prop['eat']
|
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.7.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Renaud Kern
|
@@ -51,6 +51,7 @@ files:
|
|
51
51
|
- lib/property.rb
|
52
52
|
- lib/property/attribute.rb
|
53
53
|
- lib/property/column.rb
|
54
|
+
- lib/property/core_ext/time.rb
|
54
55
|
- lib/property/declaration.rb
|
55
56
|
- lib/property/dirty.rb
|
56
57
|
- lib/property/properties.rb
|