property 0.9.1 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore CHANGED
@@ -1,3 +1,4 @@
1
1
  .DS_Store
2
2
  coverage
3
3
  *.gem
4
+ log/test.log
data/History.txt CHANGED
@@ -1,6 +1,22 @@
1
+ == 1.0.0 2010-05-27
2
+
3
+ * Major enhancements
4
+ * Added StoredRole class to store role definitions in the database.
5
+ * Validates with legacy but invalid properties if value is unchanged.
6
+ * Added the notion of used roles.
7
+ * Renamed Behavior to Role.
8
+ * Added support for index definition in stored column.
9
+ * Added support for :with option in index_reader.
10
+
11
+ * 2 minor enhancements
12
+ * Created Property::Base module for inclusion without callbacks.
13
+ * Raises an exception if we try to define a property that would hide a superclass method.
14
+ * Fixed multiple column declarations in one go.
15
+ * Fixed bug when adding role as class to sub-class.
16
+
1
17
  == 0.9.1 2010-03-20
2
18
 
3
- * 1 major enhancement
19
+ * 2 major enhancements
4
20
  * Added support for custom indexer classes.
5
21
  * Removed after_commit dependency (no need for an after_commit).
6
22
 
@@ -24,13 +40,13 @@
24
40
  == 0.8.1 2010-02-14
25
41
 
26
42
  * 2 major enhancement
27
- * Enabled behavior method definitions.
43
+ * Enabled role method definitions.
28
44
  * Enabled external storage with 'store_properties_in' method.
29
45
 
30
46
  == 0.8.0 2010-02-11
31
47
 
32
48
  * 3 major enhancements
33
- * Enabled Behaviors that can be added on an instance.
49
+ * Enabled Roles that can be added on an instance.
34
50
  * Enabled non-DB types.
35
51
  * 100% test coverage.
36
52
 
data/README.rdoc CHANGED
@@ -47,12 +47,12 @@ And set them with:
47
47
  @contact.prop['name'] = 'Gandhi'
48
48
  @contact.name = 'Gandhi'
49
49
 
50
- == Behaviors
50
+ == Roles
51
51
 
52
52
  Properties would not be really fun if you could not add new properties to your instances depending
53
- on some flags. First define the behaviors:
53
+ on what the object does. First define the roles:
54
54
 
55
- @picture = Property::Behavior.new do |p|
55
+ @picture = Property::Role.new do |p|
56
56
  p.integer :width, :default => :get_width
57
57
  p.integer :height, :default => :get_height
58
58
  p.string 'camera'
@@ -76,16 +76,21 @@ on some flags. First define the behaviors:
76
76
  end
77
77
  end
78
78
 
79
- And then, either when creating new pictures or updating them, you need to include the behavior:
79
+ And then, either when creating new pictures or updating them, you need to include the role:
80
80
 
81
- @model.behave_like @picture
81
+ @model.has_role @picture
82
82
 
83
83
  The model now has the picture's properties defined, with accessors like @model.camera, methods like
84
84
  @model.image, get_with, etc and default values will be fetched on save.
85
85
 
86
- Note that you do not need to include a behavior just to read the data as long as you use the 'prop'
86
+ Note that you do not need to include a role just to read the data as long as you use the 'prop'
87
87
  accessor.
88
88
 
89
+ == StoredRole
90
+
91
+ The dynamic nature of the Property gem goes to the point where you can store your property definitions
92
+ in the database by using the StoredRole and StoredColumn modules.
93
+
89
94
  == External storage
90
95
 
91
96
  You might need to define properties in a model but store them in another model (versioning). In this
@@ -102,3 +107,43 @@ case you can simply use 'store_properties_in' class method:
102
107
 
103
108
  Doing so will not touch the storage class. All property definitions, validations and method
104
109
  definitions are executed on the 'Contact' class.
