property 0.5.0 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore CHANGED
@@ -1,2 +1,3 @@
1
1
  .DS_Store
2
2
  coverage
3
+ *.gem
data/History.txt CHANGED
@@ -1,10 +1,15 @@
1
- == 0.4.0 2010-02-02
1
+ == 0.6.0 2010-02-11
2
2
 
3
3
  * 1 major enhancement
4
- * initial plugin code
4
+ * enabled ruby accessors in model
5
5
 
6
6
  == 0.5.0 2010-02-11
7
7
 
8
8
  * 2 major enhancement
9
9
  * changed plugin into gem
10
- * using Rails columns to handle defaults and type casting
10
+ * using Rails columns to handle defaults and type casting
11
+
12
+ == 0.4.0 2010-02-02
13
+
14
+ * 1 major enhancement
15
+ * initial plugin code
data/Rakefile CHANGED
@@ -43,7 +43,7 @@ begin
43
43
 
44
44
  # Gem dependecies
45
45
  gemspec.add_development_dependency('shoulda')
46
- gemspec.add_dependency('active_record')
46
+ gemspec.add_dependency('activerecord')
47
47
  end
48
48
  rescue LoadError
49
49
  puts "Jeweler not available. Gem packaging tasks not available."
data/lib/property.rb CHANGED
@@ -6,7 +6,7 @@ require 'property/declaration'
6
6
  require 'property/serialization/json'
7
7
 
8
8
  module Property
9
- VERSION = '0.5.0'
9
+ VERSION = '0.6.0'
10
10
 
11
11
  def self.included(base)
12
12
  base.class_eval do
@@ -50,11 +50,11 @@ module Property
50
50
 
51
51
  private
52
52
  def attributes_with_properties=(attributes, guard_protected_attributes = true)
53
- columns = self.class.column_names
53
+ property_columns = self.class.property_columns
54
54
  properties = {}
55
55
 
56
56
  attributes.keys.each do |k|
57
- if !respond_to?("#{k}=") && !columns.include?(k)
57
+ if property_columns.include?(k)
58
58
  properties[k] = attributes.delete(k)
59
59
  end
60
60
  end
@@ -74,7 +74,6 @@ module Property
74
74
 
75
75
  def dump_properties
76
76
  if @properties
77
- @properties.compact!
78
77
  if !@properties.empty?
79
78
  write_attribute('properties', encode_properties(@properties))
80
79
  else
@@ -6,6 +6,7 @@ module Property
6
6
  # such as name, type and options. It is also used to typecast from strings to
7
7
  # the proper type (date, integer, float, etc).
8
8
  class Column < ::ActiveRecord::ConnectionAdapters::Column
9
+ SAFE_NAMES_REGEXP = %r{\A[a-zA-Z_]+\Z}
9
10
 
10
11
  def initialize(name, default, type, options={})
11
12
  name = name.to_s
@@ -14,14 +15,11 @@ module Property
14
15
  end
15
16
 
16
17
  def validate(value, errors)
17
- if !value.kind_of?(klass)
18
- if value.nil?
19
- default
20
- else
21
- errors.add("#{name}", "invalid data type. Received #{value.class}, expected #{klass}.")
22
- nil
23
- end
24
- end
18
+ # Do nothing for the moment
19
+ end
20
+
21
+ def should_create_accessors?
22
+ name =~ SAFE_NAMES_REGEXP
25
23
  end
26
24
 
27
25
  def indexed?
@@ -28,7 +28,9 @@ module Property
28
28
  if columns[name.to_s]
29
29
  raise TypeError.new("Property '#{name}' is already defined.")
30
30
  else
31
- own_columns[name] = Property::Column.new(name, default, type, options)
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?
32
34
  end
33
35
  end
34
36
 
@@ -107,6 +109,87 @@ module Property
107
109
  {}
108
110
  end
109
111
  end
