property 0.6.0 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,3 +1,9 @@
1
+ == 0.7.0 2010-02-11
2
+
3
+ * 2 major enhancement
4
+ * enabled instance property definitions
5
+ * Time is now natively parsed by json (no typecast)
6
+
1
7
  == 0.6.0 2010-02-11
2
8
 
3
9
  * 1 major enhancement
@@ -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.6.0'
10
+ VERSION = '0.7.0'
10
11
 
11
12
  def self.included(base)
12
13
  base.class_eval do
@@ -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.class.property_columns
53
+ property_columns = self.properties.columns
54
54
  properties = {}
55
55
 
56
56
  attributes.keys.each do |k|
@@ -26,8 +26,24 @@ module Property
26
26
  @indexed
27
27
  end
28
28
 
29
- def extract_property_options(options)
30
- @indexed = options.delete(:indexed)
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
@@ -18,66 +18,84 @@ module Property
18
18
  end
19
19
  end
20
20
 
21
- module ClassMethods
22
- class DefinitionProxy
23
- def initialize(klass)
24
- @klass = klass
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
- def column(name, default, type, options)
28
- if columns[name.to_s]
29
- raise TypeError.new("Property '#{name}' is already defined.")
30
- else
31
- new_column = Property::Column.new(name, default, type, options)
32
- own_columns[new_column.name] = new_column
33
- @klass.define_property_methods(new_column) if new_column.should_create_accessors?
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
- # If someday we find the need to insert other native classes directly in the DB, we
38
- # could use this:
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
- private
69
- def own_columns
70
- @klass.own_property_columns ||= {}
71
- end
79
+ end
72
80
 
73
- def columns
74
- @klass.property_columns
75
- end
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
- # Use this class method to declare properties that will be used in your models. Note
80
- # that you must provide string keys. Example:
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
- begin
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
@@ -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.default
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
- columns = @owner.class.property_columns
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.default
54
+ self[key] = column.default_for(@owner)
56
55
  end
57
56
  end
58
57
 
59
58
  keys_to_validate.each do |key|
60
- columns[key].validate(self[key], errors)
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
- private
67
- def columns
68
- @columns ||= @owner.class.property_columns
69
- end
75
+ def columns
76
+ @columns ||= @owner.class.property_columns
77
+ end
70
78
  end
71
79
  end
@@ -5,7 +5,7 @@
5
5
 
6
6
  Gem::Specification.new do |s|
7
7
  s.name = %q{property}
8
- s.version = "0.6.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', :tic=>:tac}
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', :tic=>:tac], subject.properties
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 'insert accept other values' do
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.6.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