simple_master 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.
Files changed (46) hide show
  1. checksums.yaml +7 -0
  2. data/lib/simple_master/active_record/belongs_to_master_polymorphic_reflection.rb +11 -0
  3. data/lib/simple_master/active_record/belongs_to_polymorphic_association.rb +17 -0
  4. data/lib/simple_master/active_record/belongs_to_polymorphic_builder.rb +21 -0
  5. data/lib/simple_master/active_record/extension.rb +183 -0
  6. data/lib/simple_master/active_record/preloader_association_extension.rb +11 -0
  7. data/lib/simple_master/active_record.rb +12 -0
  8. data/lib/simple_master/loader/dataset_loader.rb +20 -0
  9. data/lib/simple_master/loader/marshal_loader.rb +15 -0
  10. data/lib/simple_master/loader/query_loader.rb +63 -0
  11. data/lib/simple_master/loader.rb +55 -0
  12. data/lib/simple_master/master/association/belongs_to_association.rb +79 -0
  13. data/lib/simple_master/master/association/belongs_to_polymorphic_association.rb +79 -0
  14. data/lib/simple_master/master/association/has_many_association.rb +53 -0
  15. data/lib/simple_master/master/association/has_many_through_association.rb +64 -0
  16. data/lib/simple_master/master/association/has_one_association.rb +57 -0
  17. data/lib/simple_master/master/association.rb +50 -0
  18. data/lib/simple_master/master/column/bitmask_column.rb +74 -0
  19. data/lib/simple_master/master/column/boolean_column.rb +51 -0
  20. data/lib/simple_master/master/column/enum_column.rb +96 -0
  21. data/lib/simple_master/master/column/float_column.rb +21 -0
  22. data/lib/simple_master/master/column/id_column.rb +31 -0
  23. data/lib/simple_master/master/column/integer_column.rb +21 -0
  24. data/lib/simple_master/master/column/json_column.rb +27 -0
  25. data/lib/simple_master/master/column/polymorphic_type_column.rb +44 -0
  26. data/lib/simple_master/master/column/sti_type_column.rb +21 -0
  27. data/lib/simple_master/master/column/string_column.rb +17 -0
  28. data/lib/simple_master/master/column/symbol_column.rb +23 -0
  29. data/lib/simple_master/master/column/time_column.rb +38 -0
  30. data/lib/simple_master/master/column.rb +138 -0
  31. data/lib/simple_master/master/dsl.rb +239 -0
  32. data/lib/simple_master/master/editable.rb +155 -0
  33. data/lib/simple_master/master/filterable.rb +47 -0
  34. data/lib/simple_master/master/queryable.rb +75 -0
  35. data/lib/simple_master/master/storable.rb +20 -0
  36. data/lib/simple_master/master/validatable.rb +216 -0
  37. data/lib/simple_master/master.rb +417 -0
  38. data/lib/simple_master/schema.rb +49 -0
  39. data/lib/simple_master/storage/dataset.rb +172 -0
  40. data/lib/simple_master/storage/ondemand_table.rb +68 -0
  41. data/lib/simple_master/storage/table.rb +197 -0
  42. data/lib/simple_master/storage/test_table.rb +69 -0
  43. data/lib/simple_master/storage.rb +11 -0
  44. data/lib/simple_master/version.rb +5 -0
  45. data/lib/simple_master.rb +62 -0
  46. metadata +128 -0