110
+
111
+ == Indexing support
112
+
113
+ The property gem lets you very easily export content from the packed data to any kind of external table. Using
114
+ a key/value tables:
115
+
116
+ class Contact < ActiveRecord::Base
117
+ include Property
118
+ property do |p|
119
+ p.string 'name', :indexed => true
120
+ p.string 'first_name', :index => Proc.new {|rec| { 'fullname' => rec.fullname }}
121
+
122
+ p.index(:string) do |record|
123
+ {
124
+ 'fulltext' => "name:#{record.name} first_name:#{record.first_name}",
125
+ "name_#{record.lang}" => record.name
126
+ }
127
+ end
128
+ end
129
+ end
130
+
131
+ Using a custom indexer, you can group indexed values together in a single record. This can be interesting if you have
132
+ some legacy code or queries that need direct access to some values:
133
+
134
+ class Contact < ActiveRecord::Base
135
+ include Property
136
+ property do |p|
137
+ p.string 'name'
138
+ p.string 'first_name'
139
+
140
+ p.index(ContactIndexer) do |record|
141
+ {
142
+ 'name' => record.name,
143
+ 'first_name' => record.first_name,
144
+ }
145
+ end
146
+ end
147
+ end
148
+
149
+ Please read the docs for details: http://zenadmin.org/635
data/lib/property.rb CHANGED
@@ -2,16 +2,18 @@ require 'property/attribute'
2
2
  require 'property/dirty'
3
3
  require 'property/properties'
4
4
  require 'property/column'
5
- require 'property/behavior'
5
+ require 'property/role'
6
+ require 'property/stored_role'
6
7
  require 'property/schema'
7
8
  require 'property/declaration'
8
9
  require 'property/db'
9
10
  require 'property/index'
10
11
  require 'property/serialization/json'
11
12
  require 'property/core_ext/time'
13
+ require 'property/base'
12
14
 
13
15
  module Property
14
- VERSION = '0.9.1'
16
+ VERSION = '1.0.0'
15
17
 
16
18
  def self.included(base)
17
19
  base.class_eval do
@@ -11,18 +11,25 @@ module Property
11
11
  # them apart.
12
12
  #
13
13
  module Attribute
14
-
15
14
  def self.included(base)
16
- base.extend ClassMethods
17
-
18
15
  base.class_eval do
19
- include InstanceMethods
16
+ include Base
17
+ after_validation :dump_properties
18
+ alias_method_chain :attributes=, :properties
19
+ end
20
+ end
20
21
 
21
- store_properties_in self
22
+ # This is just a helper module that includes necessary code for property access, but without
23
+ # the validation/save hooks.
24
+ module Base
25
+ def self.included(base)
26
+ base.extend ClassMethods
22
27
 
23
- after_validation :dump_properties
28
+ base.class_eval do
29
+ include InstanceMethods
24
30
 
25
- alias_method_chain :attributes=, :properties
31
+ store_properties_in self
32
+ end
26
33
  end
27
34
  end
28
35
 
@@ -0,0 +1,16 @@
1
+ module Property
2
+
3
+ # This module is used when we need to access the properties in the properties storage model (to
4
+ # compare versions for example). Including this module has the same effect as including 'Property'
5
+ # but without the hooks (validation, save, etc).
6
+ module Base
7
+ def self.included(base)
8
+ base.class_eval do
9
+ include Attribute::Base
10
+ include Serialization::JSON
11
+ include Declaration::Base
12
+ include Dirty
13
+ end
14
+ end
15
+ end
16
+ end
@@ -12,11 +12,11 @@ module Property
12
12
 
13
13
  def initialize(name, default, type, options={})
14
14
  name = name.to_s
15
- extract_property_options(options)
16
15
  if type.kind_of?(Class)
17
16
  @klass = type
18
17
  end
19
18
  super(name, default, type, options)
19
+ extract_property_options(options)
20
20
  end
21
21
 
22
22
  def validate(value, errors)
@@ -48,6 +48,9 @@ module Property
48
48
  @klass || super
49
49
  end
50
50
 
51
+ # Property type used instead of 'type' when column is stored
52
+ alias ptype type
53
+
51
54
  def type_cast(value)
52
55
  if type == :string
53
56
  value = value.to_s
@@ -62,6 +65,15 @@ module Property
62
65
  private
63
66
  def extract_property_options(options)
64
67
  @index = options.delete(:index) || options.delete(:indexed)
