flexi_model 0.2.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/README.md ADDED
@@ -0,0 +1,122 @@
1
+ flexi-model
2
+ ===========
3
+
4
+ [![Code Climate](https://codeclimate.com/badge.png)](https://codeclimate.com/github/we4tech/flexi-model)
5
+
6
+ Build flexible database model with dynamic fields (right now based on ActiveRecord soon it will work with mongoid too)
7
+
8
+ How to do ?
9
+ ===========
10
+
11
+ Define your first model.
12
+ ---------
13
+ ```ruby
14
+ class User
15
+ include FlexiModel
16
+ _string :name, :email
17
+ _text :bio
18
+ validates_presence_of :name, :email
19
+ end
20
+ ```
21
+
22
+ Create your new record.
23
+ ------------
24
+ ```ruby
25
+ User.create name: 'hasan', email: 'hasan@welltreat.us', bio: 'Ruby developer'
26
+ #=> #<User:...>
27
+ ```
28
+
29
+ Find record by id.
30
+ -----------
31
+ ```ruby
32
+ User.find(1)
33
+ ```
34
+
35
+ Find records by name
36
+ --------
37
+ ```ruby
38
+ User.where(name: 'hasan')
39
+ #=> #<...::Criteria...> # Instance of criteria object
40
+ ```
41
+
42
+ Define belongs to and has many relationship
43
+ ---------
44
+ ```ruby
45
+ class Blog
46
+ include FlexiModel
47
+ _string :title
48
+ _text :content
49
+ belongs_to :user
50
+ end
51
+
52
+ class User
53
+ include FlexiModel
54
+ _string :name
55
+ has_many :blogs
56
+ end
57
+
58
+ # Create records
59
+ user = User.create(name: 'nafi')
60
+ Blog.create(title: 'Hello world', content: 'Hello content', user: user)
61
+
62
+ # Find all related blogs
63
+ user.blogs
64
+
65
+ # Find parent record
66
+ user.blogs.first.user
67
+ ```
68
+
69
+ Define has and belongs to many relationships
70
+ -----------
71
+ ```ruby
72
+ class User
73
+ inlude FlexiModel
74
+ _string :name
75
+ has_and_belongs_to_many :roles
76
+ end
77
+
78
+ class Role
79
+ include FlexiModel
80
+ _string :name
81
+ has_and_belongs_to_many :users
82
+ end
83
+
84
+ # Create user with roles
85
+ User.create(name: 'khan', roles: [Role.create(name: 'admin'), Role.create(name: 'moderator')])
86
+
87
+ # Find user roles
88
+ user = User.where(name: 'khan').first
89
+ user.roles
90
+ ```
91
+
92
+ Update attribute
93
+ ---------
94
+ ```ruby
95
+ user = User.where(...)
96
+ user.update_attribute :name, 'raju'
97
+ ```
98
+
99
+ Update attributes
100
+ -----------
101
+ ```ruby
102
+ user = User.where(...)
103
+ user.update_attributes name: 'raju', email: 'hola@hola.com'
104
+ ```
105
+
106
+ Destroy record
107
+ -------
108
+ ```ruby
109
+ user.destroy
110
+ User.where(...conditions...).destroy_all
111
+ ```
112
+
113
+ Observers
114
+ --------
115
+ * Before, After and Around Create
116
+ * Before, After and Around Update
117
+ * Before, After and Around Destroy
118
+
119
+ TODOS
120
+ =====
121
+
122
+ * Write documentation
data/lib/discover.rb ADDED
@@ -0,0 +1,11 @@
1
+ # Include plugins
2
+ require 'autotest/fsevent'
3
+ require 'autotest/growl'
4
+
5
+ Autotest.add_discovery { "rspec2" }
6
+
7
+ # Skip some paths
8
+ Autotest.add_hook :initialize do |autotest|
9
+ %w{.idea .git .DS_Store ._* vendor}.each { |exception| autotest.add_exception(exception) }
10
+ false
11
+ end
@@ -0,0 +1,15 @@
1
+ module FlexiModel
2
+ module ArModels
3
+ class Collection < ActiveRecord::Base
4
+ self.table_name = 'flexi_model_collections'
5
+
6
+ validates_presence_of :name
7
+ validates_uniqueness_of :name, :scope => :partition_id, :if => :partition_id
8
+ has_and_belongs_to_many :fields, :join_table => 'flexi_model_collections_fields'
9
+ has_many :records, :dependent => :destroy
10
+
11
+ attr_accessible :namespace, :name, :partition_id,
12
+ :singular_label, :plural_label, :fields
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,36 @@
1
+ module FlexiModel
2
+ module ArModels
3
+ class Field < ActiveRecord::Base
4
+ self.table_name = 'flexi_model_fields'
5
+ attr_accessible :name, :singular_label,:plural_label, :namespace,
6
+ :partition_id, :field_type, :default_value
7
+
8
+ COLUMNS_MAP = {
9
+ boolean: :bool_value,
10
+ integer: :int_value,
11
+ decimal: :dec_value,
12
+ float: :dec_value,
13
+ string: :str_value,
14
+ email: :str_value,
15
+ phone: :str_value,
16
+ location: :str_value,
17
+ address: :txt_value,
18
+ text: :txt_value,
19
+ multiple: :txt_value,
20
+ datetime: :dt_value,
21
+ date: :dt_value,
22
+ time: :dt_value
23
+ }
24
+
25
+ has_and_belongs_to_many :collections, :join_table => 'flexi_model_collections_fields'
26
+ has_many :values, :dependent => :destroy
27
+
28
+ validates_presence_of :name, :field_type
29
+ validates_uniqueness_of :name, :scope => [:namespace, :partition_id, :field_type]
30
+
31
+ def value_column
32
+ FlexiModel::ArModels::Field::COLUMNS_MAP[self.field_type.to_sym]
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,31 @@
1
+ module FlexiModel
2
+ module ArModels
3
+ class Record < ActiveRecord::Base
4
+ self.table_name = 'flexi_model_records'
5
+
6
+ attr_accessible :collection_id, :namespace, :values, :values_attributes,
7
+ :collection
8
+
9
+ belongs_to :collection
10
+ has_many :values, :dependent => :destroy
11
+ has_many :fields, :through => :values
12
+
13
+ scope :by_namespace, lambda { |n| where(namespace: n) }
14
+ scope :recent, order('created_at DESC')
15
+
16
+ accepts_nested_attributes_for :values
17
+
18
+ def value_of(field_name)
19
+ values.select{|v| v.field.present? }.
20
+ select{|v| v.field.name.downcase == field_name.to_s.downcase}.first
21
+ end
22
+
23
+ def title
24
+ _value = value_of(:name) || value_of(:title)
25
+ return _value.value if _value.present?
26
+
27
+ self.collection.singular_label
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,52 @@
1
+ module FlexiModel
2
+ module ArModels
3
+ class Value < ActiveRecord::Base
4
+ self.table_name = 'flexi_model_values'
5
+
6
+ attr_accessible :record_id, :field_id, :bool_value, :int_value,
7
+ :dec_value, :str_value, :txt_value, :dt_value, :field,
8
+ :value
9
+
10
+ belongs_to :record
11
+ belongs_to :field
12
+
13
+ # Set value based on field type.
14
+ # ie. if it is `string` type it will store value in str_value
15
+ #
16
+ # List of field value mappings -
17
+ # Boolean 'bool_value'
18
+ # Integer 'int_value'
19
+ # Decimal 'dec_value'
20
+ # String 'str_value'
21
+ # Text 'txt_value'
22
+ # Datetime 'dt_value'
23
+ def value=(val)
24
+ self.send :"#{_mapped_value_column}=", val
25
+ end
26
+
27
+ # Get value from corresponding column based on field type
28
+ def value
29
+ self.send :"#{_mapped_value_column}"
30
+ end
31
+
32
+ def column_for_attribute(name)
33
+ if :value == name.to_sym
34
+ self.class.columns_hash[self.field.value_column.to_s]
35
+ else
36
+ self.class.columns_hash[name.to_s]
37
+ end
38
+ end
39
+
40
+ private
41
+ def _mapped_value_column
42
+ raise 'No field is set' if field.nil?
43
+
44
+ value_col =
45
+ FlexiModel::ArModels::Field::COLUMNS_MAP[field.field_type.to_sym]
46
+ raise "Unknown field type - #{field.field_type}" if value_col.nil?
47
+
48
+ value_col
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,10 @@
1
+ module FlexiModel
2
+ module ArModels
3
+
4
+ end
5
+ end
6
+
7
+ require 'flexi_model/ar_models/collection'
8
+ require 'flexi_model/ar_models/field'
9
+ require 'flexi_model/ar_models/record'
10
+ require 'flexi_model/ar_models/value'
@@ -0,0 +1,318 @@
1
+ module FlexiModel
2
+ module ArPersistence
3
+ extend ActiveSupport::Concern
4
+
5
+ RECORD = FlexiModel::ArModels::Record
6
+ COLLECTION = FlexiModel::ArModels::Collection
7
+ FIELD = FlexiModel::ArModels::Field
8
+ VALUE = FlexiModel::ArModels::Value
9
+
10
+ included do
11
+ class_eval <<-RUBY
12
+ @@_flexi_collection = nil
13
+ cattr_accessor :_flexi_collection
14
+
15
+ @@_flexi_metadata = { }
16
+ cattr_accessor :_flexi_metadata
17
+
18
+ @@_flexi_fields_map = nil
19
+ cattr_accessor :_flexi_fields_map
20
+
21
+ RUBY
22
+ end
23
+
24
+ module ClassMethods
25
+ # Set collection label
26
+ #
27
+ # singular - Set singular name for the collection
28
+ # plural - Set plural name for the collection
29
+ def set_flexi_label(singular, plural)
30
+ _flexi_metadata[:label_singular] = singular
31
+ _flexi_metadata[:label_plural] = plural
32
+ end
33
+
34
+ # Return singular and plural collection label
35
+ # If not defined it will take class name as collection name
36
+ #
37
+ # Returns array of singular and plural labels
38
+ def get_flexi_label
39
+ labels = [_flexi_metadata[:label_singular],
40
+ _flexi_metadata[:label_plural]].compact
41
+
42
+ if labels.empty?
43
+ _f_name = _friendly_name(self.name)
44
+ [_f_name.singularize, _f_name.pluralize]
45
+ else
46
+ labels
47
+ end
48
+ end
49
+
50
+ # Return collection name based on parametrized class name
51
+ def flexi_collection_name
52
+ self.name.parameterize
53
+ end
54
+
55
+ def get_flexi_namespace
56
+ self.flexi_collection_name.parameterize
57
+ end
58
+
59
+ # Initialize new instance and set data from record
60
+ def initialize_with_record(record)
61
+ inst = self.new
62
+ inst.send(:_record=, record)
63
+ inst.send(:_id=, record.id)
64
+ inst
65
+ end
66
+
67
+ # Return by default plural label
68
+ def flexi_label(singular = false)
69
+ if singular
70
+ self._flexi_collection.singular_label
71
+ else
72
+ self._flexi_collection.plural_label
73
+ end
74
+ end
75
+
76
+ def flexi_collection;
77
+ _flexi_collection
78
+ end
79
+
80
+ delegate :id, :to => :_flexi_collection
81
+
82
+ def destroy_all;
83
+ RECORD.by_namespace(self.get_flexi_namespace).each do |record|
84
+ inst = initialize_with_record(record)
85
+ inst.destroy
86
+ end
87
+ end
88
+
89
+ def delete_all;
90
+ RECORD.by_namespace(self.get_flexi_namespace).delete_all
91
+ end
92
+
93
+ # Create does exactly as `save`, but it initiates `:create` callbacks
94
+ def create(attributes = { })
95
+ inst = self.new(attributes)
96
+ inst.save
97
+
98
+ inst
99
+ end
100
+
101
+ private
102
+ def _friendly_name(long_name)
103
+ long_name.to_s.split("::").last
104
+ end
105
+ end
106
+
107
+ def initialize(*)
108
+ super
109
+ _find_or_update_or_build_collection!
110
+ end
111
+
112
+ # Ensure object with same _id returns true on equality check
113
+ def ==(another_instance)
114
+ self._id && self._id == another_instance._id
115
+ end
116
+
117
+ # Return true if record is not saved
118
+ def new_record?
119
+ !_id.present?
120
+ end
121
+
122
+ # Store record in persistent storage
123
+ def save
124
+ create_or_update
125
+ _id.present?
126
+ end
127
+
128
+ # Update stored attributes by give hash
129
+ def update_attributes(_params)
130
+ assign_attributes _params
131
+ save
132
+ end
133
+
134
+ # Update single attribute by key and value
135
+ def update_attribute(key, value)
136
+ self.update_attributes(key => value)
137
+ end
138
+
139
+ def destroy
140
+ if _id.present?
141
+ RECORD.delete(self._id)
142
+ else
143
+ false
144
+ end
145
+ end
146
+
147
+ # Reload object instance
148
+ def reload
149
+ self.class.find(self._id)
150
+ end
151
+
152
+ # Forcefully load all attributes
153
+ def load_attributes!
154
+ self.flexi_fields.map { |f| self.send(f.name.to_sym) }
155
+ end
156
+
157
+ # Return existing or create new collection set
158
+ def get_flexi_collection
159
+ _find_or_update_or_build_collection!
160
+
161
+ self._flexi_collection
162
+ end
163
+
164
+ # Return flexi fields in name and field object map
165
+ def get_flexi_fields_map
166
+ self._flexi_fields_map ||=
167
+ Hash[get_flexi_collection.fields.
168
+ map { |_field| [_field.name.to_sym, _field] }]
169
+ end
170
+
171
+ delegate :created_at, :updated_at, :to => :_get_record, :prefix => :flexi
172
+
173
+ private
174
+ def create_or_update
175
+ _id.nil? ? create : update
176
+ end
177
+
178
+ def create(*)
179
+ # Initialize AR record and store
180
+ record = _get_record
181
+ record.save
182
+
183
+ # Set Id and errors to the parent host object
184
+ self._id = record.id
185
+ @errors = record.errors
186
+
187
+ record
188
+ end
189
+
190
+ def update(*)
191
+ record = _get_record
192
+ record.values.destroy_all
193
+ record.update_attributes(values: _get_values)
194
+ record
195
+ end
196
+
197
+ def _get_record
198
+ _load_record_instance!
199
+ self._record
200
+ end
201
+
202
+ def _load_record_instance!
203
+ self._record ||= _record_load_or_initialize
204
+ end
205
+
206
+ def _record_load_or_initialize
207
+ collection = _find_or_update_or_build_collection!
208
+
209
+ if self._id.nil?
210
+ RECORD.new(
211
+ namespace: self.class.get_flexi_namespace,
212
+ collection: collection,
213
+ values: self.send(:_get_values)
214
+ )
215
+ else
216
+ RECORD.find(self._id)
217
+ end
218
+ end
219
+
220
+ # Return `Value` object based on flexi attributes
221
+ def _get_values
222
+ _fields_map = get_flexi_fields_map
223
+
224
+ @attributes.map do |k, v|
225
+ field = _fields_map[k]
226
+ raise "Field - #{k} not defined" if field.nil?
227
+ VALUE.new(:field => field, value: self.send(:"#{k}"))
228
+ end
229
+ end
230
+
231
+ # Find existing collection object
232
+ # If not found create new collection
233
+ # If found but schema is back dated
234
+ # update schema
235
+ def _find_or_update_or_build_collection!
236
+ return _flexi_collection if _flexi_collection.present?
237
+
238
+ # Find existing collection
239
+ self._flexi_collection = COLLECTION.where(
240
+ namespace: self.class.get_flexi_namespace,
241
+ name: self.class.flexi_collection_name,
242
+ partition_id: self.class.flexi_partition_id
243
+ ).first
244
+
245
+ # Update if schema changed
246
+ if self._flexi_collection
247
+ _update_schema
248
+ else
249
+ _build_collection
250
+ end
251
+
252
+ self._flexi_collection
253
+ end
254
+
255
+ # Check whether update is back dated
256
+ # This update is verified through comparing stored collection
257
+ # and new definition
258
+ def _update_schema
259
+ singular_label, plural_label = self.class.get_flexi_label
260
+ existing = self._flexi_collection
261
+
262
+ # Check labels
263
+ if existing.singular_label != singular_label
264
+ existing.update_attribute :singular_label, singular_label
265
+ end
266
+
267
+ if existing.plural_label != plural_label
268
+ existing.update_attribute :plural_label, plural_label
269
+ end
270
+
271
+
272
+ # Check fields
273
+ fields = _build_fields
274
+ if _fields_changed? fields, existing
275
+ # TODO: Dangerous need to fix it up
276
+ existing.fields.destroy_all
277
+ existing.update_attribute :fields, fields
278
+ end
279
+ end
280
+
281
+ def _fields_changed?(fields, existing)
282
+ added_or_removed = existing.fields.length != fields.length
283
+ name_changed = existing.fields.map(&:name).sort != fields.map(&:name).sort
284
+ type_changed = existing.fields.map(&:field_type).sort != fields.map(&:field_type).sort
285
+
286
+ added_or_removed || name_changed || type_changed
287
+ end
288
+
289
+ def _build_collection
290
+ singular_label, plural_label = self.class.get_flexi_label
291
+
292
+ self._flexi_collection = COLLECTION.create(
293
+ namespace: self.class.get_flexi_namespace,
294
+ name: self.class.flexi_collection_name,
295
+ partition_id: self.class.flexi_partition_id,
296
+ singular_label: singular_label,
297
+ plural_label: plural_label,
298
+
299
+ fields: _build_fields
300
+ )
301
+ end
302
+
303
+ def _build_fields
304
+ self.flexi_fields.map do |field|
305
+ params = {
306
+ namespace: self.class.get_flexi_namespace,
307
+ name: field.name.to_s,
308
+ partition_id: self.class.flexi_partition_id,
309
+ field_type: field.type,
310
+ singular_label: field.singular,
311
+ plural_label: field.plural
312
+ }
313
+
314
+ FIELD.send(:"find_or_create_by_#{params.keys.map(&:to_s).join('_and_')}", params)
315
+ end
316
+ end
317
+ end
318
+ end