baza_models 0.0.0 → 0.0.1

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.
Files changed (71) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop_todo.yml +35 -0
  3. data/Gemfile +9 -3
  4. data/Gemfile.lock +90 -51
  5. data/README.md +98 -3
  6. data/Rakefile +20 -17
  7. data/VERSION +1 -1
  8. data/baza_models.gemspec +149 -0
  9. data/config/best_project_practice_rubocop.yml +2 -0
  10. data/config/best_project_practice_rubocop_todo.yml +35 -0
  11. data/lib/baza_models.rb +15 -0
  12. data/lib/baza_models/autoloader.rb +135 -0
  13. data/lib/baza_models/baza_orm_adapter.rb +39 -0
  14. data/lib/baza_models/can_can_adapter.rb +9 -0
  15. data/lib/baza_models/class_translation.rb +44 -0
  16. data/lib/baza_models/errors.rb +24 -2
  17. data/lib/baza_models/model.rb +236 -115
  18. data/lib/baza_models/model/belongs_to_relations.rb +49 -0
  19. data/lib/baza_models/model/custom_validations.rb +24 -0
  20. data/lib/baza_models/model/delegation.rb +21 -0
  21. data/lib/baza_models/model/has_many_relations.rb +85 -0
  22. data/lib/baza_models/model/has_one_relations.rb +93 -0
  23. data/lib/baza_models/model/manipulation.rb +123 -0
  24. data/lib/baza_models/model/queries.rb +45 -0
  25. data/lib/baza_models/model/scopes.rb +19 -0
  26. data/lib/baza_models/model/translation_functionality.rb +30 -0
  27. data/lib/baza_models/model/validations.rb +95 -0
  28. data/lib/baza_models/query.rb +447 -0
  29. data/lib/baza_models/query/inspector.rb +74 -0
  30. data/lib/baza_models/query/not.rb +34 -0
  31. data/lib/baza_models/ransacker.rb +30 -0
  32. data/lib/baza_models/test_database_cleaner.rb +23 -0
  33. data/lib/baza_models/validators/base_validator.rb +14 -0
  34. data/lib/baza_models/validators/confirmation_validator.rb +12 -0
  35. data/lib/baza_models/validators/format_validator.rb +11 -0
  36. data/lib/baza_models/validators/length_validator.rb +16 -0
  37. data/lib/baza_models/validators/uniqueness_validator.rb +21 -0
  38. data/shippable.yml +3 -1
  39. data/spec/baza_models/autoloader_spec.rb +57 -0
  40. data/spec/baza_models/baza_orm_adapter_spec.rb +52 -0
  41. data/spec/baza_models/class_translation_spec.rb +25 -0
  42. data/spec/baza_models/factory_girl_spec.rb +13 -0
  43. data/spec/baza_models/model/belongs_to_relations_spec.rb +26 -0
  44. data/spec/baza_models/model/custom_validations_spec.rb +18 -0
  45. data/spec/baza_models/model/delgation_spec.rb +16 -0
  46. data/spec/baza_models/model/has_many_relations_spec.rb +68 -0
  47. data/spec/baza_models/model/has_one_relations_spec.rb +35 -0
  48. data/spec/baza_models/model/manipulation_spec.rb +25 -0
  49. data/spec/baza_models/model/queries_spec.rb +59 -0
  50. data/spec/baza_models/model/scopes_spec.rb +23 -0
  51. data/spec/baza_models/model/translate_functionality_spec.rb +13 -0
  52. data/spec/baza_models/model/validations_spec.rb +52 -0
  53. data/spec/baza_models/model_spec.rb +75 -98
  54. data/spec/baza_models/query/not_spec.rb +16 -0
  55. data/spec/baza_models/query_spec.rb +155 -0
  56. data/spec/baza_models/ransacker_spec.rb +15 -0
  57. data/spec/baza_models/validators/confirmation_validator_spec.rb +28 -0
  58. data/spec/baza_models/validators/format_validator_spec.rb +17 -0
  59. data/spec/baza_models/validators/length_validator_spec.rb +19 -0
  60. data/spec/baza_models/validators/uniqueness_validator_spec.rb +24 -0
  61. data/spec/factories/organization.rb +5 -0
  62. data/spec/factories/user.rb +7 -0
  63. data/spec/spec_helper.rb +17 -5
  64. data/spec/support/database_helper.rb +87 -0
  65. data/spec/test_classes/organization.rb +3 -0
  66. data/spec/test_classes/person.rb +3 -0
  67. data/spec/test_classes/role.rb +12 -0
  68. data/spec/test_classes/user.rb +40 -0
  69. data/spec/test_classes/user_passport.rb +3 -0
  70. metadata +146 -7
  71. data/spec/test_classes/user_test.rb +0 -17