@@ -0,0 +1,216 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SimpleMaster
4
+ class Master
5
+ class Errors
6
+ def initialize
7
+ @errors = []
8
+ end
9
+
10
+ def add(attribute, type = :invalid, **options)
11
+ @errors << [attribute, type, options]
12
+ end
13
+
14
+ delegate :empty?, :map, :each, :size, to: :@errors
15
+ end
16
+
17
+ # Validator works similar to ActiveRecord::Validations::UniquenessValidator.
18
+ class UniquenessValidator < ActiveModel::EachValidator # :nodoc:
19
+ def initialize(options)
20
+ if options[:conditions] && !options[:conditions].respond_to?(:call)
21
+ fail ArgumentError, "#{options[:conditions]} was passed as :conditions but is not callable. " \
22
+ "Pass a callable instead: `conditions: -> { where(approved: true) }`"
23
+ end
24
+ unless Array(options[:scope]).all? { |scope| scope.respond_to?(:to_sym) }
25
+ fail ArgumentError, "#{options[:scope]} is not supported format for :scope option. " \
26
+ "Pass a symbol or an array of symbols instead: `scope: :user_id`"
27
+ end
28
+ super
29
+ @klass = options[:class]
30
+ end
31
+
32
+ def validate_each(record, attribute, value)
33
+ unless @klass.select { _1.send(attribute) == value }.one?
34
+ error_options = options.except(:case_sensitive, :scope, :conditions)
35
+ error_options[:value] = value
36
+
37
+ record.errors.add(attribute, :taken, **error_options)
38
+ end
39
+ end
40
+ end
41
+
42
+ module Validatable
43
+ extend ActiveSupport::Concern
44
+
45
+ #: () -> bool
46
+ def valid?
47
+ _run_validate_callbacks
48
+ # Refer directly instead of errors.empty? to avoid creating many errors objects
49
+ !Thread.current[:errors]&.[](self).present?
50
+ end
51
+
52
+ # @rbs skip
53
+ def validate
54
+ _run_validate_callbacks
55
+ end
56
+
57
+ def _run_validate_callbacks
58
+ self.class._validate_callbacks.each do |validation_proc|
59
+ instance_exec(&validation_proc)
60
+ rescue => e
61
+ errors.add(:base, "Error occurred while validation: #{e}")
62
+ end
63
+ end
64
+
65
+ def errors
66
+ Thread.current[:errors] ||= {}
67
+ Thread.current[:errors][self] ||= Errors.new
68
+ end
69
+
70
+ def run_proc_or_call(symbol_or_proc)
71
+ return send(symbol_or_proc) if symbol_or_proc.is_a? Symbol
72
+ return instance_eval(&symbol_or_proc) if symbol_or_proc.arity == 1
73
+ instance_exec(&symbol_or_proc)
74
+ end
75
+
76
+ def read_attribute_for_validation(attribute)
77
+ send(attribute)
78
+ end
79
+
80
+ class_methods do
81
+ # Validation procs to be called by current class on each record
82
+ def _validate_procs
83
+ @_validate_procs ||= []
84
+ end
85
+
86
+ # Validation procs to be called by current and parent classes on each record
87
+ # (Similar to ActiveModel interface, but returns procs.)
88
+ def _validate_callbacks
89
+ if superclass < SimpleMaster::Master
90
+ superclass._validate_callbacks + _validate_procs
91
+ else
92
+ _validate_procs
93
+ end
94
+ end
95
+
96
+ # ref: https://github.com/ruby/gem_rbs_collection/blob/54fe53bbe56e5ff4e9bc2cb2c95426f13abe77ca/gems/activemodel/6.0/activemodel.rbs#L48-L50
97
+ # @rbs!
98
+ # type condition[T] = Symbol | ^(T) [self: T] -> boolish
99
+ # type conditions[T] = condition[T] | Array[condition[T]]
100
+
101
+ # (ActiveModel interface compliant.)
102
+ # @rbs *validations: untyped
103
+ # @rbs **options: { on?: Symbol | Array[Symbol], if?: conditions[instance], unless?: conditions[instance] }
104
+ # @rbs &: ?{ (instance) [self: instance] -> void }
105
+ # @rbs return: void
106
+ def validate(*validations, **options, &)
107
+ conditions = options.slice(:if, :unless)
108
+
109
+ condition_procs = validation_condition_procs(conditions)
110
+
111
+ validations.each do |validation|
112
+ _validate_procs << proc {
113
+ run_proc_or_call(validation) if condition_procs.all? { run_proc_or_call(_1) }
114
+ }
115
+ end
116
+ _validate_procs << -> { instance_exec(&) if condition_procs.all? { run_proc_or_call(_1) } } if block_given?
117
+ end
118
+
119
+ # (ActiveModel interface compliant.)
120
+ # @rbs skip
121
+ def validates(*attributes)
122
+ defaults = attributes.extract_options!.dup
123
+ validations = defaults.slice!(:if, :unless, :on, :allow_blank, :allow_nil, :strict)
124
+
125
+ fail ArgumentError, "You need to supply at least one attribute" if attributes.empty?
126
+ fail ArgumentError, "You need to supply at least one validation" if validations.empty?
127
+ warn "Option :on is not supported" if defaults[:on]
128
+ warn "Option :strict is not supported" if defaults[:strict]
129
+
130
+ defaults[:attributes] = attributes
131
+
132
+ validations.each do |key, options|
133
+ key = "#{key.to_s.camelize}Validator"
134
+
135
+ validator = find_validator(key) || find_validator("ActiveModel::Validations::#{key}")
136
+
137
+ fail ArgumentError, "Unknown validator: '#{key}'" unless validator
138
+
139
+ next unless options
140
+
141
+ validates_with(validator, defaults.merge(_parse_validates_options(options)))
142
+ end
143
+ end
144
+
145
+ # (ActiveModel interface compliant.)
146
+ # block args: (record, attribute, value)
147
+ def validates_each(*attributes, **options)
148
+ conditions = options.slice(:if, :unless)
149
+ condition_procs = validation_condition_procs(conditions)
150
+ attributes.each do |attribute|
151
+ _validate_procs << proc {
152
+ value = send(attribute)
153
+ next if options[:allow_nil] && value.nil?
154
+ next if options[:allow_blank] && value.blank?
155
+ yield(self, attribute, value) if condition_procs.all? { run_proc_or_call(_1) }
156
+ }
157
+ end
158
+ end
159
+
160
+ # (ActiveModel interface compliant.)
161
+ def validates_with(*args, &)
162
+ options = args.extract_options!
163
+ options[:class] = self
164
+
165
+ conditions = options.slice(:if, :unless)
166
+
167
+ condition_procs = validation_condition_procs(conditions)
168
+
169
+ args.each do |klass|
170
+ validator = klass.new(options.dup, &)
171
+
172
+ _validate_procs << proc {
173
+ validator.validate(self) if condition_procs.all? { run_proc_or_call(_1) }
174
+ }
175
+ end
176
+ end
177
+
178
+ private
179
+
180
+ def _parse_validates_options(options)
181
+ case options
182
+ when TrueClass
183
+ EMPTY_HASH
184
+ when Hash
185
+ options
186
+ when Range, Array
187
+ { in: options }
188
+ else
189
+ { with: options }
190
+ end
191
+ end
192
+
193
+ def find_validator(key)
194
+ const_get(key)
195
+ rescue NameError
196
+ false
197
+ end
198
+
199
+ # processing :if, :unless conditions
200
+ def validation_condition_procs(conditions)
201
+ conditions.flat_map do |key, value|
202
+ if key == :if
203
+ Array.wrap(value)
204
+ elsif key == :unless
205
+ Array.wrap(value).map { |v|
206
+ -> { !run_proc_or_call(v) }
207
+ }
208
+ else
209
+ EMPTY_ARRAY
210
+ end
211
+ end
212
+ end
213
+ end
214
+ end
215
+ end
216
+ end
@@ -0,0 +1,417 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SimpleMaster
4
+ class Master
5
+ autoload :Column, "simple_master/master/column"
6
+ autoload :Association, "simple_master/master/association"
7
+ autoload :Editable, "simple_master/master/editable"
8
+ autoload :Dsl, "simple_master/master/dsl"
9
+ autoload :Filterable, "simple_master/master/filterable"
10
+ autoload :Storable, "simple_master/master/storable"
11
+ autoload :Queryable, "simple_master/master/queryable"
12
+ autoload :Validatable, "simple_master/master/validatable"
13
+
14
+ extend Dsl
15
+ extend Filterable
16
+ extend Storable
17
+ extend Queryable
18
+ include Validatable
19
+
20
+ class << self
21
+ attr_accessor :abstract_class
22
+
23
+ #: () -> bool
24
+ def abstract_class?
25
+ !!abstract_class
26
+ end
27
+
28
+ #: () -> bool
29
+ def table_exists?
30
+ table_available?
31
+ end
32
+
33
+ # Auto-generated methods live in a module so they can be overridden.
34
+ def simple_master_module
35
+ @simple_master_module ||= Module.new
36
+ end
37
+
38
+ # def simple_master_class_methods
39
+ # @simple_master_class_methods ||= Module.new
40
+ # end
41
+
42
+ #: () -> Array[Integer]
43
+ def ids
44
+ id_hash.keys
45
+ end
46
+
47
+ #: () -> Array[SimpleMaster::Master::Column]
48
+ def columns
49
+ @columns ||= []
50
+ end
51
+
52
+ #: () -> Array[SimpleMaster::Master::Column]
53
+ def all_columns
54
+ if superclass < SimpleMaster::Master
55
+ (superclass.all_columns + columns).reverse.uniq(&:name).reverse
56
+ else
57
+ columns
58
+ end
59
+ end
60
+
61
+ def update_column_info(name)
62
+ if (column_index = columns.find_index { _1.name == name })
63
+ columns[column_index] = yield(columns[column_index])
64
+ return
65
+ end
66
+
67
+ if (column_from_parent = superclass.all_columns.find { _1.name == name })
68
+ new_column = yield(column_from_parent)
69
+ columns << new_column
70
+ return
71
+ end
72
+
73
+ fail "Column #{name} not found on #{self}!"
74
+ end
75
+
76
+ #: () -> Array[String]
77
+ def column_names
78
+ all_columns.map(&:name)
79
+ end
80
+
81
+ # Unlike ActiveRecord, we ensure column existence can be checked.
82
+ #: () -> Hash[String, SimpleMaster::Master::Column]
83
+ def columns_hash
84
+ all_columns.index_by(&:name).with_indifferent_access
85
+ end
86
+
87
+ def has_one_associations
88
+ @has_one_associations ||= []
89
+ end
90
+
91
+ def all_has_one_associations
92
+ if superclass < SimpleMaster::Master
93
+ superclass.all_has_one_associations + has_one_associations
94
+ else
95
+ has_one_associations
96
+ end
97
+ end
98
+
99
+ def has_many_associations
100
+ @has_many_associations ||= []
101
+ end
102
+
103
+ def all_has_many_associations
104
+ if superclass < SimpleMaster::Master
105
+ superclass.all_has_many_associations + has_many_associations
106
+ else
107
+ has_many_associations
108
+ end
109
+ end
110
+
111
+ def belongs_to_associations
112
+ @belongs_to_associations ||= []
113
+ end
114
+
115
+ def all_belongs_to_associations
116
+ if superclass < SimpleMaster::Master
117
+ superclass.all_belongs_to_associations + belongs_to_associations
118
+ else
119
+ belongs_to_associations
120
+ end
121
+ end
122
+
123
+ def group_keys
124
+ @group_keys || all_columns.select(&:group_key).map(&:name)
125
+ end
126
+
127
+ def sti_base_class
128
+ nil
129
+ end
130
+
131
+ def sti_column
132
+ nil
133
+ end
134
+
135
+ # @rbs skip
136
+ alias inheritance_column sti_column
137
+
138
+ def primary_key
139
+ :id
140
+ end
141
+
142
+ # @rbs skip
143
+ alias polymorphic_name name
144
+
145
+ def has_query_constraints?
146
+ false
147
+ end
148
+
149
+ def composite_primary_key?
150
+ false
151
+ end
152
+
153
+ def table_name
154
+ ActiveSupport::Inflector.tableize(base_class.to_s).tr("/", "_")
155
+ end
156
+
157
+ def current_scope
158
+ fail
159
+ # pp caller
160
+ # raise
161
+ # @current_scope ||= Scope.new
162
+ end
163
+
164
+ #: () -> void
165
+ def init(database_available = true, for_test: false)
166
+ @object_cache = Hash.new { |k, v| k[v] = {} }
167
+
168
+ _build_dsl
169
+ _build_columns(for_test)
170
+ _build_associations(database_available, for_test)
171
+ _build_sti_methods
172
+
173
+ # Override on test env.
174
+ define_method(:cache_object) { |_column, input, &block| block.call(input) } if for_test
175
+ end
176
+
177
+ #: () -> void
178
+ def dsl_initializers
179
+ @dsl_initializers ||= []
180
+ end
181
+
182
+ #: () -> void
183
+ def _build_dsl
184
+ dsl_initializers.each(&:call)
185
+ end
186
+
187
+ #: () -> void
188
+ def _build_columns(for_test)
189
+ columns.each do |column|
190
+ column.init(self, for_test)
191
+ end
192
+
193
+ @group_keys = group_keys
194
+
195
+ simple_master_module.class_eval <<-RUBY, __FILE__, __LINE__ + 1
196
+ def attributes
197
+ {
198
+ #{
199
+ all_columns.map { |column| "#{column.name}: #{column.name}" }.join(",\n")
200
+ }
201
+ }
202
+ end
203
+
204
+ def db_attributes
205
+ {
206
+ #{
207
+ all_columns.map { |column| "#{column.db_column_name}: #{column.name}_value_for_csv" }.join(",\n")
208
+ }
209
+ }
210
+ end
211
+
212
+ def globalized_db_attributes
213
+ {
214
+ #{
215
+ all_columns.map { |column| "#{column.db_column_name}: #{column.name}_value_for_csv" }.join(",\n")
216
+ },
217
+ #{
218
+ all_columns.select { |column| column.options[:globalize] }.map { |column|
219
+ "_globalized_#{column.name}: _globalized_#{column.name}"
220
+ }.join(",\n")
221
+ }
222
+ }
223
+ end
224
+
225
+ def create_copy
226
+ r = self.class.default_object.dup
227
+ #{
228
+ all_columns.map { |column| "r.#{column.name} = #{column.name}" }.join("\n")
229
+ }
230
+ r
231
+ end
232
+ RUBY
233
+
234
+ build_default_object
235
+ end
236
+
237
+ #: () -> void
238
+ def _build_associations(is_database_available, for_test)
239
+ associations = has_one_associations + has_many_associations + belongs_to_associations
240
+
241
+ associations.each do |association|
242
+ next if !is_database_available && association.is_active_record?
243
+ association.init(self)
244
+ association.init_for_test(self) if for_test
245
+ end
246
+ end
247
+
248
+ def _build_sti_methods
249
+ extend SubClassStorable if sti_sub_class?
250
+ end
251
+
252
+ def inherited(subclass)
253
+ super
254
+ subclass.include(subclass.simple_master_module)
255
+ end
256
+
257
+ attr_reader :class_method_cache_info #: Array[[Symbol, Proc]]
258
+ attr_reader :instance_methods_need_tap #: Array[Symbol]
259
+ attr_reader :default_object #: self
260
+ attr_reader :object_cache #: Hash[Symbol, untyped]
261
+
262
+ # @rbs return: Array[[Symbol, Proc]]
263
+ def all_class_method_cache_info
264
+ if superclass < SimpleMaster::Master
265
+ superclass.all_class_method_cache_info + (class_method_cache_info || EMPTY_ARRAY)
266
+ else
267
+ class_method_cache_info || EMPTY_ARRAY
268
+ end
269
+ end
270
+
271
+ def scope(*_option)
272
+ end
273
+
274
+ def sti_class?
275
+ !!sti_base_class
276
+ end
277
+
278
+ def sti_base_class?
279
+ sti_base_class == self
280
+ end
281
+
282
+ def sti_sub_class?
283
+ !!sti_base_class && sti_base_class != self
284
+ end
285
+
286
+ def base_class
287
+ sti_base_class || self
288
+ end
289
+
290
+ def base_class?
291
+ !sti_base_class || sti_base_class?
292
+ end
293
+
294
+ def globalized?
295
+ all_columns.any? { |column| column.options[:globalize] }
296
+ end
297
+
298
+ #: () -> void
299
+ def reset_object_cache
300
+ @object_cache.clear
301
+ end
302
+
303
+ #: () -> void
304
+ def build_default_object
305
+ default_object = allocate
306
+
307
+ all_columns.each do |column|
308
+ # Assign nil even when undefined to keep this fast
309
+ default_object.send :"#{column.name}=", column.options[:default].freeze
310
+ default_object.send :"_globalized_#{column.name}=", nil if column.options[:globalize]
311
+ end
312
+ default_object.type = name if sti_class?
313
+
314
+ @default_object = default_object
315
+ end
316
+ end
317
+
318
+ #: (Symbol, untyped) { (untyped) -> untyped } -> untyped
319
+ def cache_object(column, input)
320
+ cache = self.class.base_class.object_cache[column]
321
+ cache.fetch(input) { cache[input] = yield(input) }
322
+ end
323
+
324
+ def instance_store
325
+ store = RequestStore.store
326
+ instance_store = store.fetch(:instance_store) { store[:instance_store] = Hash.new { |hash, key| hash[key] = {} }.compare_by_identity }
327
+ instance_store[self]
328
+ end
329
+
330
+ def has_many_store
331
+ RequestStore.store[:has_many_store] ||= {}.compare_by_identity
332
+ RequestStore.store[:has_many_store][self] ||= {}
333
+ end
334
+
335
+ def belongs_to_store
336
+ RequestStore.store[:belongs_to_store] ||= {}.compare_by_identity
337
+ RequestStore.store[:belongs_to_store][self] ||= {}
338
+ end
339
+
340
+ def dirty!
341
+ end
342
+
343
+ alias [] send
344
+
345
+ def _read_attribute(key)
346
+ instance_variable_get("@#{key}")
347
+ end
348
+
349
+ alias read_attribute _read_attribute
350
+
351
+ def slice(*attrs)
352
+ attrs.index_with { send(_1) }
353
+ end
354
+
355
+ def as_json(_option = nil)
356
+ attributes
357
+ end
358
+
359
+ # for comparing in test.
360
+ def json_slice(*attrs)
361
+ JSON.parse(slice(*attrs).to_json)
362
+ end
363
+
364
+ #: () -> bool
365
+ def destroyed?
366
+ false
367
+ end
368
+
369
+ #: () -> bool
370
+ def new_record?
371
+ false
372
+ end
373
+
374
+ #: () -> bool
375
+ def has_changes_to_save?
376
+ false
377
+ end
378
+
379
+ #: () -> bool
380
+ def marked_for_destruction?
381
+ false
382
+ end
383
+
384
+ def save(*_)
385
+ end
386
+
387
+ def save!(*_)
388
+ end
389
+
390
+ def update(*_)
391
+ end
392
+
393
+ def update!(*_)
394
+ end
395
+
396
+ def destroy
397
+ end
398
+
399
+ def destroy!
400
+ end
401
+
402
+ def inspect
403
+ "#<#{self.class.name}:#{'%#016x' % (object_id << 1)} #{attributes.inspect}>"
404
+ end
405
+
406
+ def initialize(attributes = nil)
407
+ attributes&.each do |key, value|
408
+ send :"#{key}=", value
409
+ end
410
+ yield self if block_given?
411
+ end
412
+
413
+ def self.new(attributes = nil, &)
414
+ default_object.dup.tap { _1.send(:initialize, attributes, &) }
415
+ end
416
+ end
417
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SimpleMaster
4
+ class Schema
5
+ class << self
6
+ TYPE = {
7
+ integer: :integer,
8
+ float: :float,
9
+ string: :string,
10
+ text: :string,
11
+ json: :json,
12
+ boolean: :boolean,
13
+ datetime: :time,
14
+ time: :time,
15
+ }.freeze
16
+
17
+ def generate(table_name)
18
+ ar_columns = ::ActiveRecord::Base.connection.columns(table_name)
19
+ ar_columns.each do |ar_column|
20
+ info = []
21
+
22
+ info << " def_column :#{ar_column.name}"
23
+
24
+ if ar_column.name == "type"
25
+ info << "sti: true"
26
+ elsif ar_column.name.end_with?("type") && ar_columns.any? { _1.name == "#{ar_column.name.delete_suffix('_type')}_id" }
27
+ info << "polymorphic_type: true"
28
+ elsif ar_column.name != "id" && TYPE[ar_column.type]
29
+ info << "type: :#{TYPE[ar_column.type]}"
30
+ end
31
+
32
+ unless ar_column.default.nil?
33
+ db_default_value = db_default_value(ar_column)
34
+
35
+ info << "default: #{db_default_value.inspect}"
36
+ end
37
+
38
+ if ar_column.type == :time
39
+ info << "db_type: :time"
40
+ end
41
+
42
+ puts info.join(", ")
43
+ end
44
+
45
+ nil
46
+ end
47
+ end
48
+ end
49
+ end