68
+ if @index == true
69
+ @index = ptype
70
+ end
71
+
72
+ if @index.blank?
73
+ @index = nil
74
+ elsif @index.kind_of?(Symbol)
75
+ @index = @index.to_s
76
+ end
65
77
  end
66
78
 
67
79
  def extract_default(default)
@@ -3,39 +3,50 @@ module Property
3
3
  # Property::Declaration module is used to declare property definitions in a Class. The module
4
4
  # also manages property inheritence in sub-classes.
5
5
  module Declaration
6
-
7
6
  def self.included(base)
8
7
  base.class_eval do
9
- extend ClassMethods
10
- include InstanceMethods
8
+ include Base
9
+ validate :properties_validation, :if => :properties
10
+ end
11
+ end
11
12
 
12
- class << self
13
- attr_accessor :schema
13
+ module Base
14
+ def self.included(base)
15
+ base.class_eval do
16
+ extend ClassMethods
17
+ include InstanceMethods
14
18
 
15
- def schema
16
- @schema ||= make_schema
17
- end
19
+ class << self
20
+ attr_accessor :schema
18
21
 
19
- private
20
- def make_schema
21
- schema = Property::Schema.new(self.to_s, self)
22
- if superclass.respond_to?(:schema)
23
- schema.behave_like superclass
24
- end
25
- schema
22
+ def schema
23
+ @schema ||= make_schema
26
24
  end
27
- end
28
25
 
29
- validate :properties_validation, :if => :properties
26
+ private
27
+ def make_schema
28
+ schema = Property::Schema.new(self.to_s, self)
29
+ if superclass.respond_to?(:schema)
30
+ schema.has_role superclass
31
+ end
32
+ schema
33
+ end
34
+ end
35
+ end
30
36
  end
31
37
  end
32
38
 
33
39
  module ClassMethods
34
40
 
35
- # Include a new set of property definitions (Behavior) into the current class schema.
41
+ # Include a new set of property definitions (Role) into the current class schema.
36
42
  # You can also provide a class to simulate multiple inheritance.
37
- def behave_like(behavior)
38
- schema.behave_like behavior
43
+ def has_role(role)
44
+ schema.has_role role
45
+ end
46
+
47
+ # Return true if the current object has all the roles of the given object, class or role.
48
+ def has_role?(role)
49
+ schema.has_role? role
39
50
  end
40
51
 
41
52
  # Use this class method to declare properties and indices that will be used in your models.
@@ -52,21 +63,32 @@ module Property
52
63
  # end
53
64
  # end
54
65
  def property(&block)
55
- schema.behavior.property(&block)
66
+ schema.role.property(&block)
56
67
  end
57
68
  end # ClassMethods
58
69
 
59
70
  module InstanceMethods
60
- # Instance's schema (can be different from the instance's class schema if behaviors have been
71
+ # Instance's schema (can be different from the instance's class schema if roles have been
61
72
  # added to the instance.
62
73
  def schema
63
74
  @own_schema || self.class.schema
64
75
  end
65
76
 
66
- # Include a new set of property definitions (Behavior) into the current instance's schema.
77
+ # Include a new set of property definitions (Role) into the current instance's schema.
67
78
  # You can also provide a class to simulate multiple inheritance.
68
- def behave_like(behavior)
69
- own_schema.behave_like behavior
79
+ def has_role(role)
80
+ own_schema.has_role role
81
+ end
82
+
83
+ # Return the list of active roles. The active roles are all the Roles included
84
+ # in the current object for which properties have been defined (not blank).
85
+ def used_roles
86
+ own_schema.used_roles_in(self)
87
+ end
88
+
89
+ # Return true if the current object has all the roles of the given object, class or role.
90
+ def has_role?(role)
91
+ own_schema.has_role? role
70
92
  end
71
93
 
72
94
  protected
@@ -81,7 +103,7 @@ module Property
81
103
  def make_own_schema
82
104
  this = class << self; self; end
83
105
  schema = Property::Schema.new(nil, this)
84
- schema.behave_like self.class
106
+ schema.has_role self.class
85
107
  schema
86
108
  end
87
109
  end # InsanceMethods
