activeentity 0.0.1.beta1

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 (74) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +42 -0
  3. data/README.md +145 -0
  4. data/Rakefile +29 -0
  5. data/lib/active_entity.rb +73 -0
  6. data/lib/active_entity/aggregations.rb +276 -0
  7. data/lib/active_entity/associations.rb +146 -0
  8. data/lib/active_entity/associations/embedded/association.rb +134 -0
  9. data/lib/active_entity/associations/embedded/builder/association.rb +100 -0
  10. data/lib/active_entity/associations/embedded/builder/collection_association.rb +69 -0
  11. data/lib/active_entity/associations/embedded/builder/embedded_in.rb +38 -0
  12. data/lib/active_entity/associations/embedded/builder/embeds_many.rb +13 -0
  13. data/lib/active_entity/associations/embedded/builder/embeds_one.rb +16 -0
  14. data/lib/active_entity/associations/embedded/builder/singular_association.rb +28 -0
  15. data/lib/active_entity/associations/embedded/collection_association.rb +188 -0
  16. data/lib/active_entity/associations/embedded/collection_proxy.rb +310 -0
  17. data/lib/active_entity/associations/embedded/embedded_in_association.rb +31 -0
  18. data/lib/active_entity/associations/embedded/embeds_many_association.rb +15 -0
  19. data/lib/active_entity/associations/embedded/embeds_one_association.rb +19 -0
  20. data/lib/active_entity/associations/embedded/singular_association.rb +35 -0
  21. data/lib/active_entity/attribute_assignment.rb +85 -0
  22. data/lib/active_entity/attribute_decorators.rb +90 -0
  23. data/lib/active_entity/attribute_methods.rb +330 -0
  24. data/lib/active_entity/attribute_methods/before_type_cast.rb +78 -0
  25. data/lib/active_entity/attribute_methods/primary_key.rb +98 -0
  26. data/lib/active_entity/attribute_methods/query.rb +35 -0
  27. data/lib/active_entity/attribute_methods/read.rb +47 -0
  28. data/lib/active_entity/attribute_methods/serialization.rb +90 -0
  29. data/lib/active_entity/attribute_methods/time_zone_conversion.rb +91 -0
  30. data/lib/active_entity/attribute_methods/write.rb +63 -0
  31. data/lib/active_entity/attributes.rb +165 -0
  32. data/lib/active_entity/base.rb +303 -0
  33. data/lib/active_entity/coders/json.rb +15 -0
  34. data/lib/active_entity/coders/yaml_column.rb +50 -0
  35. data/lib/active_entity/core.rb +281 -0
  36. data/lib/active_entity/define_callbacks.rb +17 -0
  37. data/lib/active_entity/enum.rb +234 -0
  38. data/lib/active_entity/errors.rb +80 -0
  39. data/lib/active_entity/gem_version.rb +17 -0
  40. data/lib/active_entity/inheritance.rb +278 -0
  41. data/lib/active_entity/integration.rb +78 -0
  42. data/lib/active_entity/locale/en.yml +45 -0
  43. data/lib/active_entity/model_schema.rb +115 -0
  44. data/lib/active_entity/nested_attributes.rb +592 -0
  45. data/lib/active_entity/readonly_attributes.rb +47 -0
  46. data/lib/active_entity/reflection.rb +441 -0
  47. data/lib/active_entity/serialization.rb +25 -0
  48. data/lib/active_entity/store.rb +242 -0
  49. data/lib/active_entity/translation.rb +24 -0
  50. data/lib/active_entity/type.rb +73 -0
  51. data/lib/active_entity/type/date.rb +9 -0
  52. data/lib/active_entity/type/date_time.rb +9 -0
  53. data/lib/active_entity/type/decimal_without_scale.rb +15 -0
  54. data/lib/active_entity/type/hash_lookup_type_map.rb +25 -0
  55. data/lib/active_entity/type/internal/timezone.rb +17 -0
  56. data/lib/active_entity/type/json.rb +30 -0
  57. data/lib/active_entity/type/modifiers/array.rb +72 -0
  58. data/lib/active_entity/type/registry.rb +92 -0
  59. data/lib/active_entity/type/serialized.rb +71 -0
  60. data/lib/active_entity/type/text.rb +11 -0
  61. data/lib/active_entity/type/time.rb +21 -0
  62. data/lib/active_entity/type/type_map.rb +62 -0
  63. data/lib/active_entity/type/unsigned_integer.rb +17 -0
  64. data/lib/active_entity/validate_embedded_association.rb +305 -0
  65. data/lib/active_entity/validations.rb +50 -0
  66. data/lib/active_entity/validations/absence.rb +25 -0
  67. data/lib/active_entity/validations/associated.rb +60 -0
  68. data/lib/active_entity/validations/length.rb +26 -0
  69. data/lib/active_entity/validations/presence.rb +68 -0
  70. data/lib/active_entity/validations/subset.rb +76 -0
  71. data/lib/active_entity/validations/uniqueness_in_embedding.rb +99 -0
  72. data/lib/active_entity/version.rb +10 -0
  73. data/lib/tasks/active_entity_tasks.rake +6 -0
  74. metadata +155 -0
