property 0.9.1 → 1.0.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.
@@ -0,0 +1,101 @@
1
+ require 'property/role_module'
2
+ require 'property/stored_column'
3
+
4
+ module Property
5
+ # This module lets you use a custom class to store a set of property definitions inside
6
+ # the database. For the rest, this class behaves just like Role.
7
+ #
8
+ # Once this module is included, you need to set the has_many association to the class that
9
+ # contains the columns definitions with something like:
10
+ #
11
+ # has_many :stored_columns, :class_name => NameOfColumnsClass
12
+ module StoredRole
13
+ include RoleModule
14
+
15
+ module ClassMethods
16
+ def stored_columns_class(columns_class)
17
+ has_many :stored_columns, :class_name => columns_class
18
+ end
19
+ end
20
+
21
+ def self.included(base)
22
+ base.class_eval do
23
+ after_save :update_columns
24
+ validates_presence_of :name
25
+ extend ClassMethods
26
+
27
+ def self.new(arg, &block)
28
+ unless arg.kind_of?(Hash)
29
+ arg = {:name => arg}
30
+ end
31
+
32
+ if block_given?
33
+ obj = super(arg) do
34
+ # Dummy block to hide our special property declaration block
35
+ end
36
+
37
+ obj.property(&block)
38
+ else
39
+ obj = super(arg)
40
+ end
41
+
42
+ obj
43
+ end
44
+
45
+ # Initialize a new role with the given name
46
+ def initialize(*args)
47
+ super
48
+ initialize_role_module
49
+ end
50
+ end
51
+ end # included
52
+
53
+ # List all property definitions for the current role
54
+ def columns
55
+ load_columns_from_db unless @columns_from_db_loaded
56
+ super
57
+ end
58
+
59
+ def property
60
+ initialize_role_module unless @accessor_module
61
+ super
62
+ end
63
+
64
+
65
+ private
66
+ def load_columns_from_db
67
+ initialize_role_module
68
+ @columns_from_db_loaded = true
69
+ @original_columns = {}
70
+ stored_columns.each do |column|
71
+ @original_columns[column.name] = column
72
+ add_column(Property::Column.new(column.name, column.default, column.ptype, column.options))
73
+ end
74
+ end
75
+
76
+ def update_columns
77
+ @original_columns ||= {}
78
+ stored_column_names = @original_columns.keys
79
+ defined_column_names = self.column_names
80
+
81
+ new_columns = defined_column_names - stored_column_names
82
+ updated_columns = defined_column_names & stored_column_names
83
+ # Not needed: there is no way to remove a property right now
84
+ # deleted_columns = stored_column_names - defined_column_names
85
+
86
+ new_columns.each do |name|
87
+ ActiveRecord::Base.logger.warn "Creating #{name} column"
88
+ stored_columns.create(:name => name, :ptype => columns[name].type.to_s)
89
+ end
90
+
91
+ updated_columns.each do |name|
92
+ @original_columns[name].update_attributes(:name => name, :ptype => columns[name].type.to_s)
93
+ end
94
+
95
+ # Not needed: there is no way to remove a property right now
96
+ # deleted_columns.each do |name|
97
+ # @original_columns[name].destroy!
98
+ # end
99
+ end
100
+ end
101
+ end
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.9.1"
8
+ s.version = "1.0.0"
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-03-20}
12
+ s.date = %q{2010-05-27}
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 = [
@@ -24,30 +24,41 @@ Gem::Specification.new do |s|
24
24
  "generators/property/property_generator.rb",
25
25
  "lib/property.rb",
26
26
  "lib/property/attribute.rb",
27
- "lib/property/behavior.rb",
27
+ "lib/property/base.rb",
28
28
  "lib/property/column.rb",
29
29
  "lib/property/core_ext/time.rb",
30
30
  "lib/property/db.rb",
31
31
  "lib/property/declaration.rb",
32
32
  "lib/property/dirty.rb",
33
+ "lib/property/error.rb",
33
34
  "lib/property/index.rb",
34
35
  "lib/property/properties.rb",
36
+ "lib/property/redefined_method_error.rb",
37
+ "lib/property/redefined_property_error.rb",
38
+ "lib/property/role.rb",
39
+ "lib/property/role_module.rb",
35
40
  "lib/property/schema.rb",
36
41
  "lib/property/serialization/json.rb",
37
42
  "lib/property/serialization/marshal.rb",
38
43
  "lib/property/serialization/yaml.rb",
44
+ "lib/property/stored_column.rb",
45
+ "lib/property/stored_role.rb",
39
46
  "property.gemspec",
40
47
  "test/fixtures.rb",
48
+ "test/shoulda_macros/index.rb",
49
+ "test/shoulda_macros/role.rb",
41
50
  "test/shoulda_macros/serialization.rb",
42
51
  "test/test_helper.rb",
43
52
  "test/unit/property/attribute_test.rb",
44
- "test/unit/property/behavior_test.rb",
53
+ "test/unit/property/base_test.rb",
45
54
  "test/unit/property/declaration_test.rb",
46
55
  "test/unit/property/dirty_test.rb",
47
56
  "test/unit/property/index_complex_test.rb",
48
57
  "test/unit/property/index_custom_test.rb",
49
58
  "test/unit/property/index_foreign_test.rb",
50
59
  "test/unit/property/index_simple_test.rb",
60
+ "test/unit/property/role_test.rb",
61
+ "test/unit/property/stored_role_test.rb",
51
62
  "test/unit/property/validation_test.rb",
52
63
  "test/unit/serialization/json_test.rb",
53
64
  "test/unit/serialization/marshal_test.rb",
@@ -61,16 +72,20 @@ Gem::Specification.new do |s|
61
72
  s.summary = %q{model properties wrap into a single database column}
62
73
  s.test_files = [
63
74
  "test/fixtures.rb",
75
+ "test/shoulda_macros/index.rb",
76
+ "test/shoulda_macros/role.rb",
64
77
  "test/shoulda_macros/serialization.rb",
65
78
  "test/test_helper.rb",
66
79
  "test/unit/property/attribute_test.rb",
67
- "test/unit/property/behavior_test.rb",
80
+ "test/unit/property/base_test.rb",
68
81
  "test/unit/property/declaration_test.rb",
69
82
  "test/unit/property/dirty_test.rb",
70
83
  "test/unit/property/index_complex_test.rb",
71
84
  "test/unit/property/index_custom_test.rb",
72
85
  "test/unit/property/index_foreign_test.rb",
73
86
  "test/unit/property/index_simple_test.rb",
87
+ "test/unit/property/role_test.rb",
88
+ "test/unit/property/stored_role_test.rb",
74
89
  "test/unit/property/validation_test.rb",
75
90
  "test/unit/serialization/json_test.rb",
76
91
  "test/unit/serialization/marshal_test.rb",
data/test/fixtures.rb CHANGED
@@ -4,6 +4,9 @@ class Employee < ActiveRecord::Base
4
4
  property.string 'first_name', :default => '', :index => true
5
5
  property.string 'last_name', :default => '', :index => true
6
6
  property.float 'age'
7
+
8
+ def method_in_parent
9
+ end
7
10
  end
8
11
 
9
12
  class Developer < Employee
@@ -78,6 +81,23 @@ begin
78
81
  t.string 'value'
79
82
  end
80
83
 
84
+ # multilingual index strings in employees
85
+ create_table 'i_ml_string_employees' do |t|
86
+ t.integer 'employee_id'
87
+ t.integer 'version_id'
88
+ t.string 'lang'
89
+ t.integer 'site_id'
90
+ t.string 'key'
91
+ t.string 'value'
92
+ end
93
+
94
+ # index strings in employees
95
+ create_table 'i_special_employees' do |t|
96
+ t.integer 'employee_id'
97
+ t.string 'key'
98
+ t.string 'value'
99
+ end
100
+
81
101
  # index integer in employees
82
102
  create_table 'i_integer_employees' do |t|
83
103
  t.integer 'employee_id'
@@ -99,11 +119,29 @@ begin
99
119
  t.string 'name'
100
120
  t.string 'other_name'
101
121
  end
122
+
123
+ # Database stored role
124
+ create_table 'roles' do |t|
125
+ t.integer 'id'
126
+ t.string 'name'
127
+ end
128
+
129
+ create_table 'columns' do |t|
130
+ t.integer 'id'
131
+ t.integer 'role_id'
132
+ t.string 'name'
133
+ # Property Type
134
+ t.string 'ptype'
135
+ # Indexed (we store an integer so that we can have multiple index types)
136
+ t.string 'index'
137
+ end
102
138
  end
103
139
  end
104
140
 
105
141
  ActiveRecord::Base.establish_connection(:adapter=>'sqlite3', :database=>':memory:')
106
- # ActiveRecord::Base.logger = Logger.new(STDOUT)
142
+ log_path = Pathname(__FILE__).dirname + '../log/test.log'
143
+ Dir.mkdir(log_path.dirname) unless File.exist?(log_path.dirname)
144
+ ActiveRecord::Base.logger = Logger.new(File.open(log_path, 'wb'))
107
145
  ActiveRecord::Migration.verbose = false
108
146
  #PropertyMigration.migrate(:down)
109
147
  PropertyMigration.migrate(:up)
@@ -0,0 +1,95 @@
1
+ module IndexMacros
2
+ class IndexedStringEmp < ActiveRecord::Base
3
+ set_table_name :i_string_employees
4
+ end
5
+
6
+ class MLIndexedStringEmp < ActiveRecord::Base
7
+ set_table_name :i_ml_string_employees
8
+ end
9
+
10
+ class IndexedIntegerEmp < ActiveRecord::Base
11
+ set_table_name :i_integer_employees
12
+ end
13
+
14
+ # Simple class
15
+ class Employee < ActiveRecord::Base
16
+ include Property
17
+
18
+ def index_reader(group_name)
19
+ if group_name.to_s == 'ml_string'
20
+ super.merge(:with => {'lang' => ['en', 'fr'], 'site_id' => '123'})
21
+ else
22
+ super
23
+ end
24
+ end
25
+ end
26
+ end
27
+
28
+ class Test::Unit::TestCase
29
+
30
+ def self.should_maintain_indices
31
+
32
+ context "assigned to an instance of Dummy" do
33
+ subject do
34
+ dummy = IndexMacros::Employee.new
35
+ dummy.has_role @poet
36
+ dummy
37
+ end
38
+
39
+ should 'use index_reader method' do
40
+ assert_equal Hash[:with=>{'lang'=>['en', 'fr'], 'site_id'=>'123'}, "employee_id"=>nil], subject.index_reader(:ml_string)
41
+ end
42
+
43
+ should 'create multilingual string indices on save' do
44
+ assert_difference('IndexMacros::MLIndexedStringEmp.count', 2) do
45
+ subject.poem = 'Hyperions Schicksalslied'
46
+ subject.save
47
+ end
48
+ end
49
+
50
+ should 'create integer indices on save' do
51
+ assert_difference('IndexMacros::IndexedIntegerEmp.count', 1) do
52
+ subject.year = 1770
53
+ subject.save
54
+ end
55
+ end
56
+
57
+ should 'not create blank indices on save' do
58
+ assert_difference('IndexMacros::IndexedStringEmp.count', 0) do
59
+ assert_difference('IndexMacros::IndexedIntegerEmp.count', 0) do
60
+ subject.save
61
+ end
62
+ end
63
+ end
64
+ end # An instance of Person
65
+ end
66
+
67
+ def self.should_not_maintain_indices
68
+
69
+ context "assigned to an instance of Dummy" do
70
+ subject do
71
+ dummy = IndexMacros::Employee.new
72
+ dummy.has_role @poet
73
+ dummy
74
+ end
75
+
76
+ should 'not create indices on save' do
77
+ assert_difference('IndexMacros::IndexedStringEmp.count', 0) do
78
+ assert_difference('IndexMacros::IndexedIntegerEmp.count', 0) do
79
+ subject.year = 1770
80
+ subject.poem = 'Hyperions Schicksalslied'
81
+ subject.save
82
+ end
83
+ end
84
+ end
85
+
86
+ should 'not create blank indices on save' do
87
+ assert_difference('IndexMacros::IndexedStringEmp.count', 0) do
88
+ assert_difference('IndexMacros::IndexedIntegerEmp.count', 0) do
89
+ subject.save
90
+ end
91
+ end
92
+ end
93
+ end # An instance of Person
94
+ end
95
+ end
@@ -0,0 +1,237 @@
1
+ class Test::Unit::TestCase
2
+ def self.should_store_property_definitions(klass)
3
+ subject { klass.new('Foobar') }
4
+
5
+ should 'allow string columns' do
6
+ subject.property.string('weapon')
7
+ column = subject.columns['weapon']
8
+ assert_equal 'weapon', column.name
9
+ assert_equal String, column.klass
10
+ assert_equal :string, column.type
11
+ end
12
+
13
+ should 'treat symbol keys as strings' do
14
+ subject.property.string(:weapon)
15
+ column = subject.columns['weapon']
16
+ assert_equal 'weapon', column.name
17
+ assert_equal String, column.klass
18
+ assert_equal :string, column.type
19
+ end
20
+
21
+ should 'allow integer columns' do
22
+ subject.property.integer('indestructible')
23
+ column = subject.columns['indestructible']
24
+ assert_equal 'indestructible', column.name
25
+ assert_equal Fixnum, column.klass
26
+ assert_equal :integer, column.type
27
+ end
28
+
29
+ should 'allow float columns' do
30
+ subject.property.float('boat')
31
+ column = subject.columns['boat']
32
+ assert_equal 'boat', column.name
33
+ assert_equal Float, column.klass
34
+ assert_equal :float, column.type
35
+ end
36
+
37
+ should 'allow datetime columns' do
38
+ subject.property.datetime('time_weapon')
39
+ column = subject.columns['time_weapon']
40
+ assert_equal 'time_weapon', column.name
41
+ assert_equal Time, column.klass
42
+ assert_equal :datetime, column.type
43
+ end
44
+
45
+ should 'allow default value option' do
46
+ subject.property.integer('force', :default => 10)
47
+ column = subject.columns['force']
48
+ assert_equal 10, column.default
49
+ end
50
+
51
+ should 'allow index option' do
52
+ subject.property.string('rolodex', :index => true)
53
+ column = subject.columns['rolodex']
54
+ assert column.indexed?
55
+ end
56
+
57
+ should 'return a list of indices on indices' do
58
+ subject.property.string('rolodex', :index => true)
59
+ subject.property.integer('foobar', :index => true)
60
+ assert_equal %w{integer string}, subject.indices.map {|i| i[0].to_s }.sort
61
+ end
62
+
63
+ context 'created with a Hash' do
64
+ subject { klass.new(:name => 'Foobar') }
65
+
66
+ should 'set name' do
67
+ assert_equal 'Foobar', subject.name
68
+ end
69
+ end
70
+
71
+ context 'created with a String Hash' do
72
+ subject { klass.new('name' => 'Foobar') }
73
+
74
+ should 'set name' do
75
+ assert_equal 'Foobar', subject.name
76
+ end
77
+ end
78
+ end # should_store_property_definitions
79
+
80
+ def self.should_insert_properties_on_has_role_poet
81
+ context 'added' do
82
+
83
+ context 'to a parent class' do
84
+ setup do
85
+ @parent = Class.new(ActiveRecord::Base) do
86
+ set_table_name :dummies
87
+ include Property
88
+ property.string 'name'
89
+ end
90
+
91
+ @klass = Class.new(@parent)
92
+ end
93
+
94
+ should 'propagate definitions to child' do
95
+ @parent.has_role @poet
96
+ assert_equal %w{name poem year}, @klass.schema.column_names.sort
97
+ end
98
+
99
+ should 'return true on has_role?' do
100
+ @parent.has_role @poet
101
+ assert @klass.has_role?(@poet)
102
+ end
103
+
104
+ should 'raise an exception if class contains same definitions' do
105
+ @parent.property.string 'poem'
106
+ assert_raise(Property::RedefinedPropertyError) { @parent.has_role @poet }
107
+ end
108
+
109
+ should 'not raise an exception on double inclusion' do
110
+ @parent.has_role @poet
111
+ assert_nothing_raised { @parent.has_role @poet }
112
+ end
113
+
114
+ should 'add accessor methods to child' do
115
+ subject = @klass.new
116
+ assert_raises(NoMethodError) { subject.poem = 'Poe'}
117
+ @parent.has_role @poet
118
+
119
+ assert_nothing_raised { subject.poem = 'Poe'}
120
+ end
121
+ end
122
+
123
+ context 'to a class' do
124
+ setup do
125
+ @klass = Class.new(ActiveRecord::Base) do
126
+ set_table_name :dummies
127
+ include Property
128
+ property.string 'name'
129
+ end
130
+ end
131
+
132
+ should 'insert definitions' do
133
+ @klass.has_role @poet
134
+ assert_equal %w{name poem year}, @klass.schema.column_names.sort
135
+ end
136
+
137
+ should 'return true on class has_role?' do
138
+ @klass.has_role @poet
139
+ assert @klass.has_role?(@poet)
140
+ end
141
+
142
+ end
143
+
144
+ context 'to an instance' do
145
+ subject { Developer.new }
146
+
147
+ setup do
148
+ subject.has_role @poet
149
+ end
150
+
151
+ should 'merge property definitions' do
152
+ assert_equal %w{age first_name language last_name poem year}, subject.schema.column_names.sort
153
+ end
154
+ end
155
+ end
156
+ end # should_insert_properties_on_has_role
157
+
158
+ def self.should_add_role_methods
159
+ context 'added' do
160
+ context 'to a parent class' do
161
+ setup do
162
+ @parent = Class.new(ActiveRecord::Base) do
163
+ set_table_name :dummies
164
+ include Property
165
+ property.string 'name'
166
+ end
167
+
168
+ @klass = Class.new(@parent)
169
+ end
170
+
171
+ should 'add role methods to child' do
172
+ subject = @klass.new
173
+ assert_raises(NoMethodError) { subject.muse }
174
+ @parent.has_role @poet
175
+
176
+ assert_nothing_raised { subject.muse }
177
+ end
178
+
179
+ should 'use role methods for defaults' do
180
+ subject = @klass.new
181
+ @parent.has_role @poet
182
+ assert subject.save
183
+ assert_equal 'I am your muse', subject.poem
184
+ end
185
+ end
186
+ end
187
+ end # should_add_role_methods
188
+
189
+ def self.should_take_part_in_used_list(has_defaults = true)
190
+
191
+ context 'added' do
192
+ context 'to a class' do
193
+ setup do
194
+ @klass = Class.new(ActiveRecord::Base) do
195
+ set_table_name :dummies
196
+ include Property
197
+ property.string 'name'
198
+ end
199
+
200
+ @klass.has_role @poet
201
+ end
202
+
203
+ subject do
204
+ @klass.new
205
+ end
206
+
207
+ should 'return true on instance has_role?' do
208
+ assert subject.has_role?(@poet)
209
+ end
210
+
211
+ should 'not return role without corresponding attributes' do
212
+ subject.attributes = {'name' => 'hello'}
213
+ assert_equal [@klass.schema.role], subject.used_roles
214
+ end
215
+
216
+ should 'return role with corresponding attributes' do
217
+ subject.attributes = {'poem' => 'hello'}
218
+ roles = @poet.kind_of?(Class) ? @poet.schema.roles : [@poet]
219
+ assert_equal roles, subject.used_roles
220
+ end
221
+
222
+ should 'not return role only with blank attributes' do
223
+ subject.attributes = {'name' => ''}
224
+ assert_equal [], subject.used_roles
225
+ end
226
+
227
+ if has_defaults
228
+ should 'return role with default attributes after validate' do
229
+ subject.valid?
230
+ roles = @poet.kind_of?(Class) ? @poet.schema.roles : [@poet]
231
+ assert_equal roles, subject.used_roles
232
+ end
233
+ end
234
+ end # to a class
235
+ end # added
236
+ end
237
+ end