112
+
113
+ def define_property_methods(column)
114
+ name = column.name
115
+ unless instance_method_already_implemented?(name)
116
+ if create_time_zone_conversion_attribute?(name, column)
117
+ define_read_property_method_for_time_zone_conversion(name)
118
+ else
119
+ define_read_property_method(name.to_sym, name, column)
120
+ end
121
+ end
122
+
123
+ unless instance_method_already_implemented?("#{name}=")
124
+ if create_time_zone_conversion_attribute?(name, column)
125
+ define_write_property_method_for_time_zone_conversion(name)
126
+ else
127
+ define_write_property_method(name.to_sym)
128
+ end
129
+ end
130
+
131
+ unless instance_method_already_implemented?("#{name}?")
132
+ define_question_property_method(name)
133
+ end
134
+ end
135
+
136
+ private
137
+ # Define a property reader method. Cope with nil column.
138
+ def define_read_property_method(symbol, attr_name, column)
139
+ # Unlike rails, we do not cast on read
140
+ evaluate_attribute_property_method attr_name, "def #{symbol}; prop['#{attr_name}']; end"
141
+ end
142
+
143
+ # Defined for all +datetime+ and +timestamp+ attributes when +time_zone_aware_attributes+ are enabled.
144
+ # This enhanced read method automatically converts the UTC time stored in the database to the time zone stored in Time.zone.
145
+ def define_read_property_method_for_time_zone_conversion(attr_name)
146
+ method_body = <<-EOV
147
+ def #{attr_name}(reload = false)
148
+ cached = @attributes_cache['#{attr_name}']
149
+ return cached if cached && !reload
150
+ time = properties['#{attr_name}']
151
+ @attributes_cache['#{attr_name}'] = time.acts_like?(:time) ? time.in_time_zone : time
152
+ end
153
+ EOV
154
+ evaluate_attribute_property_method attr_name, method_body
155
+ end
156
+
157
+ # Defines a predicate method <tt>attr_name?</tt>.
158
+ def define_question_property_method(attr_name)
159
+ evaluate_attribute_property_method attr_name, "def #{attr_name}?; prop['#{attr_name}']; end", "#{attr_name}?"
160
+ end
161
+
162
+ def define_write_property_method(attr_name)
163
+ evaluate_attribute_property_method attr_name, "def #{attr_name}=(new_value);prop['#{attr_name}'] = new_value; end", "#{attr_name}="
164
+ end
165
+
166
+ # Defined for all +datetime+ and +timestamp+ attributes when +time_zone_aware_attributes+ are enabled.
167
+ # This enhanced write method will automatically convert the time passed to it to the zone stored in Time.zone.
168
+ def define_write_property_method_for_time_zone_conversion(attr_name)
169
+ method_body = <<-EOV
170
+ def #{attr_name}=(time)
171
+ unless time.acts_like?(:time)
172
+ time = time.is_a?(String) ? Time.zone.parse(time) : time.to_time rescue time
173
+ end
174
+ time = time.in_time_zone rescue nil if time
175
+ prop['#{attr_name}'] = time
176
+ end
177
+ EOV
178
+ evaluate_attribute_property_method attr_name, method_body, "#{attr_name}="
179
+ end
180
+
181
+ # Evaluate the definition for an attribute related method
182
+ 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
192
+ end
110
193
  end # ClassMethods
111
194
 
112
195
  module InstanceMethods
@@ -47,11 +47,6 @@ module Property
47
47
  super
48
48
  end
49
49
 
50
- def merge!(other_hash)
51
- @original_hash ||= self.dup
52
- super
53
- end
54
-
55
50
  def changed?
56
51
  !changes.empty?
57
52
  end
@@ -62,7 +57,6 @@ module Property
62
57
 
63
58
  def changes
64
59
  return {} unless @original_hash
65
- compact!
66
60
  changes = {}
67
61
 
68
62
  # look for updated value
@@ -8,10 +8,7 @@ module Property
8
8
  end
9
9
 
10
10
  def to_json(*args)
