activeentity 0.0.1.beta1

Sign up to get free protection for your applications and to get access to all the features.
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