property 0.8.0 → 0.8.1

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/History.txt CHANGED
@@ -1,3 +1,9 @@
1
+ == 0.8.1 2010-02-14
2
+
3
+ * 2 major enhancement
4
+ * enabled behavior method definitions
5
+ * enabled external storage with 'store_properties_in' method
6
+
1
7
  == 0.8.0 2010-02-11
2
8
 
3
9
  * 3 major enhancements
data/README.rdoc CHANGED
@@ -1,8 +1,9 @@
1
- == DESCRIPTION:
1
+ == Property
2
2
 
3
3
  Wrap model properties into a single database column and declare properties from within the model.
4
4
 
5
5
  website: http://zenadmin.org/635
6
+
6
7
  license: MIT
7
8
 
8
9
  == Status: Beta
@@ -51,20 +52,53 @@ And set them with:
51
52
  Properties would not be really fun if you could not add new properties to your instances depending
52
53
  on some flags. First define the behaviors:
53
54
 
54
- @a_picture = Property::Behavior.new do |p|
55
+ @picture = Property::Behavior.new do |p|
55
56
  p.integer :width, :default => :get_width
56
57
  p.integer :height, :default => :get_height
57
58
  p.string 'camera'
58
59
  p.string 'location'
60
+
61
+ p.actions do
62
+ # Define new methods to insert into model
63
+
64
+ def get_width
65
+ image.width
66
+ end
67
+
68
+ def get_height
69
+ image.height
70
+ end
71
+
72
+ def image
73
+ raise 'Missing file' unless @file
74
+ @image ||= ImageBuilder(@file)
75
+ end
76
+ end
59
77
  end
60
78
 
61
79
  And then, either when creating new pictures or updating them, you need to include the behavior:
62
80
 
63
- @model.behave_like @a_picture
81
+ @model.behave_like @picture
64
82
 
65
- The model now has the picture's properties defined, with accessors like @model.camera and default
66
- values will be fetched on save.
83
+ The model now has the picture's properties defined, with accessors like @model.camera, methods like
84
+ @model.image, get_with, etc and default values will be fetched on save.
67
85
 
68
86
  Note that you do not need to include a behavior just to read the data as long as you use the 'prop'
69
87
  accessor.
70
88
 
89
+ == External storage
90
+
91
+ You might need to define properties in a model but store them in another model (versioning). In this
92
+ case you can simply use 'store_properties_in' class method:
93
+
94
+ class Contact < ActiveRecord::Base
95
+ include Property
96
+ store_properties_in :version
97
+ property do |p|
98
+ p.string 'name', 'first_name'
99
+ p.string 'childhood', :default => 'happy'
100
+ end
101
+ end
102
+
103
+ Doing so will not touch the storage class. All property definitions, validations and method
104
+ definitions are executed on the 'Contact' class.
data/lib/property.rb CHANGED
@@ -9,7 +9,7 @@ require 'property/serialization/json'
9
9
  require 'property/core_ext/time'
10
10
 
11
11
  module Property
12
- VERSION = '0.8.0'
12
+ VERSION = '0.8.1'
13
13
 
14
14
  def self.included(base)
15
15
  base.class_eval do
@@ -13,18 +13,56 @@ module Property
13
13
  module Attribute
14
14
 
15
15
  def self.included(base)
16
+ base.extend ClassMethods
17
+
16
18
  base.class_eval do
17
19
  include InstanceMethods
18
20
  include Serialization::JSON
19
21
  include Declaration
20
22
  include Dirty
21
23
 
24
+ store_properties_in self
25
+
22
26
  before_save :dump_properties
23
27
 
24
28
  alias_method_chain :attributes=, :properties
25
29
  end
26
30
  end
27
31
 
