property 1.2.0 → 2.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.
@@ -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