@@ -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
+ @attributes[attr_name.to_s].value_before_type_cast
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
+ # Handle *_before_type_cast for method_missing.
69
+ def attribute_before_type_cast(attribute_name)
70
+ read_attribute_before_type_cast(attribute_name)
71
+ end
72
+
73
+ def attribute_came_from_user?(attribute_name)
74
+ @attributes[attribute_name].came_from_user?
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "set"
4
+
5
+ module ActiveEntity
6
+ module AttributeMethods
7
+ module PrimaryKey
8
+ extend ActiveSupport::Concern
9
+
10
+ # Returns this record's primary key value wrapped in an array if one is
11
+ # available.
12
+ def to_key
13
+ key = id
14
+ [key] if key
15
+ end
16
+
17
+ # Returns the primary key column's value.
18
+ def id
19
+ primary_key = self.class.primary_key
20
+ _read_attribute(primary_key) if primary_key
21
+ end
22
+
23
+ # Sets the primary key column's value.
24
+ def id=(value)
25
+ primary_key = self.class.primary_key
26
+ _write_attribute(primary_key, value) if primary_key
27
+ end
28
+
29
+ # Queries the primary key column's value.
30
+ def id?
31
+ query_attribute(self.class.primary_key)
32
+ end
33
+
34
+ # Returns the primary key column's value before type cast.
35
+ def id_before_type_cast
36
+ read_attribute_before_type_cast(self.class.primary_key)
37
+ end
38
+
39
+ # Returns the primary key column's previous value.
40
+ def id_was
41
+ attribute_was(self.class.primary_key)
42
+ end
43
+
44
+ private
45
+
46
+ def attribute_method?(attr_name)
47
+ attr_name == "id" || super
48
+ end
49
+
50
+ module ClassMethods
51
+ ID_ATTRIBUTE_METHODS = %w(id id= id? id_before_type_cast id_was id_in_database).to_set
52
+
53
+ def instance_method_already_implemented?(method_name)
54
+ super || primary_key && ID_ATTRIBUTE_METHODS.include?(method_name)
55
+ end
56
+
57
+ def dangerous_attribute_method?(method_name)
58
+ super && !ID_ATTRIBUTE_METHODS.include?(method_name)
59
+ end
60
+
61
+ # Defines the primary key field -- can be overridden in subclasses.
62
+ # Overwriting will negate any effect of the +primary_key_prefix_type+
63
+ # setting, though.
64
+ def primary_key
65
+ unless defined? @primary_key
66
+ @primary_key =
67
+ if has_attribute?("id")
68
+ "id"
69
+ else
70
+ nil
71
+ end
72
+ end
73
+
74
+ @primary_key
75
+ end
76
+
77
+ # Sets the name of the primary key column.
78
+ #
79
+ # class Project < ActiveEntity::Base
80
+ # self.primary_key = 'sysid'
81
+ # end
82
+ #
83
+ # You can also define the #primary_key method yourself:
84
+ #
85
+ # class Project < ActiveEntity::Base
86
+ # def self.primary_key
87
+ # 'foo_' + super
88
+ # end
89
+ # end
90
+ #
91
+ # Project.primary_key # => "foo_id"
92
+ def primary_key=(value)
93
+ @primary_key = value&.to_s
94
+ end
95
+ end
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveEntity
4
+ module AttributeMethods
5
+ module Query
6
+ extend ActiveSupport::Concern
7
+
8
+ included do
9
+ attribute_method_suffix "?"
10
+ end
11
+
12
+ def query_attribute(attr_name)
13
+ value = self[attr_name]
14
+
15
+ case value
16
+ when true then true
17
+ when false, nil then false
18
+ else
19
+ if Numeric === value || value !~ /[^0-9]/
20
+ !value.to_i.zero?
21
+ else
22
+ return false if ActiveModel::Type::Boolean::FALSE_VALUES.include?(value)
23
+ !value.blank?
24
+ end
25
+ end
26
+ end
27
+
28
+ private
29
+ # Handle *? for method_missing.
30
+ def attribute?(attribute_name)
31
+ query_attribute(attribute_name)
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveEntity
4
+ module AttributeMethods
5
+ module Read
6
+ extend ActiveSupport::Concern
7
+
8
+ module ClassMethods # :nodoc:
9
+ private
10
+
11
+ def define_method_attribute(name)
12
+ ActiveModel::AttributeMethods::AttrNames.define_attribute_accessor_method(
13
+ generated_attribute_methods, name
14
+ ) do |temp_method_name, attr_name_expr|
15
+ generated_attribute_methods.module_eval <<-RUBY, __FILE__, __LINE__ + 1
16
+ def #{temp_method_name}
17
+ name = #{attr_name_expr}
18
+ _read_attribute(name) { |n| missing_attribute(n, caller) }
19
+ end
20
+ RUBY
21
+ end
22
+ end
23
+ end
24
+
25
+ # Returns the value of the attribute identified by <tt>attr_name</tt> after
26
+ # it has been typecast (for example, "2004-12-12" in a date column is cast
27
+ # to a date object, like Date.new(2004, 12, 12)).
28
+ def read_attribute(attr_name, &block)
29
+ name = attr_name.to_s
30
+ if self.class.attribute_alias?(name)
31
+ name = self.class.attribute_alias(name)
32
+ end
33
+
34
+ _read_attribute(name, &block)
35
+ end
36
+
37
+ # This method exists to avoid the expensive primary_key check internally, without
38
+ # breaking compatibility with the read_attribute API
39
+ def _read_attribute(attr_name, &block) # :nodoc
40
+ @attributes.fetch_value(attr_name.to_s, &block)
41
+ end
42
+
43
+ alias :attribute :_read_attribute
44
+ private :attribute
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveEntity
4
+ module AttributeMethods
5
+ module Serialization
6
+ extend ActiveSupport::Concern
7
+
8
+ class ColumnNotSerializableError < StandardError
9
+ def initialize(name, type)
10
+ super <<~EOS
11
+ Column `#{name}` of type #{type.class} does not support `serialize` feature.
12
+ Usually it means that you are trying to use `serialize`
13
+ on a column that already implements serialization natively.
14
+ EOS
15
+ end
16
+ end
17
+
18
+ module ClassMethods
19
+ # If you have an attribute that needs to be saved to the database as an
20
+ # object, and retrieved as the same object, then specify the name of that
21
+ # attribute using this method and it will be handled automatically. The
22
+ # serialization is done through YAML. If +class_name+ is specified, the
23
+ # serialized object must be of that class on assignment and retrieval.
24
+ # Otherwise SerializationTypeMismatch will be raised.
25
+ #
26
+ # Empty objects as <tt>{}</tt>, in the case of +Hash+, or <tt>[]</tt>, in the case of
27
+ # +Array+, will always be persisted as null.
28
+ #
29
+ # Keep in mind that database adapters handle certain serialization tasks
30
+ # for you. For instance: +json+ and +jsonb+ types in PostgreSQL will be
31
+ # converted between JSON object/array syntax and Ruby +Hash+ or +Array+
32
+ # objects transparently. There is no need to use #serialize in this
33
+ # case.
34
+ #
35
+ # For more complex cases, such as conversion to or from your application
36
+ # domain objects, consider using the ActiveEntity::Attributes API.
37
+ #
38
+ # ==== Parameters
39
+ #
40
+ # * +attr_name+ - The field name that should be serialized.
41
+ # * +class_name_or_coder+ - Optional, a coder object, which responds to +.load+ and +.dump+
42
+ # or a class name that the object type should be equal to.
43
+ #
44
+ # ==== Example
45
+ #
46
+ # # Serialize a preferences attribute.
47
+ # class User < ActiveEntity::Base
48
+ # serialize :preferences
49
+ # end
50
+ #
51
+ # # Serialize preferences using JSON as coder.
52
+ # class User < ActiveEntity::Base
53
+ # serialize :preferences, JSON
54
+ # end
55
+ #
56
+ # # Serialize preferences as Hash using YAML coder.
57
+ # class User < ActiveEntity::Base
58
+ # serialize :preferences, Hash
59
+ # end
60
+ def serialize(attr_name, class_name_or_coder = Object)
61
+ # When ::JSON is used, force it to go through the Active Support JSON encoder
62
+ # to ensure special objects (e.g. Active Entity models) are dumped correctly
63
+ # using the #as_json hook.
64
+ coder = if class_name_or_coder == ::JSON
65
+ Coders::JSON
66
+ elsif [:load, :dump].all? { |x| class_name_or_coder.respond_to?(x) }
67
+ class_name_or_coder
68
+ else
69
+ Coders::YAMLColumn.new(attr_name, class_name_or_coder)
70
+ end
71
+
72
+ decorate_attribute_type(attr_name, :serialize) do |type|
73
+ if type_incompatible_with_serialize?(type, class_name_or_coder)
74
+ raise ColumnNotSerializableError.new(attr_name, type)
75
+ end
76
+
77
+ Type::Serialized.new(type, coder)
78
+ end
79
+ end
80
+
81
+ private
82
+
83
+ def type_incompatible_with_serialize?(type, class_name)
84
+ type.is_a?(ActiveEntity::Type::Json) && class_name == ::JSON ||
85
+ type.respond_to?(:type_cast_array, true) && class_name == ::Array
86
+ end
87
+ end
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveEntity
4
+ module AttributeMethods
5
+ module TimeZoneConversion
6
+ class TimeZoneConverter < DelegateClass(Type::Value) # :nodoc:
7
+ def deserialize(value)
8
+ convert_time_to_time_zone(super)
9
+ end
10
+
11
+ def cast(value)
12
+ return if value.nil?
13
+
14
+ if value.is_a?(Hash)
15
+ set_time_zone_without_conversion(super)
16
+ elsif value.respond_to?(:in_time_zone)
17
+ begin
18
+ super(user_input_in_time_zone(value)) || super
19
+ rescue ArgumentError
20
+ nil
21
+ end
22
+ else
23
+ map_avoiding_infinite_recursion(super) { |v| cast(v) }
24
+ end
25
+ end
26
+
27
+ private
28
+
29
+ def convert_time_to_time_zone(value)
30
+ return if value.nil?
31
+
32
+ if value.acts_like?(:time)
33
+ value.in_time_zone
34
+ elsif value.is_a?(::Float)
35
+ value
36
+ else
37
+ map_avoiding_infinite_recursion(value) { |v| convert_time_to_time_zone(v) }
38
+ end
39
+ end
40
+
41
+ def set_time_zone_without_conversion(value)
42
+ ::Time.zone.local_to_utc(value).try(:in_time_zone) if value
43
+ end
44
+
45
+ def map_avoiding_infinite_recursion(value)
46
+ map(value) do |v|
47
+ if value.equal?(v)
48
+ nil
49
+ else
50
+ yield(v)
51
+ end
52
+ end
53
+ end
54
+ end
55
+
56
+ extend ActiveSupport::Concern
57
+
58
+ included do
59
+ mattr_accessor :time_zone_aware_attributes, instance_writer: false, default: false
60
+
61
+ class_attribute :skip_time_zone_conversion_for_attributes, instance_writer: false, default: []
62
+ class_attribute :time_zone_aware_types, instance_writer: false, default: [ :datetime, :time ]
63
+ end
64
+
65
+ module ClassMethods # :nodoc:
66
+ private
67
+
68
+ def inherited(subclass)
69
+ super
70
+ # We need to apply this decorator here, rather than on module inclusion. The closure
71
+ # created by the matcher would otherwise evaluate for `ActiveEntity::Base`, not the
72
+ # sub class being decorated. As such, changes to `time_zone_aware_attributes`, or
73
+ # `skip_time_zone_conversion_for_attributes` would not be picked up.
74
+ subclass.class_eval do
75
+ matcher = ->(name, type) { create_time_zone_conversion_attribute?(name, type) }
76
+ decorate_matching_attribute_types(matcher, "_time_zone_conversion") do |type|
77
+ TimeZoneConverter.new(type)
78
+ end
79
+ end
80
+ end
81
+
82
+ def create_time_zone_conversion_attribute?(name, cast_type)
83
+ enabled_for_column = time_zone_aware_attributes &&
84
+ !skip_time_zone_conversion_for_attributes.include?(name.to_sym)
85
+
86
+ enabled_for_column && time_zone_aware_types.include?(cast_type.type)
87
+ end
88
+ end
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveEntity
4
+ module AttributeMethods
5
+ module Write
6
+ extend ActiveSupport::Concern
7
+
8
+ included do
9
+ attribute_method_suffix "="
10
+ end
11
+
12
+ module ClassMethods # :nodoc:
13
+ private
14
+
15
+ def define_method_attribute=(name)
16
+ ActiveModel::AttributeMethods::AttrNames.define_attribute_accessor_method(
17
+ generated_attribute_methods, name, writer: true,
18
+ ) do |temp_method_name, attr_name_expr|
19
+ generated_attribute_methods.module_eval <<-RUBY, __FILE__, __LINE__ + 1
20
+ def #{temp_method_name}(value)
21
+ name = #{attr_name_expr}
22
+ _write_attribute(name, value)
23
+ end
24
+ RUBY
25
+ end
26
+ end
27
+ end
28
+
29
+ # Updates the attribute identified by <tt>attr_name</tt> with the
30
+ # specified +value+. Empty strings for Integer and Float columns are
31
+ # turned into +nil+.
32
+ def write_attribute(attr_name, value)
33
+ name = attr_name.to_s
34
+ if self.class.attribute_alias?(name)
35
+ name = self.class.attribute_alias(name)
36
+ end
37
+
38
+ _write_attribute(name, value)
39
+ end
40
+
41
+ # This method exists to avoid the expensive primary_key check internally, without
42
+ # breaking compatibility with the write_attribute API
43
+ def _write_attribute(attr_name, value) # :nodoc:
44
+ return if readonly_attribute?(attr_name) && readonly_enabled?
45
+
46
+ @attributes.write_from_user(attr_name.to_s, value)
47
+ value
48
+ end
49
+
50
+ private
51
+ def write_attribute_without_type_cast(attr_name, value)
52
+ name = attr_name.to_s
53
+ @attributes.write_cast_value(name, value)
54
+ value
55
+ end
56
+
57
+ # Handle *= for method_missing.
58
+ def attribute=(attribute_name, value)
59
+ _write_attribute(attribute_name, value)
60
+ end
61
+ end
62
+ end
63
+ end