32
+ module ClassMethods
33
+ def store_properties_in(accessor)
34
+ if accessor.nil? || accessor == self
35
+ accessor = ''
36
+ else
37
+ accessor = "#{accessor}."
38
+ end
39
+ load_and_dump_methods =<<-EOF
40
+ private
41
+ def load_properties
42
+ raw_data = #{accessor}read_attribute('properties')
43
+ prop = raw_data ? decode_properties(raw_data) : Properties.new
44
+ # We need to set the owner to access property definitions and enable
45
+ # type casting on write.
46
+ prop.owner = self
47
+ prop
48
+ end
49
+
50
+ def dump_properties
51
+ if @properties
52
+ if !@properties.empty?
53
+ #{accessor}write_attribute('properties', encode_properties(@properties))
54
+ else
55
+ #{accessor}write_attribute('properties', nil)
56
+ end
57
+ end
58
+ @properties.clear_changes!
59
+ true
60
+ end
61
+ EOF
62
+ class_eval(load_and_dump_methods, __FILE__, __LINE__)
63
+ end
64
+ end
65
+
28
66
  module InstanceMethods
29
67
  def properties
30
68
  @properties ||= load_properties
@@ -62,27 +100,6 @@ module Property
62
100
  self.properties = properties
63
101
  self.attributes_without_properties = attributes
64
102
  end
65
-
66
- def load_properties
67
- raw_data = read_attribute('properties')
68
- prop = raw_data ? decode_properties(raw_data) : Properties.new
69
- # We need to set the owner to access property definitions and enable
70
- # type casting on write.
71
- prop.owner = self
72
- prop
73
- end
74
-
75
- def dump_properties
76
- if @properties
77
- if !@properties.empty?
78
- write_attribute('properties', encode_properties(@properties))
79
- else
80
- write_attribute('properties', nil)
81
- end
82
- end
83
- @properties.clear_changes!
84
- true
85
- end
86
103
  end # InstanceMethods
87
104
  end # Attribute
88
105
  end # Property
@@ -5,10 +5,10 @@ module Property
5
5
  class Behavior
6
6
  attr_accessor :name, :included, :accessor_module
7
7
 
8
- def self.new(name)
8
+ def self.new(name, &block)
9
9
  obj = super
10
10
  if block_given?
11
- yield obj
11
+ obj.property(&block)
12
12
  end
13
13
  obj
14
14
  end
@@ -17,7 +17,7 @@ module Property
17
17
  def initialize(name)
18
18
  @name = name
19
19
  @included_in_schemas = []
20
- @accessor_module = Module.new
20
+ @accessor_module = build_accessor_module
21
21
  end
22
22
 
23
23
  # List all property definitiosn for the current behavior
@@ -40,33 +40,9 @@ module Property
40
40
  # end
41
41
  def property
42
42
  if block_given?
43
- yield self
43
+ yield accessor_module
44
44
  end
45
- self
46
- end
47
-
48
- # def string(*args)
49
- # options = args.extract_options!
50
- # column_names = args
51
- # default = options.delete(:default)
52
- # column_names.each { |name| column(name, default, 'string', options) }
53
- # end
54
- %w( string text integer float decimal datetime timestamp time date binary boolean ).each do |column_type|
55
- class_eval <<-EOV
56
- def #{column_type}(*args)
57
- options = args.extract_options!
58
- column_names = args
59
- default = options.delete(:default)
60
- column_names.each { |name| add_column(Property::Column.new(name, default, '#{column_type}', options)) }
61
- end
62
- EOV
63
- end
64
-
65
- # This is used to serialize a non-native DB type. Use:
66
- # p.serialize 'pet', Dog
67
- def serialize(name, klass, options = {})
68
- Property.validate_property_class(klass)
69
- add_column(Property::Column.new(name, nil, klass, options))
45
+ accessor_module
70
46
  end
71
47
 
72
48
  # @internal
@@ -75,7 +51,56 @@ module Property
75
51
  @included_in_schemas << schema
76
52
  end
77
53
 
54
+ # @internal
55
+ def add_column(column)
56
+ name = column.name
57
+
58
+ if columns[name]
59
+ raise TypeError.new("Property '#{name}' is already defined.")
60
+ else
61
+ verify_not_defined_in_schemas_using_this_behavior(name)
62
+ define_property_methods(column) if column.should_create_accessors?
63
+ columns[column.name] = column
64
+ end
65
+ end
66
+
78
67
  private
