all_futures 1.0.3 → 2.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 (106) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +4 -1
  3. data/Gemfile.lock +272 -61
  4. data/LICENSE.txt +1 -1
  5. data/README.md +59 -79
  6. data/Rakefile +13 -1
  7. data/SUMMARY.md +22 -2
  8. data/bin/console +8 -0
  9. data/bin/loc +0 -0
  10. data/bin/setup +0 -0
  11. data/bin/standardize +0 -0
  12. data/lib/all_futures/association.rb +42 -0
  13. data/lib/all_futures/association_index.rb +69 -0
  14. data/lib/all_futures/attributes.rb +28 -0
  15. data/lib/all_futures/base.rb +66 -0
  16. data/lib/all_futures/callbacks.rb +71 -0
  17. data/lib/all_futures/configuration.rb +9 -0
  18. data/lib/all_futures/dirty.rb +116 -0
  19. data/lib/all_futures/embeds.rb +40 -0
  20. data/lib/all_futures/errors.rb +37 -0
  21. data/lib/all_futures/finder.rb +214 -0
  22. data/lib/all_futures/locale/en.yml +49 -0
  23. data/lib/all_futures/persistence.rb +595 -0
  24. data/lib/all_futures/presenter.rb +35 -0
  25. data/lib/all_futures/railtie.rb +26 -0
  26. data/lib/all_futures/translation.rb +22 -0
  27. data/lib/all_futures/validations.rb +10 -0
  28. data/lib/all_futures/version.rb +3 -1
  29. data/lib/all_futures/versions.rb +80 -0
  30. data/lib/all_futures.rb +24 -1
  31. data/lib/vendor/ACTIVE_ENTITY_MIT-LICENSE +42 -0
  32. data/lib/vendor/README.md +8 -0
  33. data/lib/vendor/active_entity/aggregations.rb +277 -0
  34. data/lib/vendor/active_entity/associations/embeds/association.rb +134 -0
  35. data/lib/vendor/active_entity/associations/embeds/builder/association.rb +105 -0
  36. data/lib/vendor/active_entity/associations/embeds/builder/collection_association.rb +71 -0
  37. data/lib/vendor/active_entity/associations/embeds/builder/embedded_in.rb +38 -0
  38. data/lib/vendor/active_entity/associations/embeds/builder/embeds_many.rb +13 -0
  39. data/lib/vendor/active_entity/associations/embeds/builder/embeds_one.rb +16 -0
  40. data/lib/vendor/active_entity/associations/embeds/builder/singular_association.rb +28 -0
  41. data/lib/vendor/active_entity/associations/embeds/collection_association.rb +209 -0
  42. data/lib/vendor/active_entity/associations/embeds/collection_proxy.rb +310 -0
  43. data/lib/vendor/active_entity/associations/embeds/embedded_in_association.rb +31 -0
  44. data/lib/vendor/active_entity/associations/embeds/embeds_many_association.rb +15 -0
  45. data/lib/vendor/active_entity/associations/embeds/embeds_one_association.rb +26 -0
  46. data/lib/vendor/active_entity/associations/embeds/singular_association.rb +35 -0
  47. data/lib/vendor/active_entity/associations.rb +168 -0
  48. data/lib/vendor/active_entity/attribute_assignment.rb +87 -0
  49. data/lib/vendor/active_entity/attribute_decorators.rb +90 -0
  50. data/lib/vendor/active_entity/attribute_methods/before_type_cast.rb +78 -0
  51. data/lib/vendor/active_entity/attribute_methods/dirty.rb +180 -0
  52. data/lib/vendor/active_entity/attribute_methods/primary_key.rb +96 -0
  53. data/lib/vendor/active_entity/attribute_methods/query.rb +38 -0
  54. data/lib/vendor/active_entity/attribute_methods/read.rb +44 -0
  55. data/lib/vendor/active_entity/attribute_methods/serialization.rb +90 -0
  56. data/lib/vendor/active_entity/attribute_methods/time_zone_conversion.rb +93 -0
  57. data/lib/vendor/active_entity/attribute_methods/write.rb +54 -0
  58. data/lib/vendor/active_entity/attribute_methods.rb +833 -0
  59. data/lib/vendor/active_entity/attributes.rb +240 -0
  60. data/lib/vendor/active_entity/base.rb +293 -0
  61. data/lib/vendor/active_entity/callbacks.rb +21 -0
  62. data/lib/vendor/active_entity/coders/json.rb +15 -0
  63. data/lib/vendor/active_entity/coders/yaml_column.rb +50 -0
  64. data/lib/vendor/active_entity/core.rb +348 -0
  65. data/lib/vendor/active_entity/enum.rb +260 -0
  66. data/lib/vendor/active_entity/errors.rb +69 -0
  67. data/lib/vendor/active_entity/gem_version.rb +17 -0
  68. data/lib/vendor/active_entity/inheritance.rb +176 -0
  69. data/lib/vendor/active_entity/integration.rb +78 -0
  70. data/lib/vendor/active_entity/locale/en.yml +45 -0
  71. data/lib/vendor/active_entity/model_schema.rb +118 -0
  72. data/lib/vendor/active_entity/nested_attributes.rb +554 -0
  73. data/lib/vendor/active_entity/persistence.rb +20 -0
  74. data/lib/vendor/active_entity/railtie.rb +76 -0
  75. data/lib/vendor/active_entity/readonly_attributes.rb +55 -0
  76. data/lib/vendor/active_entity/reflection.rb +447 -0
  77. data/lib/vendor/active_entity/serialization.rb +28 -0
  78. data/lib/vendor/active_entity/store.rb +291 -0
  79. data/lib/vendor/active_entity/translation.rb +24 -0
  80. data/lib/vendor/active_entity/type/date.rb +9 -0
  81. data/lib/vendor/active_entity/type/date_time.rb +9 -0
  82. data/lib/vendor/active_entity/type/internal/timezone.rb +17 -0
  83. data/lib/vendor/active_entity/type/json.rb +30 -0
  84. data/lib/vendor/active_entity/type/modifiers/array.rb +72 -0
  85. data/lib/vendor/active_entity/type/modifiers/array_without_blank.rb +19 -0
  86. data/lib/vendor/active_entity/type/registry.rb +112 -0
  87. data/lib/vendor/active_entity/type/serialized.rb +71 -0
  88. data/lib/vendor/active_entity/type/text.rb +11 -0
  89. data/lib/vendor/active_entity/type/time.rb +21 -0
  90. data/lib/vendor/active_entity/type/unsigned_integer.rb +17 -0
  91. data/lib/vendor/active_entity/type.rb +75 -0
  92. data/lib/vendor/active_entity/validate_embeds_association.rb +300 -0
  93. data/lib/vendor/active_entity/validations/absence.rb +25 -0
  94. data/lib/vendor/active_entity/validations/associated.rb +60 -0
  95. data/lib/vendor/active_entity/validations/length.rb +26 -0
  96. data/lib/vendor/active_entity/validations/presence.rb +68 -0
  97. data/lib/vendor/active_entity/validations/subset.rb +76 -0
  98. data/lib/vendor/active_entity/validations/uniqueness_in_embeds.rb +91 -0
  99. data/lib/vendor/active_entity/validations/uniqueness_on_active_record.rb +234 -0
  100. data/lib/vendor/active_entity/validations.rb +47 -0
  101. data/lib/vendor/active_entity/version.rb +10 -0
  102. data/lib/vendor/active_entity.rb +99 -0
  103. data/lib/vendor/core_ext/array_without_blank.rb +48 -0
  104. metadata +175 -21
  105. data/lib/all_futures/all_futures.rb +0 -28
  106. data/lib/all_futures/config.rb +0 -14
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveEntity
4
+ module Associations
5
+ module Embeds
6
+ # = Active Entity Belongs To Association
7
+ class EmbeddedInAssociation < SingularAssociation #:nodoc:
8
+ def default(&block)
9
+ writer(owner.instance_exec(&block)) if reader.nil?
10
+ end
11
+
12
+ private
13
+
14
+ def replace(record)
15
+ if record
16
+ raise_on_type_mismatch!(record)
17
+ set_inverse_instance(record)
18
+ end
19
+
20
+ self.target = record
21
+ end
22
+ # NOTE - for now, we're only supporting inverse setting from belongs_to back onto
23
+ # has_one associations.
24
+ def invertible_for?(record)
25
+ inverse = inverse_reflection_for(record)
26
+ inverse&.embeds_one?
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveEntity
4
+ module Associations
5
+ module Embeds
6
+ # = Active Entity Has Many Association
7
+ # This is the proxy that handles a has many association.
8
+ #
9
+ # If the association has a <tt>:through</tt> option further specialization
10
+ # is provided by its child HasManyThroughAssociation.
11
+ class EmbedsManyAssociation < CollectionAssociation #:nodoc:
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveEntity
4
+ module Associations
5
+ module Embeds
6
+ # = Active Entity Has One Association
7
+ class EmbedsOneAssociation < SingularAssociation #:nodoc:
8
+ private
9
+
10
+ def replace(record)
11
+ self.target =
12
+ if record.is_a? reflection.klass
13
+ record
14
+ elsif record.nil?
15
+ nil
16
+ elsif record.respond_to?(:to_h)
17
+ build_record(record.to_h)
18
+ end
19
+ rescue => ex
20
+ raise_on_type_mismatch!(record)
21
+ raise ex
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveEntity
4
+ module Associations
5
+ module Embeds
6
+ class SingularAssociation < Association #:nodoc:
7
+ # Implements the reader method, e.g. foo.bar for Foo.has_one :bar
8
+ def reader
9
+ target
10
+ end
11
+
12
+ # Implements the writer method, e.g. foo.bar= for Foo.belongs_to :bar
13
+ def writer(record)
14
+ replace(record)
15
+ end
16
+
17
+ def build(attributes = {}, &block)
18
+ record = build_record(attributes, &block)
19
+ set_new_record(record)
20
+ record
21
+ end
22
+
23
+ private
24
+
25
+ def replace(_record)
26
+ raise NotImplementedError, "Subclasses must implement a replace(record) method"
27
+ end
28
+
29
+ def set_new_record(record)
30
+ replace(record)
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,168 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/core_ext/enumerable"
4
+ require "active_support/core_ext/string/conversions"
5
+
6
+ module ActiveEntity
7
+ class AssociationNotFoundError < ConfigurationError #:nodoc:
8
+ def initialize(record = nil, association_name = nil)
9
+ if record && association_name
10
+ super("Association named '#{association_name}' was not found on #{record.class.name}; perhaps you misspelled it?")
11
+ else
12
+ super("Association was not found.")
13
+ end
14
+ end
15
+ end
16
+
17
+ class InverseOfAssociationNotFoundError < ActiveEntityError #:nodoc:
18
+ def initialize(reflection = nil, associated_class = nil)
19
+ if reflection
20
+ super("Could not find the inverse association for #{reflection.name} (#{reflection.options[:inverse_of].inspect} in #{associated_class.nil? ? reflection.class_name : associated_class.name})")
21
+ else
22
+ super("Could not find the inverse association.")
23
+ end
24
+ end
25
+ end
26
+
27
+ # See ActiveEntity::Associations::ClassMethods for documentation.
28
+ module Associations # :nodoc:
29
+ extend ActiveSupport::Autoload
30
+ extend ActiveSupport::Concern
31
+ # These classes will be loaded when associations are created.
32
+ # So there is no need to eager load them.
33
+ module Embeds
34
+ extend ActiveSupport::Autoload
35
+
36
+ autoload :Association, "active_entity/associations/embeds/association"
37
+ autoload :SingularAssociation, "active_entity/associations/embeds/singular_association"
38
+ autoload :CollectionAssociation, "active_entity/associations/embeds/collection_association"
39
+ autoload :CollectionProxy, "active_entity/associations/embeds/collection_proxy"
40
+
41
+ module Builder #:nodoc:
42
+ autoload :Association, "active_entity/associations/embeds/builder/association"
43
+ autoload :SingularAssociation, "active_entity/associations/embeds/builder/singular_association"
44
+ autoload :CollectionAssociation, "active_entity/associations/embeds/builder/collection_association"
45
+
46
+ autoload :EmbeddedIn, "active_entity/associations/embeds/builder/embedded_in"
47
+ autoload :EmbedsOne, "active_entity/associations/embeds/builder/embeds_one"
48
+ autoload :EmbedsMany, "active_entity/associations/embeds/builder/embeds_many"
49
+ end
50
+
51
+ eager_autoload do
52
+ autoload :EmbeddedInAssociation
53
+ autoload :EmbedsOneAssociation
54
+ autoload :EmbedsManyAssociation
55
+ end
56
+ end
57
+
58
+ def self.eager_load!
59
+ super
60
+ Embeds.eager_load!
61
+ end
62
+ # Returns the association instance for the given name, instantiating it if it doesn't already exist
63
+ def association(name) #:nodoc:
64
+ association = association_instance_get(name)
65
+
66
+ if association.nil?
67
+ unless reflection = self.class._reflect_on_association(name)
68
+ raise AssociationNotFoundError.new(self, name)
69
+ end
70
+ association = reflection.association_class.new(self, reflection)
71
+ association_instance_set(name, association)
72
+ end
73
+
74
+ association
75
+ end
76
+
77
+ def association_cached?(name) # :nodoc:
78
+ @association_cache.key?(name)
79
+ end
80
+
81
+ def initialize_dup(*) # :nodoc:
82
+ @association_cache = {}
83
+ super
84
+ end
85
+
86
+ private
87
+
88
+ def init_internals
89
+ @association_cache = {}
90
+ super
91
+ end
92
+
93
+ # Returns the specified association instance if it exists, +nil+ otherwise.
94
+ def association_instance_get(name)
95
+ @association_cache[name]
96
+ end
97
+
98
+ # Set the specified association instance.
99
+ def association_instance_set(name, association)
100
+ @association_cache[name] = association
101
+ end
102
+
103
+ # \Associations are a set of macro-like class methods for tying objects together through
104
+ # foreign keys. They express relationships like "Project has one Project Manager"
105
+ # or "Project belongs to a Portfolio". Each macro adds a number of methods to the
106
+ # class which are specialized according to the collection or association symbol and the
107
+ # options hash. It works much the same way as Ruby's own <tt>attr*</tt>
108
+ # methods.
109
+ #
110
+ # class Project < ActiveEntity::Base
111
+ # belongs_to :portfolio
112
+ # has_one :project_manager
113
+ # has_many :milestones
114
+ # has_and_belongs_to_many :categories
115
+ # end
116
+ #
117
+ # The project class now has the following methods (and more) to ease the traversal and
118
+ # manipulation of its relationships:
119
+ # * <tt>Project#portfolio</tt>, <tt>Project#portfolio=(portfolio)</tt>, <tt>Project#reload_portfolio</tt>
120
+ # * <tt>Project#project_manager</tt>, <tt>Project#project_manager=(project_manager)</tt>, <tt>Project#reload_project_manager</tt>
121
+ # * <tt>Project#milestones.empty?</tt>, <tt>Project#milestones.size</tt>, <tt>Project#milestones</tt>, <tt>Project#milestones<<(milestone)</tt>,
122
+ # <tt>Project#milestones.delete(milestone)</tt>, <tt>Project#milestones.destroy(milestone)</tt>, <tt>Project#milestones.find(milestone_id)</tt>,
123
+ # <tt>Project#milestones.build</tt>, <tt>Project#milestones.create</tt>
124
+ # * <tt>Project#categories.empty?</tt>, <tt>Project#categories.size</tt>, <tt>Project#categories</tt>, <tt>Project#categories<<(category1)</tt>,
125
+ # <tt>Project#categories.delete(category1)</tt>, <tt>Project#categories.destroy(category1)</tt>
126
+ #
127
+ # === A word of warning
128
+ #
129
+ # Don't create associations that have the same name as {instance methods}[rdoc-ref:ActiveEntity::Core] of
130
+ # <tt>ActiveEntity::Base</tt>. Since the association adds a method with that name to
131
+ # its model, using an association with the same name as one provided by <tt>ActiveEntity::Base</tt> will override the method inherited through <tt>ActiveEntity::Base</tt> and will break things.
132
+ # For instance, +attributes+ and +connection+ would be bad choices for association names, because those names already exist in the list of <tt>ActiveEntity::Base</tt> instance methods.
133
+ module ClassMethods
134
+ def embedded_in(name, **options)
135
+ reflection = Embeds::Builder::EmbeddedIn.build(self, name, options)
136
+ Reflection.add_reflection self, name, reflection
137
+ end
138
+
139
+ def embeds_one(name, **options)
140
+ reflection = Embeds::Builder::EmbedsOne.build(self, name, options)
141
+ Reflection.add_reflection self, name, reflection
142
+ end
143
+
144
+ def embeds_many(name, **options)
145
+ reflection = Embeds::Builder::EmbedsMany.build(self, name, options)
146
+ Reflection.add_reflection self, name, reflection
147
+ end
148
+
149
+ def association_names
150
+ @association_names ||=
151
+ if !abstract_class?
152
+ reflections.keys.map(&:to_sym)
153
+ else
154
+ []
155
+ end
156
+ end
157
+
158
+ def embeds_association_names
159
+ @association_names ||=
160
+ if !abstract_class?
161
+ reflections.select { |_, r| r.embedded? }.keys.map(&:to_sym)
162
+ else
163
+ []
164
+ end
165
+ end
166
+ end
167
+ end
168
+ end
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_model/forbidden_attributes_protection"
4
+
5
+ module ActiveEntity
6
+ module AttributeAssignment
7
+ include ActiveModel::AttributeAssignment
8
+
9
+ private
10
+
11
+ def _assign_attributes(attributes)
12
+ multi_parameter_attributes = nested_parameter_attributes = nil
13
+
14
+ attributes.each do |k, v|
15
+ key = k.to_s
16
+
17
+ if key.include?("(")
18
+ (multi_parameter_attributes ||= {})[key] = v
19
+ elsif v.is_a?(Hash)
20
+ (nested_parameter_attributes ||= {})[key] = v
21
+ else
22
+ _assign_attribute(key, v)
23
+ end
24
+ end
25
+
26
+ assign_nested_parameter_attributes(nested_parameter_attributes) if nested_parameter_attributes
27
+ assign_multiparameter_attributes(multi_parameter_attributes) if multi_parameter_attributes
28
+ end
29
+
30
+ # Assign any deferred nested attributes after the base attributes have been set.
31
+ def assign_nested_parameter_attributes(pairs)
32
+ pairs.each { |k, v| _assign_attribute(k, v) }
33
+ end
34
+
35
+ # Instantiates objects for all attribute classes that needs more than one constructor parameter. This is done
36
+ # by calling new on the column type or aggregation type (through composed_of) object with these parameters.
37
+ # So having the pairs written_on(1) = "2004", written_on(2) = "6", written_on(3) = "24", will instantiate
38
+ # written_on (a date type) with Date.new("2004", "6", "24"). You can also specify a typecast character in the
39
+ # parentheses to have the parameters typecasted before they're used in the constructor. Use i for Integer and
40
+ # f for Float. If all the values for a given attribute are empty, the attribute will be set to +nil+.
41
+ def assign_multiparameter_attributes(pairs)
42
+ execute_callstack_for_multiparameter_attributes(
43
+ extract_callstack_for_multiparameter_attributes(pairs)
44
+ )
45
+ end
46
+
47
+ def execute_callstack_for_multiparameter_attributes(callstack)
48
+ errors = []
49
+ callstack.each do |name, values_with_empty_parameters|
50
+ if values_with_empty_parameters.each_value.all?(&:nil?)
51
+ values = nil
52
+ else
53
+ values = values_with_empty_parameters
54
+ end
55
+ send("#{name}=", values)
56
+ rescue => ex
57
+ errors << AttributeAssignmentError.new("error on assignment #{values_with_empty_parameters.values.inspect} to #{name} (#{ex.message})", ex, name)
58
+ end
59
+ unless errors.empty?
60
+ error_descriptions = errors.map(&:message).join(",")
61
+ raise MultiparameterAssignmentErrors.new(errors), "#{errors.size} error(s) on assignment of multiparameter attributes [#{error_descriptions}]"
62
+ end
63
+ end
64
+
65
+ def extract_callstack_for_multiparameter_attributes(pairs)
66
+ attributes = {}
67
+
68
+ pairs.each do |(multiparameter_name, value)|
69
+ attribute_name = multiparameter_name.split("(").first
70
+ attributes[attribute_name] ||= {}
71
+
72
+ parameter_value = value.empty? ? nil : type_cast_attribute_value(multiparameter_name, value)
73
+ attributes[attribute_name][find_parameter_position(multiparameter_name)] ||= parameter_value
74
+ end
75
+
76
+ attributes
77
+ end
78
+
79
+ def type_cast_attribute_value(multiparameter_name, value)
80
+ multiparameter_name =~ /\([0-9]*([if])\)/ ? value.send("to_" + $1) : value
81
+ end
82
+
83
+ def find_parameter_position(multiparameter_name)
84
+ multiparameter_name.scan(/\(([0-9]*).*\)/).first.first.to_i
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveEntity
4
+ module AttributeDecorators # :nodoc:
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+ class_attribute :attribute_type_decorations, instance_accessor: false, default: TypeDecorator.new # :internal:
9
+ end
10
+
11
+ module ClassMethods # :nodoc:
12
+ # This method is an internal API used to create class macros such as
13
+ # +serialize+, and features like time zone aware attributes.
14
+ #
15
+ # Used to wrap the type of an attribute in a new type.
16
+ # When the schema for a model is loaded, attributes with the same name as
17
+ # +column_name+ will have their type yielded to the given block. The
18
+ # return value of that block will be used instead.
19
+ #
20
+ # Subsequent calls where +column_name+ and +decorator_name+ are the same
21
+ # will override the previous decorator, not decorate twice. This can be
22
+ # used to create idempotent class macros like +serialize+
23
+ def decorate_attribute_type(column_name, decorator_name, &block)
24
+ matcher = ->(name, _) { name == column_name.to_s }
25
+ key = "_#{column_name}_#{decorator_name}"
26
+ decorate_matching_attribute_types(matcher, key, &block)
27
+ end
28
+
29
+ # This method is an internal API used to create higher level features like
30
+ # time zone aware attributes.
31
+ #
32
+ # When the schema for a model is loaded, +matcher+ will be called for each
33
+ # attribute with its name and type. If the matcher returns a truthy value,
34
+ # the type will then be yielded to the given block, and the return value
35
+ # of that block will replace the type.
36
+ #
37
+ # Subsequent calls to this method with the same value for +decorator_name+
38
+ # will replace the previous decorator, not decorate twice. This can be
39
+ # used to ensure that class macros are idempotent.
40
+ def decorate_matching_attribute_types(matcher, decorator_name, &block)
41
+ reload_schema_from_cache
42
+ decorator_name = decorator_name.to_s
43
+
44
+ # Create new hashes so we don't modify parent classes
45
+ self.attribute_type_decorations = attribute_type_decorations.merge(decorator_name => [matcher, block])
46
+ end
47
+
48
+ private
49
+
50
+ def load_schema!
51
+ super
52
+ attribute_types.each do |name, type|
53
+ decorated_type = attribute_type_decorations.apply(name, type)
54
+ define_attribute(name, decorated_type)
55
+ end
56
+ end
57
+ end
58
+
59
+ class TypeDecorator # :nodoc:
60
+ delegate :clear, to: :@decorations
61
+
62
+ def initialize(decorations = {})
63
+ @decorations = decorations
64
+ end
65
+
66
+ def merge(*args)
67
+ TypeDecorator.new(@decorations.merge(*args))
68
+ end
69
+
70
+ def apply(name, type)
71
+ decorations = decorators_for(name, type)
72
+ decorations.inject(type) do |new_type, block|
73
+ block.call(new_type)
74
+ end
75
+ end
76
+
77
+ private
78
+
79
+ def decorators_for(name, type)
80
+ matching(name, type).map(&:last)
81
+ end
82
+
83
+ def matching(name, type)
84
+ @decorations.values.select do |(matcher, _)|
85
+ matcher.call(name, type)
86
+ end
87
+ end
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveEntity
4
+ module AttributeMethods
5
+ # = Active Entity Attribute Methods Before Type Cast
6
+ #
7
+ # ActiveEntity::AttributeMethods::BeforeTypeCast provides a way to
8
+ # read the value of the attributes before typecasting and deserialization.
9
+ #
10
+ # class Task < ActiveEntity::Base
11
+ # end
12
+ #
13
+ # task = Task.new(id: '1', completed_on: '2012-10-21')
14
+ # task.id # => 1
15
+ # task.completed_on # => Sun, 21 Oct 2012
16
+ #
17
+ # task.attributes_before_type_cast
18
+ # # => {"id"=>"1", "completed_on"=>"2012-10-21", ... }
19
+ # task.read_attribute_before_type_cast('id') # => "1"
20
+ # task.read_attribute_before_type_cast('completed_on') # => "2012-10-21"
21
+ #
22
+ # In addition to #read_attribute_before_type_cast and #attributes_before_type_cast,
23
+ # it declares a method for all attributes with the <tt>*_before_type_cast</tt>
24
+ # suffix.
25
+ #
26
+ # task.id_before_type_cast # => "1"
27
+ # task.completed_on_before_type_cast # => "2012-10-21"
28
+ module BeforeTypeCast
29
+ extend ActiveSupport::Concern
30
+
31
+ included do
32
+ attribute_method_suffix "_before_type_cast"
33
+ attribute_method_suffix "_came_from_user?"
34
+ end
35
+
36
+ # Returns the value of the attribute identified by +attr_name+ before
37
+ # typecasting and deserialization.
38
+ #
39
+ # class Task < ActiveEntity::Base
40
+ # end
41
+ #
42
+ # task = Task.new(id: '1', completed_on: '2012-10-21')
43
+ # task.read_attribute('id') # => 1
44
+ # task.read_attribute_before_type_cast('id') # => '1'
45
+ # task.read_attribute('completed_on') # => Sun, 21 Oct 2012
46
+ # task.read_attribute_before_type_cast('completed_on') # => "2012-10-21"
47
+ # task.read_attribute_before_type_cast(:completed_on) # => "2012-10-21"
48
+ def read_attribute_before_type_cast(attr_name)
49
+ attribute_before_type_cast(attr_name.to_s)
50
+ end
51
+
52
+ # Returns a hash of attributes before typecasting and deserialization.
53
+ #
54
+ # class Task < ActiveEntity::Base
55
+ # end
56
+ #
57
+ # task = Task.new(title: nil, is_done: true, completed_on: '2012-10-21')
58
+ # task.attributes
59
+ # # => {"id"=>nil, "title"=>nil, "is_done"=>true, "completed_on"=>Sun, 21 Oct 2012, "created_at"=>nil, "updated_at"=>nil}
60
+ # task.attributes_before_type_cast
61
+ # # => {"id"=>nil, "title"=>nil, "is_done"=>true, "completed_on"=>"2012-10-21", "created_at"=>nil, "updated_at"=>nil}
62
+ def attributes_before_type_cast
63
+ @attributes.values_before_type_cast
64
+ end
65
+
66
+ private
67
+
68
+ # Dispatch target for <tt>*_before_type_cast</tt> attribute methods.
69
+ def attribute_before_type_cast(attr_name)
70
+ @attributes[attr_name].value_before_type_cast
71
+ end
72
+
73
+ def attribute_came_from_user?(attr_name)
74
+ @attributes[attr_name].came_from_user?
75
+ end
76
+ end
77
+ end
78
+ end