@@ -0,0 +1,19 @@
1
+ module BazaModels::Model::Scopes
2
+ def self.included(base)
3
+ base.extend(ClassMethods)
4
+ end
5
+
6
+ module ClassMethods
7
+ def scope(name, blk)
8
+ @scopes ||= {}
9
+ name = name.to_sym
10
+
11
+ raise "Such a scope already exists" if @scopes.key?(name)
12
+ @scopes[name] = true
13
+
14
+ (class << self; self; end).__send__(:define_method, name) do
15
+ blk.call
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,30 @@
1
+ module BazaModels::Model::TranslationFunctionality
2
+ def self.included(base)
3
+ base.extend(ClassMethods)
4
+ end
5
+
6
+ module ClassMethods
7
+ def human_attribute_name(attribute_name)
8
+ class_name = StringCases.camel_to_snake(name)
9
+
10
+ keys = [
11
+ "baza_models.attributes.#{class_name}.#{attribute_name}",
12
+ "activerecord.attributes.#{class_name}.#{attribute_name}"
13
+ ]
14
+
15
+ keys.each do |key|
16
+ return I18n.t(key) if I18n.exists?(key)
17
+ end
18
+
19
+ StringCases.snake_to_camel(attribute_name)
20
+ end
21
+
22
+ def model_name
23
+ BazaModels::ClassTranslation.new(class: self)
24
+ end
25
+ end
26
+
27
+ def model_name
28
+ self.class.model_name
29
+ end
30
+ end
@@ -0,0 +1,95 @@
1
+ module BazaModels::Model::Validations
2
+ def self.included(base)
3
+ base.extend(ClassMethods)
4
+ end
5
+
6
+ def valid?
7
+ fire_callbacks(:before_validation)
8
+
9
+ if new_record?
10
+ fire_callbacks(:before_validation_on_create)
11
+ else
12
+ fire_callbacks(:before_validation_on_update)
13
+ end
14
+
15
+ reset_errors
16
+
17
+ validators = self.class.__validators
18
+
19
+ merged_data = @data.merge(@changes)
20
+ merged_data.each do |attribute_name, attribute_value|
21
+ next unless validators.key?(attribute_name)
22
+
23
+ validators[attribute_name].each do |validator|
24
+ next unless validator.fire?(self)
25
+ validator.validate(self, attribute_value)
26
+ end
27
+ end
28
+
29
+ execute_custom_validations
30
+ fire_callbacks(:after_validation)
31
+
32
+ if new_record?
33
+ fire_callbacks(:after_validation_on_create)
34
+ else
35
+ fire_callbacks(:after_validation_on_update)
36
+ end
37
+
38
+ @errors.empty?
39
+ end
40
+
41
+ module ClassMethods
42
+ def validates(*attribute_names, args)
43
+ special_args = {
44
+ if: args.delete(:if)
45
+ }
46
+
47
+ attribute_names.each do |attribute_name|
48
+ args.each do |validator_name, _validator_args|
49
+ validator_camel_name = StringCases.snake_to_camel(validator_name)
50
+ class_name = "#{validator_camel_name}Validator"
51
+
52
+ __validators[attribute_name] ||= []
53
+ __validators[attribute_name] << BazaModels::Validators.const_get(class_name).new(attribute_name, args.merge(special_args))
54
+ end
55
+ end
56
+ end
57
+
58
+ def validates_confirmation_of(*args)
59
+ validate_shortcut(:confirmation, args)
60
+ end
61
+
62
+ def validates_format_of(*args)
63
+ validate_shortcut(:format, args)
64
+ end
65
+
66
+ def validates_length_of(*args)
67
+ validate_shortcut(:length, args)
68
+ end
69
+
70
+ def validates_presence_of(*args)
71
+ validate_shortcut(:presence, args)
72
+ end
73
+
74
+ def validates_uniqueness_of(*args)
75
+ validate_shortcut(:uniqueness, args)
76
+ end
77
+
78
+ def validate_shortcut(type, args)
79
+ if args.last.is_a?(Hash)
80
+ before_opts = args.pop
81
+
82
+ opts = {type => before_opts}
83
+ opts[:if] = before_opts.delete(:if) if before_opts.key?(:if)
84
+ else
85
+ opts = {type => true}
86
+ end
87
+
88
+ validates(*args, opts)
89
+ end
90
+
91
+ def __validators
92
+ @validators ||= {}
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,447 @@
1
+ require "array_enumerator"
2
+
3
+ class BazaModels::Query
4
+ path = "#{File.dirname(__FILE__)}/query"
5
+
6
+ autoload :Inspector, "#{path}/inspector"
7
+ autoload :Not, "#{path}/not"
8
+
9
+ attr_accessor :_previous_model, :_relation
10
+
11
+ def initialize(args)
12
+ @args = args
13
+ @model = @args[:model]
14
+ @db = @model.db
15
+
16
+ raise "No database?" unless @db
17
+
18
+ @selects = args[:selects] || []
19
+ @wheres = args[:wheres] || []
20
+ @includes = args[:includes] || []
21
+ @joins = args[:joins] || []
22
+ @groups = args[:groups] || []
23
+ @orders = args[:orders] || []
24
+ @limit = args[:limit]
25
+
26
+ @joins_tracker = {}
27
+ end
28
+
29
+ def all
30
+ self
31
+ end
32
+
33
+ def any?
34
+ if @db.query(clone.select(:id).limit(1).to_sql).fetch
35
+ return true
36
+ else
37
+ return false
38
+ end
39
+ end
40
+
41
+ def empty?
42
+ !any?
43
+ end
44
+
45
+ def none?
46
+ !any?
47
+ end
48
+
49
+ def count
50
+ if @_previous_model && @_previous_model.new_record?
51
+ return autoloaded_cache_or_create.length
52
+ else
53
+ query = clone
54
+
55
+ query.instance_variable_set(:@selects, [])
56
+ query = clone.select("COUNT(*) AS count")
57
+
58
+ @db.query(query.to_sql).fetch.fetch(:count)
59
+ end
60
+ end
61
+
62
+ def length
63
+ count
64
+ end
65
+
66
+ def find(id)
67
+ clone.where(id: id).limit(1).to_enum.first
68
+ end
69
+
70
+ def first
71
+ return autoloaded_cache.first if should_use_autoload?
72
+
73
+ query = clone.limit(1)
74
+
75
+ orders = query.instance_variable_get(:@orders)
76
+ query = query.order(:id) if orders.empty?
77
+
78
+ query.to_enum.first
79
+ end
80
+
81
+ def last
82
+ return autoloaded_cache.last if should_use_autoload?
83
+
84
+ query = clone.limit(1)
85
+
86
+ orders = query.instance_variable_get(:@orders)
87
+ query = query.order(:id) if orders.empty?
88
+
89
+ query.reverse_order.to_enum.first
90
+ end
91
+
92
+ def select(select)
93
+ if select.is_a?(Symbol)
94
+ @selects << "`#{@model.table_name}`.`#{select}`"
95
+ else
96
+ @selects << select
97
+ end
98
+
99
+ self
100
+ end
101
+
102
+ def offset(offset)
103
+ @offset = offset
104
+ self
105
+ end
106
+
107
+ def limit(limit)
108
+ @limit = limit
109
+ self
110
+ end
111
+
112
+ def includes(name)
113
+ @includes << name
114
+ self
115
+ end
116
+
117
+ def where(args = nil)
118
+ if args.is_a?(String)
119
+ @wheres << "(#{args})"
120
+ elsif args.is_a?(Array)
121
+ str = args.shift
122
+
123
+ args.each do |arg|
124
+ if arg.is_a?(Symbol)
125
+ arg = "`#{@model.table_name}`.`#{@db.escape_column(arg)}`"
126
+ elsif arg.is_a?(FalseClass)
127
+ arg = "0"
128
+ elsif arg.is_a?(TrueClass)
129
+ arg = "1"
130
+ else
131
+ arg = "'#{@db.esc(arg)}'"
132
+ end
133
+
134
+ str.sub!("?", arg)
135
+ end
136
+
137
+ @wheres << "(#{str})"
138
+ elsif args == nil
139
+ return Not.new(query: self)
140
+ else
141
+ args.each do |key, value|
142
+ if value.is_a?(Hash)
143
+ value.each do |hash_key, hash_value|
144
+ @wheres << "`#{key}`.`#{hash_key}` = '#{@db.esc(hash_value)}'"
145
+ end
146
+ else
147
+ @wheres << "`#{@model.table_name}`.`#{key}` = '#{@db.esc(value)}'"
148
+ end
149
+ end
150
+ end
151
+
152
+ self
153
+ end
154
+
155
+ def joins(*arguments)
156
+ BazaModels::Query::Inspector.new(
157
+ query: self,
158
+ model: @model,
159
+ argument: arguments,
160
+ joins: @joins,
161
+ joins_tracker: @joins_tracker
162
+ ).execute
163
+
164
+ self
165
+ end
166
+
167
+ def group(name)
168
+ if name.is_a?(Symbol)
169
+ @groups << "`#{@model.table_name}`.`#{name}`"
170
+ elsif name.is_a?(String)
171
+ @groups << name
172
+ else
173
+ raise "Didn't know how to group by that argument: #{name}"
174
+ end
175
+
176
+ self
177
+ end
178
+
179
+ def order(name)
180
+ if name.is_a?(Symbol)
181
+ @orders << "`#{@model.table_name}`.`#{name}`"
182
+ elsif name.is_a?(String)
183
+ @orders << name
184
+ else
185
+ raise "Didn't know how to order by that argument: #{name}"
186
+ end
187
+
188
+ self
189
+ end
190
+
191
+ def reverse_order
192
+ @reverse_order = true
193
+ self
194
+ end
195
+
196
+ def to_enum
197
+ return autoloaded_cache if should_use_autoload?
198
+
199
+ array_enum = ArrayEnumerator.new do |yielder|
200
+ @db.query(to_sql).each do |data|
201
+ yielder << @model.new(data, init: true)
202
+ end
203
+ end
204
+
205
+ if @includes.empty?
206
+ return array_enum
207
+ else
208
+ array = array_enum.to_a
209
+
210
+ if @includes.any? && array.any?
211
+ autoloader = BazaModels::Autoloader.new(
212
+ models: array,
213
+ autoloads: @includes,
214
+ db: @db
215
+ )
216
+ autoloader.autoload
217
+ end
218
+
219
+ return array
220
+ end
221
+ end
222
+
223
+ def each
224
+ to_enum.each do |model|
225
+ yield model
226
+ end
227
+ end
228
+
229
+ def find_each
230
+ query = clone
231
+ query.instance_variable_set(:@order, [])
232
+ query.instance_variable_set(:@limit, nil)
233
+ query = query.order(:id)
234
+
235
+ offset = 0
236
+
237
+ loop do
238
+ query = query.offset(offset, 1000)
239
+ offset += 1000
240
+
241
+ count = 0
242
+ query.each do |model|
243
+ yield model
244
+ count += 1
245
+ end
246
+
247
+ break if count == 0
248
+ end
249
+ end
250
+
251
+ def find_first(args)
252
+ where(args).first
253
+ end
254
+
255
+ def to_a
256
+ to_enum.to_a
257
+ end
258
+
259
+ def to_sql
260
+ sql = "SELECT "
261
+
262
+ if @selects.empty?
263
+ sql << "`#{@model.table_name}`.*"
264
+ else
265
+ sql << @selects.join(", ")
266
+ end
267
+
268
+ sql << " FROM `#{@model.table_name}`"
269
+
270
+ unless @joins.empty?
271
+ @joins.each do |join|
272
+ sql << " #{join}"
273
+ end
274
+ end
275
+
276
+ unless @wheres.empty?
277
+ sql << " WHERE "
278
+
279
+ first = true
280
+ @wheres.each do |where|
281
+ if first == true
282
+ first = false
283
+ else
284
+ sql << " AND "
285
+ end
286
+
287
+ sql << where
288
+ end
289
+ end
290
+
291
+ unless @groups.empty?
292
+ sql << " GROUP BY "
293
+
294
+ first = true
295
+ @groups.each do |group|
296
+ if first
297
+ first = false
298
+ else
299
+ sql << ", "
300
+ end
301
+
302
+ sql << group
303
+ end
304
+ end
305
+
306
+ unless @orders.empty?
307
+ sql << " ORDER BY "
308
+
309
+ first = true
310
+ @orders.each do |order|
311
+ if first
312
+ first = false
313
+ else
314
+ sql << ", "
315
+ end
316
+
317
+ if @reverse_order
318
+ if order.match(/\s+desc/i)
319
+ order = order.gsub(/\s+desc/i, " ASC")
320
+ elsif order.match(/\s+asc/i)
321
+ order = order.gsub(/\s+asc/i, " DESC")
322
+ else
323
+ order = "#{order} DESC"
324
+ end
325
+ end
326
+
327
+ sql << order
328
+ end
329
+ end
330
+
331
+ if @limit && @offset
332
+ sql << " LIMIT #{@offset.to_i}, #{@limit.to_i}"
333
+ elsif @limit
334
+ sql << " LIMIT #{@limit.to_i}"
335
+ end
336
+
337
+ sql.strip
338
+ end
339
+
340
+ def destroy_all
341
+ each(&:destroy!)
342
+ end
343
+
344
+ def to_s
345
+ "#<BazaModels::Query class=#{@model.name} wheres=#{@wheres}>"
346
+ end
347
+
348
+ def inspect
349
+ to_s
350
+ end
351
+
352
+ def <<(model)
353
+ raise "No previous model set" unless @_previous_model
354
+ raise "No relation" unless @_relation
355
+
356
+ if model.persisted?
357
+ model.update_attributes!(@_relation.fetch(:foreign_key) => @_previous_model.id)
358
+ else
359
+ autoloaded_cache_or_create << model
360
+ end
361
+
362
+ self
363
+ end
364
+
365
+ # CanCan supports
366
+ def accessible_by(ability, action = :index)
367
+ ability.model_adapter(self, action).database_records
368
+ end
369
+
370
+ def <=(_other)
371
+ false
372
+ end
373
+
374
+ def sanitize_sql(value)
375
+ return value if value.is_a?(Array) || value.is_a?(Integer) || value.is_a?(Integer)
376
+ "'#{@db.esc(value)}'"
377
+ end
378
+
379
+ def page(some_page)
380
+ some_page ||= 1
381
+ offset = (some_page.to_i - 1) * per
382
+
383
+ clone.offset(offset).limit(30)
384
+ end
385
+
386
+ def per
387
+ @per ||= 30
388
+ end
389
+
390
+ def total_pages
391
+ pages_count = (count.to_f / @per.to_f)
392
+ pages_count = 1 if pages_count.nan? || pages_count == Float::INFINITY
393
+ pages_count = pages_count.to_i
394
+ pages_count = 1 if pages_count == 0
395
+ pages_count
396
+ end
397
+
398
+ private
399
+
400
+ def should_use_autoload?
401
+ !any_mods? && autoloaded_on_previous_model?
402
+ end
403
+
404
+ def autoloaded_cache_or_create
405
+ @_previous_model.autoloads[@_relation.fetch(:relation_name)] ||= []
406
+ autoloaded_cache
407
+ end
408
+
409
+ def autoloaded_cache
410
+ @_previous_model.autoloads.fetch(@_relation.fetch(:relation_name))
411
+ end
412
+
413
+ def any_mods?
414
+ @groups.any? || @includes.any? || @orders.any? || @joins.any? || any_wheres_other_than_relation?
415
+ end
416
+
417
+ def any_wheres_other_than_relation?
418
+ if @_previous_model && @_relation && @wheres.length == 1
419
+ looks_like = "`#{@_relation.fetch(:table_name)}`.`#{@_relation.fetch(:foreign_key)}` = '#{@_previous_model.id}'"
420
+
421
+ return false if @wheres.first == looks_like
422
+ end
423
+
424
+ true
425
+ end
426
+
427
+ def autoloaded_on_previous_model?
428
+ if @_previous_model && @_relation
429
+ return true if @_previous_model.autoloads.include?(@_relation.fetch(:relation_name))
430
+ end
431
+
432
+ false
433
+ end
434
+
435
+ def clone
436
+ BazaModels::Query.new(
437
+ model: @model,
438
+ selects: @selects.dup,
439
+ wheres: @wheres.dup,
440
+ joins: @joins.dup,
441
+ includes: @includes.dup,
442
+ groups: @groups.dup,
443
+ orders: @orders.dup,
444
+ limit: @limit
445
+ )
446
+ end
447
+ end