property 1.2.0 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,100 +1,41 @@
1
-
2
1
  module Property
3
- # This class holds all the properties of a given class or instance. It is used
4
- # to validate content and type_cast during write operations.
5
- #
6
- # The properties are not directly defined in the schema. They are stored in a
7
- # Role instance which checks that the database is in sync with the properties
8
- # defined.
9
- class Schema
10
- attr_reader :roles, :role, :binding
11
-
12
- # Create a new Schema. If a class_name is provided, the schema automatically
13
- # creates a default Role to store definitions.
14
- def initialize(class_name, binding)
15
- @binding = binding
16
- @roles = []
17
- if class_name
18
- @role = Role.new(class_name)
19
- include_role @role
20
- @roles << @role
21
- end
22
- end
23
-
24
- # Return an identifier for the schema to help locate property redefinition errors.
25
- def name
26
- @role ? @role.name : @binding.to_s
27
- end
28
-
29
- # Return true if the current schema has all the roles of the given object, class or role.
30
- def has_role?(thing)
31
- roles = self.roles.flatten
32
- test_roles = thing.class < RoleModule ? [thing] : thing.schema.roles.flatten
33
- test_roles.each do |role|
34
- return false unless roles.include?(role)
35
- end
36
- true
37
- end
38
-
39
- # If the parameter is a class, the schema will inherit the property definitions
40
- # from the class. If the parameter is a Role, the properties from that
41
- # role will be included. Any new columns added to a role or any new
42
- # roles included in a class will be dynamically added to the sub-classes (just like
43
- # Ruby class inheritance, module inclusion works).
44
- # If you ...
45
- def has_role(thing)
46
- if thing.kind_of?(Class)
47
- if thing.respond_to?(:schema) && thing.schema.kind_of?(Schema)
48
- schema_class = thing.schema.binding
49
- if @binding.ancestors.include?(schema_class)
50
- check_super_methods = false
51
- else
52
- check_super_methods = true
53
- end
54
- thing.schema.roles.flatten.each do |role|
55
- include_role role, check_super_methods
56
- end
57
- self.roles << thing.schema.roles
58
- else
59
- raise TypeError.new("expected Role or class with schema, found #{thing}")
2
+ # A schema contains all the property definitions for a given class. If Role is a module,
3
+ # then schema is a Class.
4
+ class Schema < Role
5
+ attr_accessor :roles, :klass
6
+
7
+ # Initialize a new schema with a name and the klass linked to the schema.
8
+ def initialize(name, opts = {})
9
+ super
10
+ @klass = opts[:class]
11
+
12
+ @roles = [self]
13
+
14
+ # Schema inheritance
15
+ unless superschema = opts[:superschema]
16
+ if @klass && @klass.superclass.respond_to?(:schema)
17
+ superschema = @klass.superclass.schema
60
18
  end
61
- elsif thing.kind_of?(RoleModule)
62
- include_role thing
63
- self.roles << thing
64
- else
65
- raise TypeError.new("expected Role or class with schema, found #{thing.class}")
66
19
  end
67
- end
68
-
69
- # Return the list of active roles. The active roles are all the Roles included
70
- # in the current object for which properties have been defined (not blank).
71
- def used_roles_in(object)
72
- roles.flatten.uniq.reject do |role|
73
- !role.used_in(object)
74
- end
75
- end
76
20
 
77
- # Return the list of column names.
78
- def column_names
79
- columns.keys
80
- end
81
-
82
- # Return true if the schema has a property with the given name.
83
- def has_column?(name)
84
- name = name.to_s
85
- [@roles].flatten.each do |role|
86
- return true if role.has_column?(name)
21
+ if superschema
22
+ include_role superschema
87
23
  end
88
- false
89
24
  end
90
25
 
91
- # Return column definitions from all included roles.
92
- def columns
93
- columns = {}
94
- @roles.flatten.uniq.each do |b|
95
- columns.merge!(b.columns)
26
+ # Add a set of property definitions to the schema.
27
+ def include_role(role)
28
+ @columns = nil # clear cache
29
+ if role.kind_of?(Schema)
30
+ # Superclass inheritance
31
+ @roles << role.roles
32
+ elsif role.kind_of?(RoleModule)
33
+ @roles << role
34
+ elsif role.respond_to?(:schema) && role.schema.kind_of?(Role)
35
+ @roles << role.schema.roles
36
+ else
37
+ raise TypeError.new("Cannot include role #{role} (invalid type).")
96
38
  end
97
- columns
98
39
  end
99
40
 
