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 +6 -0
- data/README.rdoc +39 -5
- data/lib/property.rb +1 -1
- data/lib/property/attribute.rb +38 -21
- data/lib/property/behavior.rb +54 -41
- data/lib/property/column.rb +1 -0
- data/lib/property/declaration.rb +2 -8
- data/property.gemspec +2 -2
- data/test/fixtures.rb +1 -0
- data/test/unit/property/behavior_test.rb +22 -1
- data/test/unit/property/declaration_test.rb +84 -3
- metadata +2 -2
data/History.txt
CHANGED
data/README.rdoc
CHANGED
@@ -1,8 +1,9 @@
|
|
1
|
-
==
|
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
|
-
@
|
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 @
|
81
|
+
@model.behave_like @picture
|
64
82
|
|
65
|
-
The model now has the picture's properties defined, with accessors like @model.camera
|
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
data/lib/property/attribute.rb
CHANGED
@@ -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
|
data/lib/property/behavior.rb
CHANGED
@@ -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
|
-
|
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 =
|
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
|
43
|
+
yield accessor_module
|
44
44
|
end
|
45
|
-
|
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]
|
data/lib/property/column.rb
CHANGED
data/lib/property/declaration.rb
CHANGED
@@ -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
|
-
|
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.
|
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
|
+
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
@@ -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
|
-
|
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.
|
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-
|
13
|
+
date: 2010-02-14 00:00:00 +01:00
|
14
14
|
default_executable:
|
15
15
|
dependencies:
|
16
16
|
- !ruby/object:Gem::Dependency
|