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,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveEntity #:nodoc:
4
+ # = Active Entity \Serialization
5
+ module Serialization
6
+ extend ActiveSupport::Concern
7
+ include ActiveModel::Serializers::JSON
8
+
9
+ included do
10
+ self.include_root_in_json = false
11
+ end
12
+
13
+ def serializable_hash(include_embedded: true, **options)
14
+ if include_embedded
15
+ include = Array.wrap(options[:include]).concat(self.class.embedded_association_names)
16
+ options[:include] = include
17
+ end
18
+
19
+ # options[:except] = Array(options[:except]).map(&:to_s)
20
+ # options[:except] |= Array(self.class.inheritance_attribute)
21
+
22
+ super(options)
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,242 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/core_ext/hash/indifferent_access"
4
+
5
+ module ActiveEntity
6
+ # Store gives you a thin wrapper around serialize for the purpose of storing hashes in a single column.
7
+ # It's like a simple key/value store baked into your record when you don't care about being able to
8
+ # query that store outside the context of a single record.
9
+ #
10
+ # You can then declare accessors to this store that are then accessible just like any other attribute
11
+ # of the model. This is very helpful for easily exposing store keys to a form or elsewhere that's
12
+ # already built around just accessing attributes on the model.
13
+ #
14
+ # Make sure that you declare the database column used for the serialized store as a text, so there's
15
+ # plenty of room.
16
+ #
17
+ # You can set custom coder to encode/decode your serialized attributes to/from different formats.
18
+ # JSON, YAML, Marshal are supported out of the box. Generally it can be any wrapper that provides +load+ and +dump+.
19
+ #
20
+ # NOTE: If you are using structured database data types (eg. PostgreSQL +hstore+/+json+, or MySQL 5.7+
21
+ # +json+) there is no need for the serialization provided by {.store}[rdoc-ref:rdoc-ref:ClassMethods#store].
22
+ # Simply use {.store_accessor}[rdoc-ref:ClassMethods#store_accessor] instead to generate
23
+ # the accessor methods. Be aware that these columns use a string keyed hash and do not allow access
24
+ # using a symbol.
25
+ #
26
+ # NOTE: The default validations with the exception of +uniqueness+ will work.
27
+ # For example, if you want to check for +uniqueness+ with +hstore+ you will
28
+ # need to use a custom validation to handle it.
29
+ #
30
+ # Examples:
31
+ #
32
+ # class User < ActiveEntity::Base
33
+ # store :settings, accessors: [ :color, :homepage ], coder: JSON
34
+ # store :parent, accessors: [ :name ], coder: JSON, prefix: true
35
+ # store :spouse, accessors: [ :name ], coder: JSON, prefix: :partner
36
+ # store :settings, accessors: [ :two_factor_auth ], suffix: true
37
+ # store :settings, accessors: [ :login_retry ], suffix: :config
38
+ # end
39
+ #
40
+ # u = User.new(color: 'black', homepage: '37signals.com', parent_name: 'Mary', partner_name: 'Lily')
41
+ # u.color # Accessor stored attribute
42
+ # u.parent_name # Accessor stored attribute with prefix
43
+ # u.partner_name # Accessor stored attribute with custom prefix
44
+ # u.two_factor_auth_settings # Accessor stored attribute with suffix
45
+ # u.login_retry_config # Accessor stored attribute with custom suffix
46
+ # u.settings[:country] = 'Denmark' # Any attribute, even if not specified with an accessor
47
+ #
48
+ # # There is no difference between strings and symbols for accessing custom attributes
49
+ # u.settings[:country] # => 'Denmark'
50
+ # u.settings['country'] # => 'Denmark'
51
+ #
52
+ # # Add additional accessors to an existing store through store_accessor
53
+ # class SuperUser < User
54
+ # store_accessor :settings, :privileges, :servants
55
+ # store_accessor :parent, :birthday, prefix: true
56
+ # store_accessor :settings, :secret_question, suffix: :config
57
+ # end
58
+ #
59
+ # The stored attribute names can be retrieved using {.stored_attributes}[rdoc-ref:rdoc-ref:ClassMethods#stored_attributes].
60
+ #
61
+ # User.stored_attributes[:settings] # [:color, :homepage, :two_factor_auth, :login_retry]
62
+ #
63
+ # == Overwriting default accessors
64
+ #
65
+ # All stored values are automatically available through accessors on the Active Entity
66
+ # object, but sometimes you want to specialize this behavior. This can be done by overwriting
67
+ # the default accessors (using the same name as the attribute) and calling <tt>super</tt>
68
+ # to actually change things.
69
+ #
70
+ # class Song < ActiveEntity::Base
71
+ # # Uses a stored integer to hold the volume adjustment of the song
72
+ # store :settings, accessors: [:volume_adjustment]
73
+ #
74
+ # def volume_adjustment=(decibels)
75
+ # super(decibels.to_i)
76
+ # end
77
+ #
78
+ # def volume_adjustment
79
+ # super.to_i
80
+ # end
81
+ # end
82
+ module Store
83
+ extend ActiveSupport::Concern
84
+
85
+ included do
86
+ class << self
87
+ attr_accessor :local_stored_attributes
88
+ end
89
+ end
90
+
91
+ module ClassMethods
92
+ def store(store_attribute, options = {})
93
+ serialize store_attribute, IndifferentCoder.new(store_attribute, options[:coder])
94
+ store_accessor(store_attribute, options[:accessors], options.slice(:prefix, :suffix)) if options.has_key? :accessors
95
+ end
96
+
97
+ def store_accessor(store_attribute, *keys, prefix: nil, suffix: nil)
98
+ keys = keys.flatten
99
+
100
+ accessor_prefix =
101
+ case prefix
102
+ when String, Symbol
103
+ "#{prefix}_"
104
+ when TrueClass
105
+ "#{store_attribute}_"
106
+ else
107
+ ""
108
+ end
109
+ accessor_suffix =
110
+ case suffix
111
+ when String, Symbol
112
+ "_#{suffix}"
113
+ when TrueClass
114
+ "_#{store_attribute}"
115
+ else
116
+ ""
117
+ end
118
+
119
+ _store_accessors_module.module_eval do
120
+ keys.each do |key|
121
+ accessor_key = "#{accessor_prefix}#{key}#{accessor_suffix}"
122
+
123
+ define_method("#{accessor_key}=") do |value|
124
+ write_store_attribute(store_attribute, key, value)
125
+ end
126
+
127
+ define_method(accessor_key) do
128
+ read_store_attribute(store_attribute, key)
129
+ end
130
+ end
131
+ end
132
+
133
+ # assign new store attribute and create new hash to ensure that each class in the hierarchy
134
+ # has its own hash of stored attributes.
135
+ self.local_stored_attributes ||= {}
136
+ self.local_stored_attributes[store_attribute] ||= []
137
+ self.local_stored_attributes[store_attribute] |= keys
138
+ end
139
+
140
+ def _store_accessors_module # :nodoc:
141
+ @_store_accessors_module ||= begin
142
+ mod = Module.new
143
+ include mod
144
+ mod
145
+ end
146
+ end
147
+
148
+ def stored_attributes
149
+ parent = superclass.respond_to?(:stored_attributes) ? superclass.stored_attributes : {}
150
+ if local_stored_attributes
151
+ parent.merge!(local_stored_attributes) { |k, a, b| a | b }
152
+ end
153
+ parent
154
+ end
155
+ end
156
+
157
+ private
158
+ def read_store_attribute(store_attribute, key) # :doc:
159
+ accessor = store_accessor_for(store_attribute)
160
+ accessor.read(self, store_attribute, key)
161
+ end
162
+
163
+ def write_store_attribute(store_attribute, key, value) # :doc:
164
+ accessor = store_accessor_for(store_attribute)
165
+ accessor.write(self, store_attribute, key, value)
166
+ end
167
+
168
+ def store_accessor_for(store_attribute)
169
+ type_for_attribute(store_attribute).accessor
170
+ end
171
+
172
+ class HashAccessor # :nodoc:
173
+ def self.read(object, attribute, key)
174
+ prepare(object, attribute)
175
+ object.public_send(attribute)[key]
176
+ end
177
+
178
+ def self.write(object, attribute, key, value)
179
+ prepare(object, attribute)
180
+ if value != read(object, attribute, key)
181
+ object.public_send :"#{attribute}_will_change!"
182
+ object.public_send(attribute)[key] = value
183
+ end
184
+ end
185
+
186
+ def self.prepare(object, attribute)
187
+ object.public_send :"#{attribute}=", {} unless object.send(attribute)
188
+ end
189
+ end
190
+
191
+ class StringKeyedHashAccessor < HashAccessor # :nodoc:
192
+ def self.read(object, attribute, key)
193
+ super object, attribute, key.to_s
194
+ end
195
+
196
+ def self.write(object, attribute, key, value)
197
+ super object, attribute, key.to_s, value
198
+ end
199
+ end
200
+
201
+ class IndifferentHashAccessor < ActiveEntity::Store::HashAccessor # :nodoc:
202
+ def self.prepare(object, store_attribute)
203
+ attribute = object.send(store_attribute)
204
+ unless attribute.is_a?(ActiveSupport::HashWithIndifferentAccess)
205
+ attribute = IndifferentCoder.as_indifferent_hash(attribute)
206
+ object.send :"#{store_attribute}=", attribute
207
+ end
208
+ attribute
209
+ end
210
+ end
211
+
212
+ class IndifferentCoder # :nodoc:
213
+ def initialize(attr_name, coder_or_class_name)
214
+ @coder =
215
+ if coder_or_class_name.respond_to?(:load) && coder_or_class_name.respond_to?(:dump)
216
+ coder_or_class_name
217
+ else
218
+ ActiveEntity::Coders::YAMLColumn.new(attr_name, coder_or_class_name || Object)
219
+ end
220
+ end
221
+
222
+ def dump(obj)
223
+ @coder.dump self.class.as_indifferent_hash(obj)
224
+ end
225
+
226
+ def load(yaml)
227
+ self.class.as_indifferent_hash(@coder.load(yaml || ""))
228
+ end
229
+
230
+ def self.as_indifferent_hash(obj)
231
+ case obj
232
+ when ActiveSupport::HashWithIndifferentAccess
233
+ obj
234
+ when Hash
235
+ obj.with_indifferent_access
236
+ else
237
+ ActiveSupport::HashWithIndifferentAccess.new
238
+ end
239
+ end
240
+ end
241
+ end
242
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveEntity
4
+ module Translation
5
+ include ActiveModel::Translation
6
+
7
+ # Set the lookup ancestors for ActiveModel.
8
+ def lookup_ancestors #:nodoc:
9
+ klass = self
10
+ classes = [klass]
11
+ return classes if klass == ActiveEntity::Base
12
+
13
+ while !klass.base_class?
14
+ classes << klass = klass.superclass
15
+ end
16
+ classes
17
+ end
18
+
19
+ # Set the i18n scope to overwrite ActiveModel.
20
+ def i18n_scope #:nodoc:
21
+ :active_entity
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_model/type"
4
+
5
+ require "active_entity/type/internal/timezone"
6
+
7
+ require "active_entity/type/date"
8
+ require "active_entity/type/date_time"
9
+ require "active_entity/type/decimal_without_scale"
10
+ require "active_entity/type/json"
11
+ require "active_entity/type/time"
12
+ require "active_entity/type/text"
13
+ require "active_entity/type/unsigned_integer"
14
+
15
+ require "active_entity/type/modifiers/array"
16
+
17
+ require "active_entity/type/serialized"
18
+ require "active_entity/type/registry"
19
+
20
+ require "active_entity/type/type_map"
21
+ require "active_entity/type/hash_lookup_type_map"
22
+
23
+ module ActiveEntity
24
+ module Type
25
+ @registry = ActiveEntity::Type::Registry.new
26
+
27
+ class << self
28
+ attr_accessor :registry # :nodoc:
29
+ delegate :add_modifier, to: :registry
30
+
31
+ # Add a new type to the registry, allowing it to be referenced as a
32
+ # symbol by {ActiveEntity::Base.attribute}[rdoc-ref:Attributes::ClassMethods#attribute].
33
+ # <tt>override: true</tt> will cause your type to be used instead of the native type.
34
+ # <tt>override: false</tt> will cause the native type to be used over yours if one exists.
35
+ def register(type_name, klass = nil, **options, &block)
36
+ registry.register(type_name, klass, **options, &block)
37
+ end
38
+
39
+ def lookup(*args, **kwargs) # :nodoc:
40
+ registry.lookup(*args, **kwargs)
41
+ end
42
+
43
+ def default_value # :nodoc:
44
+ @default_value ||= Value.new
45
+ end
46
+ end
47
+
48
+ Helpers = ActiveModel::Type::Helpers
49
+ BigInteger = ActiveModel::Type::BigInteger
50
+ Binary = ActiveModel::Type::Binary
51
+ Boolean = ActiveModel::Type::Boolean
52
+ Decimal = ActiveModel::Type::Decimal
53
+ Float = ActiveModel::Type::Float
54
+ Integer = ActiveModel::Type::Integer
55
+ String = ActiveModel::Type::String
56
+ Value = ActiveModel::Type::Value
57
+
58
+ add_modifier({ array: true }, Modifiers::Array)
59
+
60
+ register(:big_integer, Type::BigInteger, override: false)
61
+ register(:binary, Type::Binary, override: false)
62
+ register(:boolean, Type::Boolean, override: false)
63
+ register(:date, Type::Date, override: false)
64
+ register(:datetime, Type::DateTime, override: false)
65
+ register(:decimal, Type::Decimal, override: false)
66
+ register(:float, Type::Float, override: false)
67
+ register(:integer, Type::Integer, override: false)
68
+ register(:json, Type::Json, override: false)
69
+ register(:string, Type::String, override: false)
70
+ register(:text, Type::Text, override: false)
71
+ register(:time, Type::Time, override: false)
72
+ end
73
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveEntity
4
+ module Type
5
+ class Date < ActiveModel::Type::Date
6
+ include Internal::Timezone
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveEntity
4
+ module Type
5
+ class DateTime < ActiveModel::Type::DateTime
6
+ include Internal::Timezone
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveEntity
4
+ module Type
5
+ class DecimalWithoutScale < ActiveModel::Type::BigInteger # :nodoc:
6
+ def type
7
+ :decimal
8
+ end
9
+
10
+ def type_cast_for_schema(value)
11
+ value.to_s.inspect
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveEntity
4
+ module Type
5
+ class HashLookupTypeMap < TypeMap # :nodoc:
6
+ def alias_type(type, alias_type)
7
+ register_type(type) { |_, *args| lookup(alias_type, *args) }
8
+ end
9
+
10
+ def key?(key)
11
+ @mapping.key?(key)
12
+ end
13
+
14
+ def keys
15
+ @mapping.keys
16
+ end
17
+
18
+ private
19
+
20
+ def perform_fetch(type, *args, &block)
21
+ @mapping.fetch(type, block).call(type, *args)
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveEntity
4
+ module Type
5
+ module Internal
6
+ module Timezone
7
+ def is_utc?
8
+ ActiveEntity::Base.default_timezone == :utc
9
+ end
10
+
11
+ def default_timezone
12
+ ActiveEntity::Base.default_timezone
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end