property 0.6.0 → 0.7.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.
@@ -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