68
+ def build_accessor_module
69
+ accessor_module = Module.new
70
+ accessor_module.class_eval do
71
+ class << self
72
+ attr_accessor :behavior
73
+
74
+ # def string(*args)
75
+ # options = args.extract_options!
76
+ # column_names = args
77
+ # default = options.delete(:default)
78
+ # column_names.each { |name| column(name, default, 'string', options) }
79
+ # end
80
+ %w( string text integer float decimal datetime timestamp time date binary boolean ).each do |column_type|
81
+ class_eval <<-EOV
82
+ def #{column_type}(*args)
83
+ options = args.extract_options!
84
+ column_names = args
85
+ default = options.delete(:default)
86
+ column_names.each { |name| behavior.add_column(Property::Column.new(name, default, '#{column_type}', options)) }
87
+ end
88
+ EOV
89
+ end
90
+
91
+ # This is used to serialize a non-native DB type. Use:
92
+ # p.serialize 'pet', Dog
93
+ def serialize(name, klass, options = {})
94
+ Property.validate_property_class(klass)
95
+ behavior.add_column(Property::Column.new(name, nil, klass, options))
96
+ end
97
+
98
+ alias actions class_eval
99
+ end
100
+ end
101
+ accessor_module.behavior = self
102
+ accessor_module
103
+ end
79
104
 
80
105
  def define_property_methods(column)
81
106
  name = column.name
@@ -144,18 +169,6 @@ module Property
144
169
  accessor_module.class_eval(method_definition, __FILE__, __LINE__)
145
170
  end
146
171
 
147
- def add_column(column)
148
- name = column.name
149
-
150
- if columns[name]
151
- raise TypeError.new("Property '#{name}' is already defined.")
152
- else
153
- verify_not_defined_in_schemas_using_this_behavior(name)
154
- define_property_methods(column) if column.should_create_accessors?
155
- columns[column.name] = column
156
- end
157
- end
158
-
159
172
  def verify_not_defined_in_schemas_using_this_behavior(name)
160
173
  @included_in_schemas.each do |schema|
161
174
  if schema.columns[name]
@@ -32,6 +32,7 @@ module Property
32
32
  end
33
33
 
34
34
  def default_for(owner)
35
+ default = self.default
35
36
  if default.kind_of?(Proc)
36
37
  default.call
37
38
  elsif default.kind_of?(Symbol)
@@ -46,14 +46,8 @@ module Property
46
46
  # property do |p|
47
47
  # p.string 'phone', 'name', :default => ''
48
48
  # end
49
- def property
50
- setter = schema.behavior
51
-
52
- if block_given?
53
- yield setter
54
- end
55
-
56
- setter
49
+ def property(&block)
50
+ schema.behavior.property(&block)
57
51
  end
58
52
  end # ClassMethods
59
53
 
data/property.gemspec CHANGED
@@ -5,11 +5,11 @@
5
5
 
6
6
  Gem::Specification.new do |s|
7
7
  s.name = %q{property}
8
- s.version = "0.8.0"
8
+ s.version = "0.8.1"
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"]
12
- s.date = %q{2010-02-12}
12
+ s.date = %q{2010-02-14}
13
13
  s.description = %q{Wrap model properties into a single database column and declare properties from within the model.}
14
14
  s.email = %q{gaspard@teti.ch}