100
41
  # Return a hash with indexed types as keys and index definitions as values.
@@ -108,38 +49,48 @@ module Property
108
49
  index_groups
109
50
  end
110
51
 
111
- private
112
- def include_role(role, check_methods = true)
113
- return if roles.flatten.include?(role)
114
-
115
- stored_column_names = role.column_names
116
-
117
- check_duplicate_property_definitions(role, stored_column_names)
118
- check_duplicate_method_definitions(role, stored_column_names) if check_methods
119
-
120
- role.included_in(self)
121
- @binding.send(:include, role.accessor_module)
122
- end
123
-
124
- def check_duplicate_property_definitions(role, keys)
125
- common_keys = keys & self.columns.keys
126
- if !common_keys.empty?
127
- raise RedefinedPropertyError.new("Cannot include role '#{role.name}' in '#{name}'. Duplicate definitions: #{common_keys.join(', ')}")
52
+ # Return a hash with the column definitions defined in the schema and in the included
53
+ # roles.
54
+ def columns
55
+ # FIXME: can we memoize this list on the first call ? Do we need to update properties after such a call ?
56
+ # @columns ||=
57
+ begin
58
+ res = {}
59
+ @roles.flatten.uniq.each do |role|
60
+ # TODO: we could check for property redefinitions.
61
+ res.merge!(role.defined_columns)
128
62
  end
63
+ res
129
64
  end
65
+ end
130
66
 
131
- def check_duplicate_method_definitions(role, keys)
132
- common_keys = []
133
- # we are in an instance's metaclass, find the class
134
- superclass = @binding.ancestors.detect {|a| a.kind_of?(Class)}
135
- keys.each do |k|
136
- common_keys << k if superclass.method_defined?(k)
137
- end
67
+ # Return the list of active roles. The active roles are all the Roles included
68
+ # in the current object for which properties have been defined (not blank).
69
+ def used_roles_in(object)
70
+ roles.flatten.uniq.select do |role|
71
+ role.used_in(object)
72
+ end
73
+ end
138
74
 
139
- if !common_keys.empty?
140
- raise RedefinedMethodError.new("Cannot include role '#{role.name}' in '#{@binding}'. Would hide methods in superclass (#{superclass}): #{common_keys.join(', ')}")
141
- end
75
+ # Return true if the role has been included or is included in any superclass.
76
+ def has_role?(role)
77
+ if role.kind_of?(Schema)
78
+ role.roles.flatten - @roles.flatten == []
79
+ elsif role.kind_of?(RoleModule)
80
+ @roles.flatten.include?(role)
81
+ elsif role.respond_to?(:schema) && role.schema.kind_of?(Role)
82
+ has_role?(role.schema)
83
+ else
84
+ false
142
85
  end
86
+ end
143
87
 
88
+ # When a column is added in a Schema: define accessors in related class
89
+ def add_column(column)
90
+ super
91
+ if @klass
92
+ @klass.define_property_methods(column) if column.should_create_accessors?
93
+ end
94
+ end
144
95
  end
145
- end
96
+ end # Property
@@ -8,7 +8,7 @@ module Property
8
8
  # Once this module is included, you need to set the has_many association to the class that
9
9
  # contains the columns definitions with something like:
10
10
  #
11
- # has_many :stored_columns, :class_name => NameOfColumnsClass
11
+ # stored_columns_class NameOfColumnsClass
12
12
  module StoredRole
13
13
  include RoleModule
14
14
 
@@ -50,16 +50,20 @@ module Property
50
50
  end
51
51
  end # included
52
52
 
53
- # List all property definitions for the current role
54
- def columns
53
+ # Get all property definitions defined for this role
54
+ def defined_columns
55
55
  load_columns_from_db unless @columns_from_db_loaded
56
56
  super
57
57
  end
58
58
 
59
59
  def property
60
- initialize_role_module unless @accessor_module
61
60
  super
62
61
  end
62
+
63
+ # Overwrite name reader in RoleModule
64
+ def name
65
+ self[:name]
66
+ end
63
67
 
64
68
 
65
69
  private
@@ -74,9 +78,12 @@ module Property
74
78
  end
75
79
 
76
80
  def update_columns
77
- @original_columns ||= {}
81
+ return unless @defined_columns # no change
82
+ unless @original_columns
83
+ load_columns_from_db
84
+ end
78
85
  stored_column_names = @original_columns.keys
79
- defined_column_names = self.column_names
86
+ defined_column_names = column_names
80
87
 
81
88
  new_columns = defined_column_names - stored_column_names
82
89
  updated_columns = defined_column_names & stored_column_names
