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 +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
|