15
15
  s.extra_rdoc_files = [
data/test/fixtures.rb CHANGED
@@ -56,6 +56,7 @@ begin
56
56
  end
57
57
 
58
58
  create_table "versions" do |t|
59
+ t.integer 'employee_id'
59
60
  t.string "properties"
60
61
  t.string "title"
61
62
  t.string "comment"
@@ -62,7 +62,13 @@ class BehaviorTest < Test::Unit::TestCase
62
62
  context 'Adding a behavior' do
63
63
  setup do
64
64
  @poet = Property::Behavior.new('Poet') do |p|
65
- p.string 'poem'
65
+ p.string 'poem', :default => :muse
66
+
67
+ p.actions do
68
+ def muse
69
+ 'I am your muse'
70
+ end
71
+ end
66
72
  end
67
73
  end
68
74
 
@@ -99,6 +105,21 @@ class BehaviorTest < Test::Unit::TestCase
99
105
 
100
106
  assert_nothing_raised { subject.poem = 'Poe'}
101
107
  end
108
+
109
+ should 'add behavior methods to child' do
110
+ subject = @klass.new
111
+ assert_raises(NoMethodError) { subject.muse }
112
+ @parent.behave_like @poet
113
+
114
+ assert_nothing_raised { subject.muse }
115
+ end
116
+
117
+ should 'use behavior methos for defaults' do
118
+ subject = @klass.new
119
+ @parent.behave_like @poet
120
+ assert subject.save
121
+ assert_equal 'I am your muse', subject.poem
122
+ end
102
123
  end
103
124
 
104
125
  context 'to a parent class' do
@@ -92,6 +92,14 @@ class DeclarationTest < Test::Unit::TestCase
92
92
  assert_equal :string, column.type
93
93
  end
94
94
 
95
+ should 'allow text columns' do
96
+ subject.property.text('history')
97
+ column = subject.schema.columns['history']
98
+ assert_equal 'history', column.name
99
+ assert_equal String, column.klass
100
+ assert_equal :text, column.type
101
+ end
102
+
95
103
  should 'treat symbol keys as strings' do
96
104
  subject.property.string(:weapon)
97
105
  column = subject.schema.columns['weapon']
@@ -123,7 +131,7 @@ class DeclarationTest < Test::Unit::TestCase
123
131
  assert_equal Time, column.klass
124
132
  assert_equal :datetime, column.type
125
133
  end
126
-
134
+
127
135
  should 'allow serialized columns' do
128
136
  Dog = Struct.new(:name, :toy) do
129
137
  def self.json_create(data)
@@ -135,7 +143,7 @@ class DeclarationTest < Test::Unit::TestCase
135
143
  }.to_json(*args)
136
144
  end
137
145
  end
138
-
146
+
139
147
  subject.property.serialize('pet', Dog)
140
148
  column = subject.schema.columns['pet']
141
149
  assert_equal 'pet', column.name
@@ -210,4 +218,77 @@ class DeclarationTest < Test::Unit::TestCase
210
218
  assert_raise(TypeError) { subject.behave_like 'me' }
211
219
  end
212
220
  end
213
- end
221
+
222
+ context 'A class with external storage' do
223
+ class Version < ActiveRecord::Base
224
+ belongs_to :contact, :class_name => 'DeclarationTest::Contact',
225
+ :foreign_key => 'employee_id'
226
+ end
227
+
228
+ Contact = Class.new(ActiveRecord::Base) do
229
+ set_table_name :employees
230
+ has_many :versions, :class_name => 'DeclarationTest::Version'
231
+
232
+ include Property
233
+ store_properties_in :version
234
+
235
+ property do |p|
236
+ p.string 'first_name'
237
+ p.string 'famous', :default => :get_is_famous
238
+ p.integer 'age'
239
+
240
+ p.actions do
241
+ def get_is_famous
242
+ 'no'
243
+ end
244
+ end
245
+ end
246
+
247
+ def version
248
+ @version ||= begin
249
+ if new_record?
250
+ versions.build
251
+ else
252
+ Version.first(:conditions => ['employee_id = ?', self.id]) || versions.build
253
+ end
254
+ end
255
+ end
256
+ end
257
+
258
+ setup do
259
+ @contact = Contact.create('first_name' => 'Martin')
260
+ end
261
+
262
+ subject { @contact }
263
+
264
+ should 'store properties in the given instance' do
265
+ assert_equal Hash["famous"=>"no", "first_name"=>"Martin"], JSON.parse(subject.version['properties'])
266
+ end
267
+
268
+ should 'keep a properties cache in the the main instance' do
269
+ assert_equal Hash["famous"=>"no", "first_name"=>"Martin"], subject.instance_variable_get(:@properties)
270
+ end
271
+
272
+ should 'behave as if storage was internal' do
273
+ subject.first_name = 'Hannah'
274
+ assert_equal 'no', subject.famous
275
+ assert_equal Hash["first_name"=>["Martin", "Hannah"]], subject.changes
276
+ end
277
+ end
278
+ end
279
+
280
+
281
+
282
+
283
+
284
+
285
+
286
+
287
+
288
+
289
+
290
+
291
+
292
+
293
+
294
+
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.8.0
4
+ version: 0.8.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Renaud Kern
@@ -10,7 +10,7 @@ autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
12
 
13
- date: 2010-02-12 00:00:00 +01:00
13
+ date: 2010-02-14 00:00:00 +01:00
14
14
  default_executable:
15
15
  dependencies:
16
16
  - !ruby/object:Gem::Dependency