@@ -1,3 +1,3 @@
1
1
  module Property
2
- VERSION = '1.2.0'
2
+ VERSION = '2.0.0'
3
3
  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 = "1.2.0"
8
+ s.version = "2.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-09-26}
12
+ s.date = %q{2010-11-10}
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 = [
@@ -58,6 +58,7 @@ Gem::Specification.new do |s|
58
58
  "test/unit/property/dirty_test.rb",
59
59
  "test/unit/property/index_complex_test.rb",
60
60
  "test/unit/property/index_custom_test.rb",
61
+ "test/unit/property/index_field_test.rb",
61
62
  "test/unit/property/index_foreign_test.rb",
62
63
  "test/unit/property/index_simple_test.rb",
63
64
  "test/unit/property/role_test.rb",
@@ -71,7 +72,7 @@ Gem::Specification.new do |s|
71
72
  s.rdoc_options = ["--charset=UTF-8"]
72
73
  s.require_paths = ["lib"]
73
74
  s.rubyforge_project = %q{property}
74
- s.rubygems_version = %q{1.3.6}
75
+ s.rubygems_version = %q{1.3.7}
75
76
  s.summary = %q{model properties wrap into a single database column}
76
77
  s.test_files = [
77
78
  "test/database.rb",
@@ -87,6 +88,7 @@ Gem::Specification.new do |s|
87
88
  "test/unit/property/dirty_test.rb",
88
89
  "test/unit/property/index_complex_test.rb",
89
90
  "test/unit/property/index_custom_test.rb",
91
+ "test/unit/property/index_field_test.rb",
90
92
  "test/unit/property/index_foreign_test.rb",
91
93
  "test/unit/property/index_simple_test.rb",
92
94
  "test/unit/property/role_test.rb",
@@ -101,7 +103,7 @@ Gem::Specification.new do |s|
101
103
  current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
102
104
  s.specification_version = 3
103
105
 
104
- if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
106
+ if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
105
107
  s.add_development_dependency(%q<shoulda>, [">= 0"])
106
108
  s.add_runtime_dependency(%q<activerecord>, [">= 0"])
107
109
  else
data/test/database.rb CHANGED
@@ -11,6 +11,7 @@ begin
11
11
  create_table 'employees' do |t|
12
12
  t.string 'type'
13
13
  t.text 'properties'
14
+ t.float 'idx_float1'
14
15
  end
15
16
 
16
17
  create_table 'versions' do |t|
data/test/fixtures.rb CHANGED
@@ -17,6 +17,7 @@ class WebDeveloper < Developer
17
17
 
18
18
  end
19
19
 
20
+
20
21
  class Version < ActiveRecord::Base
21
22
  attr_accessor :backup
22
23
  include Property
@@ -3,7 +3,10 @@ module IndexMacros
3
3
  class Client < ActiveRecord::Base
4
4
  set_table_name :employees
5
5
  include Property
6
-
6
+
7
+ def muse
8
+ 'I am your muse'
9
+ end
7
10
  def index_reader(group_name)
8
11
  if group_name.to_s == 'ml_string'
9
12
  super.merge(:with => {'lang' => ['en', 'fr'], 'site_id' => '123'})
@@ -21,7 +24,7 @@ class Test::Unit::TestCase
21
24
  context "assigned to an instance of Dummy" do
22
25
  subject do
23
26
  dummy = IndexMacros::Client.new
24
- dummy.has_role @poet
27
+ dummy.include_role @poet
25
28
  dummy
26
29
  end
27
30
 
@@ -58,7 +61,7 @@ class Test::Unit::TestCase
58
61
  context "assigned to an instance of Dummy" do
59
62
  subject do
60
63
  dummy = IndexMacros::Client.new
61
- dummy.has_role @poet
64
+ dummy.include_role @poet
62
65
  dummy
63
66
  end
64
67
 
@@ -77,7 +77,7 @@ class Test::Unit::TestCase
77
77
  end
78
78
  end # should_store_property_definitions
79
79
 
80
- def self.should_insert_properties_on_has_role_poet
80
+ def self.should_insert_properties_on_include_role_poet
81
81
  context 'added' do
82
82
 
83
83
  context 'to a parent class' do
@@ -86,35 +86,34 @@ class Test::Unit::TestCase
86
86
  set_table_name :dummies
87
87
  include Property
88
88
  property.string 'name'
89
+
90
+ def muse
91
+ 'I am your muse'
92
+ end
89
93
  end
90
94
 
91
95
  @klass = Class.new(@parent)
92
96
  end
93
97
 
94
98
  should 'propagate definitions to child' do
95
- @parent.has_role @poet
99
+ @parent.include_role @poet
96
100
  assert_equal %w{name poem year}, @klass.schema.column_names.sort
97
101
  end
98
102
 
99
103
  should 'return true on has_role?' do
100
- @parent.has_role @poet
104
+ @parent.include_role @poet
101
105
  assert @klass.has_role?(@poet)
102
106
  end
103
107
 
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
108
  should 'not raise an exception on double inclusion' do
110
- @parent.has_role @poet
111
- assert_nothing_raised { @parent.has_role @poet }
109
+ @parent.include_role @poet
110
+ assert_nothing_raised { @parent.include_role @poet }
112
111
  end
113
112
 
114
113
  should 'add accessor methods to child' do
115
114
  subject = @klass.new
116
115
  assert_raises(NoMethodError) { subject.poem = 'Poe'}
117
- @parent.has_role @poet
116
+ @parent.include_role @poet
118
117
 
119
118
  assert_nothing_raised { subject.poem = 'Poe'}
120
119
  end
@@ -126,22 +125,26 @@ class Test::Unit::TestCase
126
125
  set_table_name :dummies
127
126
  include Property
128
127
  property.string 'name'
128
+
129
+ def muse
130
+ 'I am your muse'
131
+ end
129
132
  end
130
133
  end
131
134
 
132
135
  should 'insert definitions' do
133
- @klass.has_role @poet
136
+ @klass.include_role @poet
134
137
  assert_equal %w{name poem year}, @klass.schema.column_names.sort
135
138
  end
136
139
 
137
140
  should 'return true on class has_role?' do
138
- @klass.has_role @poet
141
+ @klass.include_role @poet
139
142
  assert @klass.has_role?(@poet)
140
143
  end
141
144
 
142
145
  should 'return role from column' do
143
- @klass.has_role @poet
144
- assert_equal (@poet.kind_of?(Class) ? @poet.schema.role : @poet), @klass.schema.columns['poem'].role
146
+ @klass.include_role @poet
147
+ assert_equal (@poet.kind_of?(Class) ? @poet.schema : @poet), @klass.schema.columns['poem'].role
145
148
  end
146
149
  end
147
150
 
@@ -149,7 +152,7 @@ class Test::Unit::TestCase
149
152
  subject { Developer.new }
150
153
 
151
154
  setup do
152
- subject.has_role @poet
155
+ subject.include_role @poet
153
156
  end
154
157
 
155
158
  should 'merge property definitions' do
@@ -157,38 +160,7 @@ class Test::Unit::TestCase
157
160
  end
158
161
  end
159
162
  end
160
- end # should_insert_properties_on_has_role
161
-
162
- def self.should_add_role_methods
163
- context 'added' do
164
- context 'to a parent class' do
165
- setup do
166
- @parent = Class.new(ActiveRecord::Base) do
167
- set_table_name :dummies
168
- include Property
169
- property.string 'name'
170
- end
171
-
172
- @klass = Class.new(@parent)
173
- end
174
-
175
- should 'add role methods to child' do
176
- subject = @klass.new
177
- assert_raises(NoMethodError) { subject.muse }
178
- @parent.has_role @poet
179
-
180
- assert_nothing_raised { subject.muse }
181
- end
182
-
183
- should 'use role methods for defaults' do
184
- subject = @klass.new
185
- @parent.has_role @poet
186
- assert subject.save
187
- assert_equal 'I am your muse', subject.poem
188
- end
189
- end
190
- end
191
- end # should_add_role_methods
163
+ end # should_insert_properties_on_include_role_poet
192
164
 
193
165
  def self.should_take_part_in_used_list(has_defaults = true)
194
166
 
@@ -199,9 +171,13 @@ class Test::Unit::TestCase
199
171
  set_table_name :dummies
200
172
  include Property
201
173
  property.string 'name'
174
+
175
+ def muse
176
+ 'I am your muse'
177
+ end
202
178
  end
203
179
 
204
- @klass.has_role @poet
180
+ @klass.include_role @poet
205
181
  end
206
182
 
207
183
  subject do
@@ -214,7 +190,7 @@ class Test::Unit::TestCase
214
190
 
215
191
  should 'not return role without corresponding attributes' do
216
192
  subject.attributes = {'name' => 'hello'}
217
- assert_equal [@klass.schema.role], subject.used_roles
193
+ assert_equal [@klass.schema], subject.used_roles
218
194
  end
219
195
 
220
196
  should 'return role with corresponding attributes' do