activeentity 0.0.1.beta1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/MIT-LICENSE +42 -0
- data/README.md +145 -0
- data/Rakefile +29 -0
- data/lib/active_entity.rb +73 -0
- data/lib/active_entity/aggregations.rb +276 -0
- data/lib/active_entity/associations.rb +146 -0
- data/lib/active_entity/associations/embedded/association.rb +134 -0
- data/lib/active_entity/associations/embedded/builder/association.rb +100 -0
- data/lib/active_entity/associations/embedded/builder/collection_association.rb +69 -0
- data/lib/active_entity/associations/embedded/builder/embedded_in.rb +38 -0
- data/lib/active_entity/associations/embedded/builder/embeds_many.rb +13 -0
- data/lib/active_entity/associations/embedded/builder/embeds_one.rb +16 -0
- data/lib/active_entity/associations/embedded/builder/singular_association.rb +28 -0
- data/lib/active_entity/associations/embedded/collection_association.rb +188 -0
- data/lib/active_entity/associations/embedded/collection_proxy.rb +310 -0
- data/lib/active_entity/associations/embedded/embedded_in_association.rb +31 -0
- data/lib/active_entity/associations/embedded/embeds_many_association.rb +15 -0
- data/lib/active_entity/associations/embedded/embeds_one_association.rb +19 -0
- data/lib/active_entity/associations/embedded/singular_association.rb +35 -0
- data/lib/active_entity/attribute_assignment.rb +85 -0
- data/lib/active_entity/attribute_decorators.rb +90 -0
- data/lib/active_entity/attribute_methods.rb +330 -0
- data/lib/active_entity/attribute_methods/before_type_cast.rb +78 -0
- data/lib/active_entity/attribute_methods/primary_key.rb +98 -0
- data/lib/active_entity/attribute_methods/query.rb +35 -0
- data/lib/active_entity/attribute_methods/read.rb +47 -0
- data/lib/active_entity/attribute_methods/serialization.rb +90 -0
- data/lib/active_entity/attribute_methods/time_zone_conversion.rb +91 -0
- data/lib/active_entity/attribute_methods/write.rb +63 -0
- data/lib/active_entity/attributes.rb +165 -0
- data/lib/active_entity/base.rb +303 -0
- data/lib/active_entity/coders/json.rb +15 -0
- data/lib/active_entity/coders/yaml_column.rb +50 -0
- data/lib/active_entity/core.rb +281 -0
- data/lib/active_entity/define_callbacks.rb +17 -0
- data/lib/active_entity/enum.rb +234 -0
- data/lib/active_entity/errors.rb +80 -0
- data/lib/active_entity/gem_version.rb +17 -0
- data/lib/active_entity/inheritance.rb +278 -0
- data/lib/active_entity/integration.rb +78 -0
- data/lib/active_entity/locale/en.yml +45 -0
- data/lib/active_entity/model_schema.rb +115 -0
- data/lib/active_entity/nested_attributes.rb +592 -0
- data/lib/active_entity/readonly_attributes.rb +47 -0
- data/lib/active_entity/reflection.rb +441 -0
- data/lib/active_entity/serialization.rb +25 -0
- data/lib/active_entity/store.rb +242 -0
- data/lib/active_entity/translation.rb +24 -0
- data/lib/active_entity/type.rb +73 -0
- data/lib/active_entity/type/date.rb +9 -0
- data/lib/active_entity/type/date_time.rb +9 -0
- data/lib/active_entity/type/decimal_without_scale.rb +15 -0
- data/lib/active_entity/type/hash_lookup_type_map.rb +25 -0
- data/lib/active_entity/type/internal/timezone.rb +17 -0
- data/lib/active_entity/type/json.rb +30 -0
- data/lib/active_entity/type/modifiers/array.rb +72 -0
- data/lib/active_entity/type/registry.rb +92 -0
- data/lib/active_entity/type/serialized.rb +71 -0
- data/lib/active_entity/type/text.rb +11 -0
- data/lib/active_entity/type/time.rb +21 -0
- data/lib/active_entity/type/type_map.rb +62 -0
- data/lib/active_entity/type/unsigned_integer.rb +17 -0
- data/lib/active_entity/validate_embedded_association.rb +305 -0
- data/lib/active_entity/validations.rb +50 -0
- data/lib/active_entity/validations/absence.rb +25 -0
- data/lib/active_entity/validations/associated.rb +60 -0
- data/lib/active_entity/validations/length.rb +26 -0
- data/lib/active_entity/validations/presence.rb +68 -0
- data/lib/active_entity/validations/subset.rb +76 -0
- data/lib/active_entity/validations/uniqueness_in_embedding.rb +99 -0
- data/lib/active_entity/version.rb +10 -0
- data/lib/tasks/active_entity_tasks.rake +6 -0
- 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,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
|