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