property 0.8.0 → 0.8.1

Sign up to get free protection for your applications and to get access to all the features.
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