@@ -0,0 +1,4 @@
1
+ module Property
2
+ class Error < Exception
3
+ end
4
+ end
@@ -23,37 +23,120 @@ module Property
23
23
  def get_indices(group_name)
24
24
  return {} if new_record?
25
25
  res = {}
26
- Property::Db.fetch_attributes(['key', 'value'], index_table_name(group_name), index_reader_sql).each do |row|
26
+ Property::Db.fetch_attributes(['key', 'value'], index_table_name(group_name), index_reader_sql(group_name)).each do |row|
27
27
  res[row['key']] = row['value']
28
28
  end
29
29
  res
30
30
  end
31
31
 
32
- def index_reader_sql
33
- index_reader.map {|k, v| "#{k} = #{self.class.connection.quote(v)}"}.join(' AND ')
32
+ def index_reader_sql(group_name)
33
+ index_reader(group_name).map do |k, v|
34
+ if k == :with
35
+ v.map do |subk, subv|
36
+ if subv.kind_of?(Array)
37
+ "`#{subk}` IN (#{subv.map {|ssubv| connection.quote(ssubv)}.join(',')})"
38
+ else
39
+ "`#{subk}` = #{self.class.connection.quote(subv)}"
40
+ end
41
+ end.join(' AND ')
42
+ else
43
+ "`#{k}` = #{self.class.connection.quote(v)}"
44
+ end
45
+ end.join(' AND ')
34
46
  end
35
47
 
36
- def index_reader
48
+ def index_reader(group_name)
37
49
  {index_foreign_key => self.id}
38
50
  end
39
51
 
40
- alias index_writer index_reader
52
+ def index_writer(group_name)
53
+ index_reader(group_name)
54
+ end
41
55
 
42
56
  def index_table_name(group_name)
43
57
  "i_#{group_name}_#{self.class.table_name}"
44
58
  end
45
59
 
46
60
  def index_foreign_key
47
- self.class.table_name.singularize.foreign_key
61
+ @index_foreign_key ||=self.class.table_name.singularize.foreign_key
62
+ end
63
+
64
+ # Create a list of index entries
65
+ def create_indices(group_name, new_keys, cur_indices)
66
+ # Build insert_many query
67
+ writer = index_writer(group_name)
68
+ foreign_keys = index_foreign_keys(writer)
69
+
70
+ Property::Db.insert_many(
71
+ index_table_name(group_name),
72
+ foreign_keys + %w{key value},
73
+ map_index_values(new_keys, cur_indices, foreign_keys, writer)
74
+ )
75
+ end
76
+
77
+ def index_foreign_keys(writer)
78
+ if with = writer[:with]
79
+ writer.keys - [:with] + with.keys
80
+ else
81
+ writer.keys
82
+ end
83
+ end
84
+
85
+ def map_index_values(new_keys, cur_indices, index_foreign_keys, index_writer)
86
+ if with = index_writer[:with]
87
+ foreign_values = explode_list(index_foreign_keys.map {|k| index_writer[k] || with[k]})
88
+ else
89
+ foreign_values = [index_foreign_keys.map {|k| index_writer[k]}]
90
+ end
91
+
92
+ values = new_keys.map do |key|
93
+ [connection.quote(key), connection.quote(cur_indices[key])]
94
+ end
95
+
96
+ res = []
97
+ foreign_values.each do |list|
98
+ list = list.map {|k| connection.quote(k)}
99
+ values.each do |value|
100
+ res << (list + value)
101
+ end
102
+ end
103
+ res
104
+ end
105
+
106
+ # Takes a mixed array and explodes it
107
+ # [x, ['en','fr'], y, [a,b]] ==> [[x,'en',y,a], [x,'en',y',b], [x,'fr',y,a], [x,'fr',y,b]]
108
+ def explode_list(list)
109
+ res = [[]]
110
+ list.each do |key|
111
+ if key.kind_of?(Array)
112
+ res_bak = res
113
+ res = []
114
+ key.each do |k|
115
+ res_bak.each do |list|
116
+ res << (list + [k])
117
+ end
118
+ end
119
+ else
120
+ res.each do |list|
121
+ list << key
122
+ end
123
+ end
124
+ end
125
+ res
126
+ end
127
+
128
+ # Update an index entry
129
+ def update_index(group_name, key, value)
130
+ self.class.connection.execute "UPDATE #{index_table_name(group_name)} SET `value` = #{connection.quote(value)} WHERE #{index_reader_sql(group_name)} AND `key` = #{connection.quote(key)}"
131
+ end
132
+
133
+ # Delete a list of indices (value became blank).
134
+ def delete_indices(group_name, keys)
135
+ self.class.connection.execute "DELETE FROM #{index_table_name(group_name)} WHERE #{index_reader_sql(group_name)} AND `key` IN (#{keys.map{|key| connection.quote(key)}.join(',')})"
48
136
  end
