baza_models 0.0.0 → 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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