11
- {
12
- 'json_class' => self.class.name,
13
- 'data' => Hash[self]
14
- }.to_json(*args)
11
+ { 'json_class' => self.class.name, 'data' => Hash[self] }.to_json(*args)
15
12
  end
16
13
 
17
14
  def []=(key, value)
@@ -44,8 +41,9 @@ module Property
44
41
  errors = @owner.errors
45
42
  no_errors = true
46
43
 
47
- bad_keys = keys - column_names
48
- missing_keys = column_names - keys
44
+ bad_keys = keys - column_names
45
+ missing_keys = column_names - keys
46
+ keys_to_validate = keys - bad_keys
49
47
 
50
48
  bad_keys.each do |key|
51
49
  errors.add("#{key}", 'property is not declared')
@@ -58,21 +56,14 @@ module Property
58
56
  end
59
57
  end
60
58
 
61
- bad_keys.empty?
62
- end
59
+ keys_to_validate.each do |key|
60
+ columns[key].validate(self[key], errors)
61
+ end
63
62
 
64
- def compact!
65
- #keys.each do |key|
66
- # if self[key].nil?
67
- # delete(key)
68
- # end
69
- #end
63
+ bad_keys.empty?
70
64
  end
71
65
 
72
66
  private
73
- def write_attribute(key, value)
74
- end
75
-
76
67
  def columns
77
68
  @columns ||= @owner.class.property_columns
78
69
  end
