property 0.5.0 → 0.6.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.
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