49
137
 
50
138
  # This method prepares the index
51
139
  def property_index
52
- connection = self.class.connection
53
- reader_sql = index_reader_sql
54
- foreign_keys = nil
55
- foreign_values = nil
56
-
57
140
  schema.index_groups.each do |group_name, definitions|
58
141
  cur_indices = {}
59
142
  definitions.each do |key, proc|
@@ -61,12 +144,16 @@ module Property
61
144
  value = prop[key]
62
145
  if !value.blank?
63
146
  if proc
147
+ # Get value(s) to index through proc
64
148
  cur_indices.merge!(proc.call(self))
65
149
  else
150
+ # Set current value from prop
66
151
  cur_indices[key] = value
67
152
  end
68
153
  end
69
154
  else
155
+ # No key: group index generated with
156
+ # p.index(group_name) do |record| ...
70
157
  cur_indices.merge!(proc.call(self))
71
158
  end
72
159
  end
@@ -74,7 +161,7 @@ module Property
74
161
  if group_name.kind_of?(Class)
75
162
  # Use a custom indexer
76
163
  group_name.set_property_index(self, cur_indices)
77
- else
164
+ elsif index_reader(group_name)
78
165
  # Add key/value pairs to the default tables
79
166
  old_indices = get_indices(group_name)
80
167
 
@@ -85,34 +172,22 @@ module Property
85
172
  del_keys = old_keys - cur_keys
86
173
  upd_keys = cur_keys & old_keys
87
174
 
88
- table_name = index_table_name(group_name)
89
-
90
175
  upd_keys.each do |key|
91
176
  value = cur_indices[key]
92
177
  if value.blank?
93
178
  del_keys << key
94
- else
95
- connection.execute "UPDATE #{table_name} SET value = #{connection.quote(cur_indices[key])} WHERE #{reader_sql} AND key = #{connection.quote(key)}"
179
+ elsif value != old_indices[key]
180
+ update_index(group_name, key, value)
96
181
  end
97
182
  end
98
183
 
99
184
  if !del_keys.empty?
100
- connection.execute "DELETE FROM #{table_name} WHERE #{reader_sql} AND key IN (#{del_keys.map{|key| connection.quote(key)}.join(',')})"
185
+ delete_indices(group_name, del_keys)
101
186
  end
102
187
 
103
188
  new_keys.reject! {|k| cur_indices[k].blank? }
104
189
  if !new_keys.empty?
105
- # we evaluate this now to have the id on record creation
106
- foreign_keys ||= index_writer.keys
107
- foreign_values ||= foreign_keys.map {|k| index_writer[k]}
108
-
109
- Property::Db.insert_many(
110
- table_name,
111
- foreign_keys + ['key', 'value'],
112
- new_keys.map do |key|
113
- foreign_values + [connection.quote(key), connection.quote(cur_indices[key])]
114
- end
115
- )
190
+ create_indices(group_name, new_keys, cur_indices)
116
191
  end
117
192
  end
118
193
  end
@@ -127,7 +202,7 @@ module Property
127
202
  if group_name.kind_of?(Class)
128
203
  group_name.delete_property_index(self)
129
204
  else
130
- connection.execute "DELETE FROM #{index_table_name(group_name)} WHERE #{foreign_key} = #{current_id}"
205
+ connection.execute "DELETE FROM #{index_table_name(group_name)} WHERE `#{foreign_key}` = #{current_id}"
131
206
  end
132
207
  end
133
208
  end