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,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SimpleMaster
4
+ class Master
5
+ class Association
6
+ class HasManyThroughAssociation < self
7
+ def foreign_key
8
+ @foreign_key ||= (options[:foreign_key] || ActiveSupport::Inflector.foreign_key(defined_at.to_s)).to_sym
9
+ end
10
+
11
+ def through
12
+ @through ||= options[:through]
13
+ end
14
+
15
+ def through_association
16
+ @through_association ||=
17
+ begin
18
+ through_association = defined_at.all_has_many_associations.find { |ass| ass.name == through }
19
+
20
+ if through_association.nil?
21
+ fail "Association '#{through}' not found in has_many_associations. Use 'delegate' instead."
22
+ end
23
+
24
+ through_association
25
+ end
26
+ end
27
+
28
+ delegate :target_class, to: :through_association
29
+
30
+ def source
31
+ @source ||= options[:source] || ActiveSupport::Inflector.singularize(name)
32
+ end
33
+
34
+ def primary_key
35
+ @primary_key ||= options[:primary_key] || :id
36
+ end
37
+
38
+ def init(master_class)
39
+ # check
40
+ through_association
41
+
42
+ master_class.simple_master_module.class_eval <<-RUBY, __FILE__, __LINE__ + 1
43
+ def #{name}
44
+ Array.wrap(#{through}).flat_map { |ass| ass.#{source} }.compact
45
+ end
46
+ RUBY
47
+ end
48
+
49
+ def init_for_test(master_class)
50
+ master_class.simple_master_module.class_eval <<-RUBY, __FILE__, __LINE__ + 1
51
+ def #{name}=(values)
52
+ self.#{through} = values.map { |value|
53
+ #{through_association.target_class}.new.tap do |through|
54
+ through.#{source} = value
55
+ #{"through.#{through_association.inverse_of} = self" if through_association.try(:inverse_of)}
56
+ end
57
+ }
58
+ end
59
+ RUBY
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SimpleMaster
4
+ class Master
5
+ class Association
6
+ class HasOneAssociation < self
7
+ def foreign_key
8
+ @foreign_key ||= (options[:foreign_key] || ActiveSupport::Inflector.foreign_key(defined_at.to_s)).to_sym
9
+ end
10
+
11
+ def inverse_of
12
+ @inverse_of ||= options[:inverse_of] || ActiveSupport::Inflector.underscore(defined_at.to_s)
13
+ end
14
+
15
+ def primary_key
16
+ @primary_key ||= options[:primary_key] || defined_at.primary_key || :id
17
+ end
18
+
19
+ def init(master_class)
20
+ master_class.simple_master_module.class_eval <<-RUBY, __FILE__, __LINE__ + 1
21
+ if #{target_class} < SimpleMaster::Master
22
+ def #{name}
23
+ #{target_class}.find_by(:#{foreign_key}, #{primary_key})
24
+ end
25
+ else
26
+ def #{name}
27
+ # Store in an Array so nil values still hit the cache
28
+ values = (has_many_store[:#{name}] ||= [#{target_class}.simple_master_connection.find_by_#{foreign_key}(#{primary_key})])
29
+ values.first
30
+ end
31
+ end
32
+ RUBY
33
+ end
34
+
35
+ def init_for_test(master_class)
36
+ master_class.simple_master_module.class_eval <<-RUBY, __FILE__, __LINE__ + 1
37
+ if #{target_class} < SimpleMaster::Master
38
+ def #{name}
39
+ (has_many_store[:#{name}] || #{target_class}.all_by(:#{foreign_key}, #{primary_key})).first
40
+ end
41
+ else
42
+ def #{name}
43
+ # Store in an Array so nil values still hit the cache
44
+ values = (has_many_store[:#{name}] ||= [#{target_class}.simple_master_connection.find_by_#{foreign_key}(#{primary_key})])
45
+ values.first
46
+ end
47
+ end
48
+
49
+ def #{name}=(value)
50
+ has_many_store[:#{name}] = [value]
51
+ end
52
+ RUBY
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SimpleMaster
4
+ class Master
5
+ class Association
6
+ autoload :BelongsToAssociation, "simple_master/master/association/belongs_to_association"
7
+ autoload :BelongsToPolymorphicAssociation, "simple_master/master/association/belongs_to_polymorphic_association"
8
+ autoload :HasManyAssociation, "simple_master/master/association/has_many_association"
9
+ autoload :HasManyThroughAssociation, "simple_master/master/association/has_many_through_association"
10
+ autoload :HasOneAssociation, "simple_master/master/association/has_one_association"
11
+
12
+ attr_reader :name
13
+ attr_reader :defined_at
14
+ attr_reader :options
15
+
16
+ def initialize(master_class, name, options)
17
+ @name = name
18
+ @defined_at = master_class
19
+ @options = options
20
+ end
21
+
22
+ # Execute after definition phase.
23
+ def target_class
24
+ @target_class ||= find_class(defined_at, options[:class_name] || ActiveSupport::Inflector.classify(name))
25
+ end
26
+
27
+ def is_active_record?
28
+ target_class < ::ActiveRecord::Base
29
+ end
30
+
31
+ def init(master_class)
32
+ end
33
+
34
+ def init_for_test(master_class)
35
+ end
36
+
37
+ private
38
+
39
+ def find_class(current, class_name)
40
+ loop do
41
+ return current.const_get(class_name, false)
42
+ rescue NameError => e
43
+ raise e if current == current.module_parent
44
+
45
+ current = current.module_parent
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SimpleMaster
4
+ class Master
5
+ class Column
6
+ class BitmaskColumn < self
7
+ attr_reader :name
8
+ attr_reader :bitmask
9
+ attr_reader :const_name
10
+
11
+ def initialize(name, options)
12
+ @bitmask = options[:bitmask]
13
+ @const_name = "BITMASK_FOR_#{name.upcase}"
14
+
15
+ super
16
+ end
17
+
18
+ def init(master_class, for_test = false)
19
+ super
20
+
21
+ master_class.simple_master_module.const_set(const_name, bitmask) unless master_class.const_defined?(const_name)
22
+
23
+ master_class.simple_master_module.class_eval <<-RUBY, __FILE__, __LINE__ + 1
24
+ def #{name}
25
+ return EMPTY_ARRAY if @#{name}.nil?
26
+ #{const_name}.filter_map.with_index {
27
+ _1 if (1 << _2) & @#{name} != 0
28
+ }
29
+ end
30
+
31
+ def #{name}_value
32
+ @#{name}
33
+ end
34
+
35
+ def #{name}_value=(value)
36
+ #{code_for_dirty_check if for_test}
37
+ @#{name} = value&.to_i
38
+ end
39
+ RUBY
40
+ end
41
+
42
+ private
43
+
44
+ def code_for_conversion
45
+ <<-RUBY
46
+ value = cache_object(:#{name}, value) { |value|
47
+ value = [value] if value.is_a?(Symbol)
48
+
49
+ if value.is_a?(Array)
50
+ bits = 0
51
+ value.each do |day|
52
+ bits |= 1 << #{const_name}.index(day)
53
+ end
54
+ bits
55
+ else
56
+ value&.to_i
57
+ end
58
+ }
59
+ RUBY
60
+ end
61
+
62
+ def code_for_sql_value
63
+ <<-RUBY
64
+ @#{name}
65
+ RUBY
66
+ end
67
+
68
+ def globalize
69
+ fail NotImplementedError
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SimpleMaster
4
+ class Master
5
+ class Column
6
+ class BooleanColumn < self
7
+ def init(master_class, for_test = false)
8
+ super
9
+
10
+ master_class.simple_master_module.class_eval <<-RUBY, __FILE__, __LINE__ + 1
11
+ def #{name}?
12
+ !!#{name}
13
+ end
14
+ RUBY
15
+ end
16
+
17
+ private
18
+
19
+ def code_for_conversion
20
+ <<-RUBY
21
+ value = cache_object(:#{name}, value) { |value|
22
+ if value.is_a?(Integer)
23
+ value != 0
24
+ elsif value.is_a?(String)
25
+ value.downcase == "true" || value == "1"
26
+ elsif value.nil?
27
+ value
28
+ else
29
+ !!value
30
+ end
31
+ }
32
+ RUBY
33
+ end
34
+
35
+ def code_for_sql_value
36
+ <<-RUBY
37
+ # Convert to 0/1 for systems that do not support true/false
38
+ case #{name}
39
+ when true
40
+ 1
41
+ when false
42
+ 0
43
+ else
44
+ nil
45
+ end
46
+ RUBY
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SimpleMaster
4
+ class Master
5
+ class Column
6
+ class EnumColumn < self
7
+ attr_reader :name
8
+ attr_reader :enum
9
+ attr_reader :const_name
10
+ attr_reader :prefix
11
+ attr_reader :suffix
12
+
13
+ def initialize(name, options)
14
+ @enum = options[:enum]
15
+ @const_name = "ENUM_FOR_#{name.upcase}"
16
+ @prefix = "#{options[:prefix] == true ? name : options[:prefix]}_" if options[:prefix]
17
+ @suffix = "_#{options[:suffix] == true ? name : options[:suffix]}" if options[:suffix]
18
+
19
+ super
20
+ end
21
+
22
+ def init(master_class, for_test = false)
23
+ super
24
+
25
+ master_class.simple_master_module.const_set(const_name, enum.freeze) unless master_class.const_defined?(const_name)
26
+
27
+ master_class.class_eval <<-RUBY, __FILE__, __LINE__ + 1
28
+ def self.#{name.to_s.pluralize}
29
+ #{const_name}
30
+ end
31
+ RUBY
32
+
33
+ master_class.simple_master_module.class_eval <<-RUBY, __FILE__, __LINE__ + 1
34
+ def #{name}_to_enum(value)
35
+ case value
36
+ when Integer
37
+ return #{const_name}.key(value)
38
+ when String
39
+ sym = value.to_sym
40
+ return sym if #{const_name}.key?(sym)
41
+
42
+ int_val = Integer(value, exception: false)
43
+ if int_val
44
+ result = #{const_name}.key(int_val)
45
+ return result if result
46
+ end
47
+
48
+ fail "Unsupported type \#{value.inspect}."
49
+ when Symbol, NilClass
50
+ return value
51
+ else
52
+ fail "Unsupported type \#{value.inspect}."
53
+ end
54
+ end
55
+
56
+ def #{name.to_s.pluralize}
57
+ #{const_name}
58
+ end
59
+
60
+ def #{name}_before_type_cast
61
+ #{const_name}[#{name}]
62
+ end
63
+ RUBY
64
+
65
+ enum.each_key do |enum_name|
66
+ # Skip generating helpers for names that start with a digit
67
+ next if enum_name.match?(/\A\d/) && prefix.nil?
68
+ master_class.simple_master_module.class_eval <<-RUBY, __FILE__, __LINE__ + 1
69
+ def #{prefix}#{enum_name}#{suffix}?
70
+ #{name} == :#{enum_name}
71
+ end
72
+ RUBY
73
+ end
74
+ end
75
+
76
+ private
77
+
78
+ def code_for_conversion
79
+ <<-RUBY
80
+ value = cache_object(:#{name}, value) { #{name}_to_enum(_1) }
81
+ RUBY
82
+ end
83
+
84
+ def code_for_sql_value
85
+ <<-RUBY
86
+ #{name}_before_type_cast
87
+ RUBY
88
+ end
89
+
90
+ def globalize
91
+ fail NotImplementedError
92
+ end
93
+ end
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SimpleMaster
4
+ class Master
5
+ class Column
6
+ class FloatColumn < self
7
+ private
8
+
9
+ def code_for_conversion
10
+ <<-RUBY
11
+ if value.is_a?(String) && value.strip.empty?
12
+ value = nil
13
+ else
14
+ value = value&.to_f
15
+ end
16
+ RUBY
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SimpleMaster
4
+ class Master
5
+ class Column
6
+ class IdColumn < self
7
+ private
8
+
9
+ def code_for_conversion
10
+ <<-RUBY
11
+ value = value&.to_i
12
+ RUBY
13
+ end
14
+
15
+ def code_for_dirty_check
16
+ <<-RUBY
17
+ unless @#{name} == value
18
+ dirty!
19
+ # IDs require updating @id_hash when changed
20
+ self.class.id_hash.delete(@#{name}) if @#{name}
21
+ end
22
+ RUBY
23
+ end
24
+
25
+ def globalize
26
+ fail NotImplementedError
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SimpleMaster
4
+ class Master
5
+ class Column
6
+ class IntegerColumn < self
7
+ private
8
+
9
+ def code_for_conversion
10
+ <<-RUBY
11
+ if value.is_a?(String) && value.strip.empty?
12
+ value = nil
13
+ else
14
+ value = value&.to_i
15
+ end
16
+ RUBY
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SimpleMaster
4
+ class Master
5
+ class Column
6
+ class JsonColumn < self
7
+ private
8
+
9
+ def code_for_conversion
10
+ <<-RUBY
11
+ value = cache_object(:#{name}, value) { |value|
12
+ value = JSON.parse(value, symbolize_names: #{!!options[:symbolize_names]}) if value.is_a?(String)
13
+ value
14
+ }
15
+ RUBY
16
+ end
17
+
18
+ def code_for_sql_value
19
+ <<-RUBY
20
+ # Use JSON.generate because to_json adds unnecessary escaping
21
+ #{name}&.then { JSON.generate(_1) }
22
+ RUBY
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SimpleMaster
4
+ class Master
5
+ class Column
6
+ class PolymorphicTypeColumn < self
7
+ def init(master_class, for_test = false)
8
+ super
9
+
10
+ master_class.simple_master_module.attr_reader :"#{name}_class"
11
+ master_class.simple_master_module.attr_reader name
12
+
13
+ master_class.simple_master_module.class_eval <<-RUBY, __FILE__, __LINE__ + 1
14
+ def #{name}=(value)
15
+ #{code_for_conversion}
16
+ #{code_for_dirty_check if for_test}
17
+ @#{name} = value
18
+ @#{name}_class = value&.then { ActiveSupport::Inflector.constantize(_1) }
19
+ end
20
+
21
+ def #{name}_class=(klass)
22
+ #{"dirty! unless @#{name}_class == klass" if for_test}
23
+ @#{name} = klass.to_s
24
+ @#{name}_class = klass
25
+ end
26
+ RUBY
27
+ end
28
+
29
+ private
30
+
31
+ def code_for_conversion
32
+ <<-RUBY
33
+ value = nil if value == ""
34
+ value = cache_object(:#{name}, value) { _1&.to_s }
35
+ RUBY
36
+ end
37
+
38
+ def globalize
39
+ fail NotImplementedError
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SimpleMaster
4
+ class Master
5
+ class Column
6
+ class StiTypeColumn < self
7
+ private
8
+
9
+ def code_for_conversion
10
+ <<-RUBY
11
+ value = cache_object(:#{name}, value) { _1&.to_s }
12
+ RUBY
13
+ end
14
+
15
+ def globalize
16
+ fail NotImplementedError
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SimpleMaster
4
+ class Master
5
+ class Column
6
+ class StringColumn < self
7
+ private
8
+
9
+ def code_for_conversion
10
+ <<-RUBY
11
+ value = cache_object(:#{name}, value) { _1&.to_s }
12
+ RUBY
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SimpleMaster
4
+ class Master
5
+ class Column
6
+ class SymbolColumn < self
7
+ private
8
+
9
+ def code_for_conversion
10
+ <<-RUBY
11
+ value = value&.to_s&.to_sym
12
+ RUBY
13
+ end
14
+
15
+ def code_for_sql_value
16
+ <<-RUBY
17
+ #{name}&.to_s
18
+ RUBY
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SimpleMaster
4
+ class Master
5
+ class Column
6
+ class TimeColumn < self
7
+ private
8
+
9
+ def code_for_conversion
10
+ <<-RUBY
11
+ value = cache_object(:#{name}, value) { |value|
12
+ if value.is_a?(String)
13
+ #{"value = value.sub(/\A\d{4}-\d\d-\d\d(?:T|\s)|/, \"2000-01-01 \")" if options[:db_type] == :time}
14
+ time_hash = ::Date._parse(value)
15
+ zone = time_hash[:offset] || "UTC"
16
+ value = Time.new(*time_hash.values_at(:year, :mon, :mday, :hour, :min, :sec), zone)
17
+ end
18
+ value = value.floor if value && value.subsec != 0
19
+ value
20
+ }
21
+ RUBY
22
+ end
23
+
24
+ def code_for_sql_value
25
+ if options[:db_type] == :time
26
+ <<-RUBY
27
+ #{name}&.getutc&.strftime('%H:%M:%S')
28
+ RUBY
29
+ else
30
+ <<-RUBY
31
+ #{name}&.getutc&.strftime('%Y-%m-%d %H:%M:%S')
32
+ RUBY
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end