data/property.gemspec ADDED
@@ -0,0 +1,81 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE DIRECTLY
3
+ # Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command
4
+ # -*- encoding: utf-8 -*-
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = %q{property}
8
+ s.version = "0.6.0"
9
+
10
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
+ s.authors = ["Renaud Kern", "Gaspard Bucher"]
12
+ s.date = %q{2010-02-11}
13
+ s.description = %q{Wrap model properties into a single database column and declare properties from within the model.}
14
+ s.email = %q{gaspard@teti.ch}
15
+ s.extra_rdoc_files = [
16
+ "README.rdoc"
17
+ ]
18
+ s.files = [
19
+ ".gitignore",
20
+ "History.txt",
21
+ "MIT-LICENSE",
22
+ "README.rdoc",
23
+ "Rakefile",
24
+ "generators/property/property_generator.rb",
25
+ "lib/property.rb",
26
+ "lib/property/attribute.rb",
27
+ "lib/property/column.rb",
28
+ "lib/property/declaration.rb",
29
+ "lib/property/dirty.rb",
30
+ "lib/property/properties.rb",
31
+ "lib/property/serialization/json.rb",
32
+ "lib/property/serialization/marshal.rb",
33
+ "lib/property/serialization/yaml.rb",
34
+ "property.gemspec",
35
+ "test/fixtures.rb",
36
+ "test/shoulda_macros/serialization.rb",
37
+ "test/test_helper.rb",
38
+ "test/unit/property/attribute_test.rb",
39
+ "test/unit/property/declaration_test.rb",
40
+ "test/unit/property/dirty_test.rb",
41
+ "test/unit/property/validation_test.rb",
42
+ "test/unit/serialization/json_test.rb",
43
+ "test/unit/serialization/marshal_test.rb",
44
+ "test/unit/serialization/yaml_test.rb"
45
+ ]
46
+ s.homepage = %q{http://zenadmin.org/635}
47
+ s.rdoc_options = ["--charset=UTF-8"]
48
+ s.require_paths = ["lib"]
49
+ s.rubyforge_project = %q{property}
50
+ s.rubygems_version = %q{1.3.5}
51
+ s.summary = %q{model properties wrap into a single database column}
52
+ s.test_files = [
53
+ "test/fixtures.rb",
54
+ "test/shoulda_macros/serialization.rb",
55
+ "test/test_helper.rb",
56
+ "test/unit/property/attribute_test.rb",
57
+ "test/unit/property/declaration_test.rb",
58
+ "test/unit/property/dirty_test.rb",
59
+ "test/unit/property/validation_test.rb",
60
+ "test/unit/serialization/json_test.rb",
61
+ "test/unit/serialization/marshal_test.rb",
62
+ "test/unit/serialization/yaml_test.rb"
63
+ ]
64
+
65
+ if s.respond_to? :specification_version then
66
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
67
+ s.specification_version = 3
68
+
69
+ if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
70
+ s.add_development_dependency(%q<shoulda>, [">= 0"])
71
+ s.add_runtime_dependency(%q<activerecord>, [">= 0"])
72
+ else
73
+ s.add_dependency(%q<shoulda>, [">= 0"])
74
+ s.add_dependency(%q<activerecord>, [">= 0"])
75
+ end
76
+ else
77
+ s.add_dependency(%q<shoulda>, [">= 0"])
78
+ s.add_dependency(%q<activerecord>, [">= 0"])
79
+ end
80
+ end
81
+
@@ -59,9 +59,18 @@ class AttributeTest < Test::Unit::TestCase
59
59
  end
60
60
  end
61
61
 
62
- should 'with merge! should merge new attributes' do
63
- subject.properties.merge!({'b'=>'bravo', 'c'=>'charlie'})
64
- assert_equal Hash['foo' => 'bar', 'b' => 'bravo', 'c' => 'charlie'], subject.properties
62
+ context 'with merge!' do
63
+ should 'merge new attributes' do
64
+ subject.properties.merge!({'b'=>'bravo', 'c'=>'charlie'})
65
+ assert_equal Hash['foo' => 'bar', 'b' => 'bravo', 'c' => 'charlie'], subject.properties
66
+ end
67
+ end
68
+
69
+ context 'with native ruby methods' do
70
+ should 'update current value' do
71
+ subject.foo = 'floppy disk'
72
+ assert_equal Hash['foo' => 'floppy disk'], subject.properties
73
+ end
65
74
  end
66
75
  end
67
76
 
@@ -95,6 +104,18 @@ class AttributeTest < Test::Unit::TestCase
95
104
  assert_equal Hash['foo'=>'bar', :tic=>:tac], subject.properties
96
105
  end
97
106
 
107
+ should 'be accessible with native methods' do
108
+ assert_equal 'bar', subject.foo
109
+ end
110
+
111
+ should 'respond true on predicate method' do
112
+ assert subject.foo?
113
+ end
114
+
115
+ should 'not respond true on predicate method if empty' do
116
+ assert !subject.comment?
117
+ end
118
+
98
119
  should 'be a kind of Hash' do
99
120
  assert_kind_of Hash, subject.properties
100
121
  end
@@ -64,6 +64,23 @@ class DeclarationTest < Test::Unit::TestCase
64
64
  assert_kind_of Property::Column, subject.property_columns['weapon']
65
65
  end
66
66
 
67
+ should 'create ruby accessors' do
68
+ subject.property.string('weapon')
69
+ assert subject.instance_methods.include?('weapon')
70
+ assert subject.instance_methods.include?('weapon=')
71
+ assert subject.instance_methods.include?('weapon?')
72
+ end
73
+
74
+ should 'not create accessors for illegal ruby names' do
75
+ bad_names = ['some.thing', 'puts("yo")', '/var/', 'hello darness']
76
+ assert_nothing_raised { subject.property.string bad_names }
77
+ bad_names.each do |bad_name|
78
+ assert !subject.instance_methods.include?(bad_name)
79
+ assert !subject.instance_methods.include?("#{bad_name}=")
80
+ assert !subject.instance_methods.include?("#{bad_name}?")
81
+ end
82
+ end
83
+
67
84
  should 'allow string columns' do
68
85
  subject.property.string('weapon')
69
86
  column = subject.property_columns['weapon']
@@ -72,6 +89,14 @@ class DeclarationTest < Test::Unit::TestCase
72
89
  assert_equal :string, column.type
73
90
  end
74
91
 
92
+ should 'treat symbol keys as strings' do
93
+ subject.property.string(:weapon)
94
+ column = subject.property_columns['weapon']
95
+ assert_equal 'weapon', column.name
96
+ assert_equal String, column.klass
97
+ assert_equal :string, column.type
98
+ end
99
+
75
100
  should 'allow integer columns' do
76
101
  subject.property.integer('indestructible')
77
102
  column = subject.property_columns['indestructible']
@@ -36,7 +36,8 @@ class DirtyTest < Test::Unit::TestCase
36
36
 
37
37
  context 'with changed properties' do
38
38
  setup do
39
- subject.properties = {'foo'=>'barre', 'tic'=>'taaac'}
39
+ subject.properties = {'foo'=>'barre'}
40
+ subject.properties.delete('tic')
40
41
  end
41
42
 
42
43
  should_behave_nice_after_save('barre')
@@ -58,11 +59,11 @@ class DirtyTest < Test::Unit::TestCase
58
59
  end
59
60
 
60
61
  should 'return property changes with changes' do
61
- assert_equal Hash['tic'=>['tac', 'taaac'], 'foo'=>['bar', 'barre']], subject.changes
62
+ assert_equal Hash['tic'=>['tac', nil], 'foo'=>['bar', 'barre']], subject.changes
62
63
  end
63
64
 
64
65
  should 'return property changes with properties.changes' do
65
- assert_equal Hash['tic'=>['tac', 'taaac'], 'foo'=>['bar', 'barre']], subject.properties.changes
66
+ assert_equal Hash['tic'=>['tac', nil], 'foo'=>['bar', 'barre']], subject.properties.changes
66
67
  end
67
68
 
68
69
  should 'return true on properties.key_changed?' do
@@ -152,6 +153,10 @@ class DirtyTest < Test::Unit::TestCase
152
153
  should 'return previous value on properties.key_was' do
153
154
  assert_equal 'bar', subject.properties.foo_was
154
155
  end
156
+
157
+ should 'raise NoMethodError on other missing methods' do
158
+ assert_raise(NoMethodError) { subject.properties.sleep }
159
+ end
155
160
  end
156
161
  end
157
162
  end
@@ -14,15 +14,15 @@ class ValidationTest < Test::Unit::TestCase
14
14
  subject { Pirate.create }
15
15
 
16
16
  context 'without a property column' do
17
- setup do
18
- assert !subject.update_attributes('honest' => 'man')
19
- end
20
-
21
- should 'render object invalid' do
22
- assert subject.invalid?
17
+ should 'raise ActiveRecord::UnknownAttributeError when using attributes=' do
18
+ assert_raise(ActiveRecord::UnknownAttributeError) do
19
+ subject.update_attributes('honest' => 'man')
20
+ end
23
21
  end
24
22
 
25
- should 'set an error message on the property' do
23
+ should 'set an error message on the property when set directly' do
24
+ subject.prop['honest'] = 'man'
25
+ assert !subject.save
26
26
  assert_contains subject.errors.full_messages, 'Honest property is not declared'
27
27
  assert_equal subject.errors['honest'], 'property is not declared'
28
28
  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.5.0
4
+ version: 0.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Renaud Kern
@@ -24,7 +24,7 @@ dependencies:
24
24
  version: "0"
25
25
  version:
26
26
  - !ruby/object:Gem::Dependency
27
- name: active_record
27
+ name: activerecord
28
28
  type: :runtime
29
29
  version_requirement:
30
30
  version_requirements: !ruby/object:Gem::Requirement
@@ -57,6 +57,7 @@ files:
57
57
  - lib/property/serialization/json.rb
58
58
  - lib/property/serialization/marshal.rb
59
59
  - lib/property/serialization/yaml.rb
60
+ - property.gemspec
60
61
  - test/fixtures.rb
61
62
  - test/shoulda_macros/serialization.rb
62
63
  - test/test_helper.rb