tallty_duck_record 1.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 (79) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +41 -0
  3. data/README.md +82 -0
  4. data/Rakefile +28 -0
  5. data/lib/core_ext/array_without_blank.rb +46 -0
  6. data/lib/duck_record.rb +65 -0
  7. data/lib/duck_record/associations.rb +130 -0
  8. data/lib/duck_record/associations/association.rb +271 -0
  9. data/lib/duck_record/associations/belongs_to_association.rb +71 -0
  10. data/lib/duck_record/associations/builder/association.rb +127 -0
  11. data/lib/duck_record/associations/builder/belongs_to.rb +44 -0
  12. data/lib/duck_record/associations/builder/collection_association.rb +45 -0
  13. data/lib/duck_record/associations/builder/embeds_many.rb +9 -0
  14. data/lib/duck_record/associations/builder/embeds_one.rb +9 -0
  15. data/lib/duck_record/associations/builder/has_many.rb +11 -0
  16. data/lib/duck_record/associations/builder/has_one.rb +20 -0
  17. data/lib/duck_record/associations/builder/singular_association.rb +33 -0
  18. data/lib/duck_record/associations/collection_association.rb +476 -0
  19. data/lib/duck_record/associations/collection_proxy.rb +1160 -0
  20. data/lib/duck_record/associations/embeds_association.rb +92 -0
  21. data/lib/duck_record/associations/embeds_many_association.rb +203 -0
  22. data/lib/duck_record/associations/embeds_many_proxy.rb +892 -0
  23. data/lib/duck_record/associations/embeds_one_association.rb +48 -0
  24. data/lib/duck_record/associations/foreign_association.rb +11 -0
  25. data/lib/duck_record/associations/has_many_association.rb +17 -0
  26. data/lib/duck_record/associations/has_one_association.rb +39 -0
  27. data/lib/duck_record/associations/singular_association.rb +73 -0
  28. data/lib/duck_record/attribute.rb +213 -0
  29. data/lib/duck_record/attribute/user_provided_default.rb +30 -0
  30. data/lib/duck_record/attribute_assignment.rb +118 -0
  31. data/lib/duck_record/attribute_decorators.rb +89 -0
  32. data/lib/duck_record/attribute_methods.rb +325 -0
  33. data/lib/duck_record/attribute_methods/before_type_cast.rb +76 -0
  34. data/lib/duck_record/attribute_methods/dirty.rb +107 -0
  35. data/lib/duck_record/attribute_methods/read.rb +78 -0
  36. data/lib/duck_record/attribute_methods/serialization.rb +66 -0
  37. data/lib/duck_record/attribute_methods/write.rb +70 -0
  38. data/lib/duck_record/attribute_mutation_tracker.rb +108 -0
  39. data/lib/duck_record/attribute_set.rb +98 -0
  40. data/lib/duck_record/attribute_set/yaml_encoder.rb +41 -0
  41. data/lib/duck_record/attributes.rb +262 -0
  42. data/lib/duck_record/base.rb +300 -0
  43. data/lib/duck_record/callbacks.rb +324 -0
  44. data/lib/duck_record/coders/json.rb +13 -0
  45. data/lib/duck_record/coders/yaml_column.rb +48 -0
  46. data/lib/duck_record/core.rb +262 -0
  47. data/lib/duck_record/define_callbacks.rb +23 -0
  48. data/lib/duck_record/enum.rb +139 -0
  49. data/lib/duck_record/errors.rb +71 -0
  50. data/lib/duck_record/inheritance.rb +130 -0
  51. data/lib/duck_record/locale/en.yml +46 -0
  52. data/lib/duck_record/model_schema.rb +71 -0
  53. data/lib/duck_record/nested_attributes.rb +555 -0
  54. data/lib/duck_record/nested_validate_association.rb +262 -0
  55. data/lib/duck_record/persistence.rb +39 -0
  56. data/lib/duck_record/readonly_attributes.rb +36 -0
  57. data/lib/duck_record/reflection.rb +650 -0
  58. data/lib/duck_record/serialization.rb +26 -0
  59. data/lib/duck_record/translation.rb +22 -0
  60. data/lib/duck_record/type.rb +77 -0
  61. data/lib/duck_record/type/array.rb +36 -0
  62. data/lib/duck_record/type/array_without_blank.rb +36 -0
  63. data/lib/duck_record/type/date.rb +7 -0
  64. data/lib/duck_record/type/date_time.rb +7 -0
  65. data/lib/duck_record/type/decimal_without_scale.rb +13 -0
  66. data/lib/duck_record/type/internal/abstract_json.rb +33 -0
  67. data/lib/duck_record/type/internal/timezone.rb +15 -0
  68. data/lib/duck_record/type/json.rb +6 -0
  69. data/lib/duck_record/type/registry.rb +97 -0
  70. data/lib/duck_record/type/serialized.rb +63 -0
  71. data/lib/duck_record/type/text.rb +9 -0
  72. data/lib/duck_record/type/time.rb +19 -0
  73. data/lib/duck_record/type/unsigned_integer.rb +15 -0
  74. data/lib/duck_record/validations.rb +67 -0
  75. data/lib/duck_record/validations/subset.rb +74 -0
  76. data/lib/duck_record/validations/uniqueness_on_real_record.rb +248 -0
  77. data/lib/duck_record/version.rb +3 -0
  78. data/lib/tasks/acts_as_record_tasks.rake +4 -0
  79. metadata +181 -0
@@ -0,0 +1,23 @@
1
+ module DuckRecord
2
+ # This module exists because `DuckRecord::AttributeMethods::Dirty` needs to
3
+ # define callbacks, but continue to have its version of `save` be the super
4
+ # method of `DuckRecord::Callbacks`. This will be removed when the removal
5
+ # of deprecated code removes this need.
6
+ module DefineCallbacks
7
+ extend ActiveSupport::Concern
8
+
9
+ CALLBACKS = [
10
+ :after_initialize, :before_validation, :after_validation,
11
+ ]
12
+
13
+ module ClassMethods # :nodoc:
14
+ include ActiveModel::Callbacks
15
+ end
16
+
17
+ included do
18
+ include ActiveModel::Validations::Callbacks
19
+
20
+ define_model_callbacks :initialize, only: :after
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,139 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/core_ext/object/deep_dup"
4
+
5
+ module DuckRecord
6
+ module Enum
7
+ def self.extended(base) # :nodoc:
8
+ base.class_attribute(:defined_enums, instance_writer: false)
9
+ base.defined_enums = {}
10
+ end
11
+
12
+ def inherited(base) # :nodoc:
13
+ base.defined_enums = defined_enums.deep_dup
14
+ super
15
+ end
16
+
17
+ class EnumType < ActiveModel::Type::Value # :nodoc:
18
+ delegate :type, to: :subtype
19
+
20
+ def initialize(name, mapping, subtype)
21
+ @name = name
22
+ @mapping = mapping
23
+ @subtype = subtype
24
+ end
25
+
26
+ def cast(value)
27
+ return if value.blank?
28
+
29
+ if mapping.has_key?(value)
30
+ value.to_s
31
+ elsif mapping.has_value?(value)
32
+ mapping.key(value)
33
+ else
34
+ assert_valid_value(value)
35
+ end
36
+ end
37
+
38
+ def deserialize(value)
39
+ return if value.nil?
40
+ mapping.key(subtype.deserialize(value))
41
+ end
42
+
43
+ def serialize(value)
44
+ mapping.fetch(value, value)
45
+ end
46
+
47
+ def assert_valid_value(value)
48
+ unless value.blank? || mapping.has_key?(value) || mapping.has_value?(value)
49
+ raise ArgumentError, "'#{value}' is not a valid #{name}"
50
+ end
51
+ end
52
+
53
+ private
54
+
55
+ attr_reader :name, :mapping, :subtype
56
+ end
57
+
58
+ def enum(definitions)
59
+ klass = self
60
+ enum_prefix = definitions.delete(:_prefix)
61
+ enum_suffix = definitions.delete(:_suffix)
62
+ definitions.each do |name, values|
63
+ # statuses = { }
64
+ enum_values = ActiveSupport::HashWithIndifferentAccess.new
65
+ name = name.to_sym
66
+
67
+ # def self.statuses() statuses end
68
+ detect_enum_conflict!(name, name.to_s.pluralize, true)
69
+ klass.singleton_class.send(:define_method, name.to_s.pluralize) { enum_values }
70
+
71
+ detect_enum_conflict!(name, name)
72
+ detect_enum_conflict!(name, "#{name}=")
73
+
74
+ attr = attribute_alias?(name) ? attribute_alias(name) : name
75
+ decorate_attribute_type(attr, :enum) do |subtype|
76
+ EnumType.new(attr, enum_values, subtype)
77
+ end
78
+
79
+ _enum_methods_module.module_eval do
80
+ pairs = values.respond_to?(:each_pair) ? values.each_pair : values.each_with_index
81
+ pairs.each do |value, i|
82
+ if enum_prefix == true
83
+ prefix = "#{name}_"
84
+ elsif enum_prefix
85
+ prefix = "#{enum_prefix}_"
86
+ end
87
+ if enum_suffix == true
88
+ suffix = "_#{name}"
89
+ elsif enum_suffix
90
+ suffix = "_#{enum_suffix}"
91
+ end
92
+
93
+ value_method_name = "#{prefix}#{value}#{suffix}"
94
+ enum_values[value] = i
95
+
96
+ # def active?() status == 0 end
97
+ klass.send(:detect_enum_conflict!, name, "#{value_method_name}?")
98
+ define_method("#{value_method_name}?") { self[attr] == value.to_s }
99
+ end
100
+ end
101
+ defined_enums[name.to_s] = enum_values
102
+ end
103
+ end
104
+
105
+ private
106
+ def _enum_methods_module
107
+ @_enum_methods_module ||= begin
108
+ mod = Module.new
109
+ include mod
110
+ mod
111
+ end
112
+ end
113
+
114
+ ENUM_CONFLICT_MESSAGE = \
115
+ "You tried to define an enum named \"%{enum}\" on the model \"%{klass}\", but " \
116
+ "this will generate a %{type} method \"%{method}\", which is already defined " \
117
+ "by %{source}."
118
+
119
+ def detect_enum_conflict!(enum_name, method_name, klass_method = false)
120
+ if klass_method && dangerous_class_method?(method_name)
121
+ raise_conflict_error(enum_name, method_name, type: "class")
122
+ elsif !klass_method && dangerous_attribute_method?(method_name)
123
+ raise_conflict_error(enum_name, method_name)
124
+ elsif !klass_method && method_defined_within?(method_name, _enum_methods_module, Module)
125
+ raise_conflict_error(enum_name, method_name, source: "another enum")
126
+ end
127
+ end
128
+
129
+ def raise_conflict_error(enum_name, method_name, type: "instance", source: "Active Record")
130
+ raise ArgumentError, ENUM_CONFLICT_MESSAGE % {
131
+ enum: enum_name,
132
+ klass: name,
133
+ type: type,
134
+ method: method_name,
135
+ source: source
136
+ }
137
+ end
138
+ end
139
+ end
@@ -0,0 +1,71 @@
1
+ module DuckRecord
2
+ # = Active Record Errors
3
+ #
4
+ # Generic Active Record exception class.
5
+ class DuckRecordError < StandardError
6
+ end
7
+
8
+ # Raised on attempt to update record that is instantiated as read only.
9
+ class ReadOnlyRecord < DuckRecordError
10
+ end
11
+
12
+ # Raised when attribute has a name reserved by Active Record (when attribute
13
+ # has name of one of Active Record instance methods).
14
+ class DangerousAttributeError < DuckRecordError
15
+ end
16
+
17
+ # Raised when association is being configured improperly or user tries to use
18
+ # offset and limit together with
19
+ # {ActiveRecord::Base.has_many}[rdoc-ref:Associations::ClassMethods#has_many] or
20
+ # {ActiveRecord::Base.has_and_belongs_to_many}[rdoc-ref:Associations::ClassMethods#has_and_belongs_to_many]
21
+ # associations.
22
+ class ConfigurationError < DuckRecordError
23
+ end
24
+
25
+ # Raised when an object assigned to an association has an incorrect type.
26
+ #
27
+ # class Ticket < ActiveRecord::Base
28
+ # has_many :patches
29
+ # end
30
+ #
31
+ # class Patch < ActiveRecord::Base
32
+ # belongs_to :ticket
33
+ # end
34
+ #
35
+ # # Comments are not patches, this assignment raises AssociationTypeMismatch.
36
+ # @ticket.patches << Comment.new(content: "Please attach tests to your patch.")
37
+ class AssociationTypeMismatch < DuckRecordError
38
+ end
39
+
40
+ # Raised when unknown attributes are supplied via mass assignment.
41
+ UnknownAttributeError = ActiveModel::UnknownAttributeError
42
+
43
+ # Raised when an error occurred while doing a mass assignment to an attribute through the
44
+ # {DuckRecord::Base#attributes=}[rdoc-ref:AttributeAssignment#attributes=] method.
45
+ # The exception has an +attribute+ property that is the name of the offending attribute.
46
+ class AttributeAssignmentError < DuckRecordError
47
+ attr_reader :exception, :attribute
48
+
49
+ def initialize(message = nil, exception = nil, attribute = nil)
50
+ super(message)
51
+ @exception = exception
52
+ @attribute = attribute
53
+ end
54
+ end
55
+
56
+ # Raised when unserialized object's type mismatches one specified for serializable field.
57
+ class SerializationTypeMismatch < DuckRecordError
58
+ end
59
+
60
+ # Raised when there are multiple errors while doing a mass assignment through the
61
+ # {DuckRecord::Base#attributes=}[rdoc-ref:AttributeAssignment#attributes=]
62
+ # method. The exception has an +errors+ property that contains an array of AttributeAssignmentError
63
+ # objects, each corresponding to the error while assigning to an attribute.
64
+ class MultiparameterAssignmentErrors < DuckRecordError
65
+ attr_reader :errors
66
+
67
+ def initialize(errors = nil)
68
+ @errors = errors
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,130 @@
1
+ require "active_support/core_ext/hash/indifferent_access"
2
+
3
+ module DuckRecord
4
+ # == Single table inheritance
5
+ #
6
+ # Active Record allows inheritance by storing the name of the class in a column that by
7
+ # default is named "type" (can be changed by overwriting <tt>Base.inheritance_column</tt>).
8
+ # This means that an inheritance looking like this:
9
+ #
10
+ # class Company < DuckRecord::Base; end
11
+ # class Firm < Company; end
12
+ # class Client < Company; end
13
+ # class PriorityClient < Client; end
14
+ #
15
+ # When you do <tt>Firm.create(name: "37signals")</tt>, this record will be saved in
16
+ # the companies table with type = "Firm". You can then fetch this row again using
17
+ # <tt>Company.where(name: '37signals').first</tt> and it will return a Firm object.
18
+ #
19
+ # Be aware that because the type column is an attribute on the record every new
20
+ # subclass will instantly be marked as dirty and the type column will be included
21
+ # in the list of changed attributes on the record. This is different from non
22
+ # Single Table Inheritance(STI) classes:
23
+ #
24
+ # Company.new.changed? # => false
25
+ # Firm.new.changed? # => true
26
+ # Firm.new.changes # => {"type"=>["","Firm"]}
27
+ #
28
+ # If you don't have a type column defined in your table, single-table inheritance won't
29
+ # be triggered. In that case, it'll work just like normal subclasses with no special magic
30
+ # for differentiating between them or reloading the right type with find.
31
+ #
32
+ # Note, all the attributes for all the cases are kept in the same table. Read more:
33
+ # http://www.martinfowler.com/eaaCatalog/singleTableInheritance.html
34
+ #
35
+ module Inheritance
36
+ extend ActiveSupport::Concern
37
+
38
+ module ClassMethods
39
+ # Determines if one of the attributes passed in is the inheritance column,
40
+ # and if the inheritance column is attr accessible, it initializes an
41
+ # instance of the given subclass instead of the base class.
42
+ def new(*args, &block)
43
+ if abstract_class? || self == Base
44
+ raise NotImplementedError, "#{self} is an abstract class and cannot be instantiated."
45
+ end
46
+
47
+ super
48
+ end
49
+
50
+ # Returns the class descending directly from DuckRecord::Base, or
51
+ # an abstract class, if any, in the inheritance hierarchy.
52
+ #
53
+ # If A extends DuckRecord::Base, A.base_class will return A. If B descends from A
54
+ # through some arbitrarily deep hierarchy, B.base_class will return A.
55
+ #
56
+ # If B < A and C < B and if A is an abstract_class then both B.base_class
57
+ # and C.base_class would return B as the answer since A is an abstract_class.
58
+ def base_class
59
+ unless self < Base
60
+ raise DuckRecordError, "#{name} doesn't belong in a hierarchy descending from DuckRecord"
61
+ end
62
+
63
+ if superclass == Base || superclass.abstract_class?
64
+ self
65
+ else
66
+ superclass.base_class
67
+ end
68
+ end
69
+
70
+ # Set this to true if this is an abstract class (see <tt>abstract_class?</tt>).
71
+ # If you are using inheritance with DuckRecord and don't want child classes
72
+ # to utilize the implied STI table name of the parent class, this will need to be true.
73
+ # For example, given the following:
74
+ #
75
+ # class SuperClass < DuckRecord::Base
76
+ # self.abstract_class = true
77
+ # end
78
+ # class Child < SuperClass
79
+ # self.table_name = 'the_table_i_really_want'
80
+ # end
81
+ #
82
+ #
83
+ # <tt>self.abstract_class = true</tt> is required to make <tt>Child<.find,.create, or any Arel method></tt> use <tt>the_table_i_really_want</tt> instead of a table called <tt>super_classes</tt>
84
+ #
85
+ attr_accessor :abstract_class
86
+
87
+ # Returns whether this class is an abstract class or not.
88
+ def abstract_class?
89
+ defined?(@abstract_class) && @abstract_class == true
90
+ end
91
+
92
+ def inherited(subclass)
93
+ subclass.instance_variable_set(:@_type_candidates_cache, Concurrent::Map.new)
94
+ super
95
+ end
96
+
97
+ protected
98
+
99
+ # Returns the class type of the record using the current module as a prefix. So descendants of
100
+ # MyApp::Business::Account would appear as MyApp::Business::AccountSubclass.
101
+ def compute_type(type_name)
102
+ if type_name.start_with?("::".freeze)
103
+ # If the type is prefixed with a scope operator then we assume that
104
+ # the type_name is an absolute reference.
105
+ ActiveSupport::Dependencies.constantize(type_name)
106
+ else
107
+ type_candidate = @_type_candidates_cache[type_name]
108
+ if type_candidate && type_constant = ActiveSupport::Dependencies.safe_constantize(type_candidate)
109
+ return type_constant
110
+ end
111
+
112
+ # Build a list of candidates to search for
113
+ candidates = []
114
+ type_name.scan(/::|$/) { candidates.unshift "#{$`}::#{type_name}" }
115
+ candidates << type_name
116
+
117
+ candidates.each do |candidate|
118
+ constant = ActiveSupport::Dependencies.safe_constantize(candidate)
119
+ if candidate == constant.to_s
120
+ @_type_candidates_cache[type_name] = candidate
121
+ return constant
122
+ end
123
+ end
124
+
125
+ raise NameError.new("uninitialized constant #{candidates.first}", candidates.first)
126
+ end
127
+ end
128
+ end
129
+ end
130
+ end
@@ -0,0 +1,46 @@
1
+ en:
2
+ # Attributes names common to most models
3
+ #attributes:
4
+ #created_at: "Created at"
5
+ #updated_at: "Updated at"
6
+
7
+ # Default error messages
8
+ errors:
9
+ messages:
10
+ required: "must exist"
11
+ taken: "has already been taken"
12
+ subset: "is not included in the list"
13
+
14
+ # Active Record models configuration
15
+ duck_record:
16
+ errors:
17
+ messages:
18
+ record_invalid: "Validation failed: %{errors}"
19
+ # Append your own errors here or at the model/attributes scope.
20
+
21
+ # You can define own errors for models or model attributes.
22
+ # The values :model, :attribute and :value are always available for interpolation.
23
+ #
24
+ # For example,
25
+ # models:
26
+ # user:
27
+ # blank: "This is a custom blank message for %{model}: %{attribute}"
28
+ # attributes:
29
+ # login:
30
+ # blank: "This is a custom blank message for User login"
31
+ # Will define custom blank validation message for User model and
32
+ # custom blank validation message for login attribute of User model.
33
+ #models:
34
+
35
+ # Translate model names. Used in Model.human_name().
36
+ #models:
37
+ # For example,
38
+ # user: "Dude"
39
+ # will translate User model name to "Dude"
40
+
41
+ # Translate model attribute names. Used in Model.human_attribute_name(attribute).
42
+ #attributes:
43
+ # For example,
44
+ # user:
45
+ # login: "Handle"
46
+ # will translate User attribute "login" as "Handle"
@@ -0,0 +1,71 @@
1
+ module DuckRecord
2
+ module ModelSchema
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ delegate :type_for_attribute, to: :class
7
+ end
8
+
9
+ module ClassMethods
10
+ def attribute_types # :nodoc:
11
+ load_schema
12
+ @attribute_types ||= Hash.new(Type.default_value)
13
+ end
14
+
15
+ def yaml_encoder # :nodoc:
16
+ @yaml_encoder ||= AttributeSet::YAMLEncoder.new(attribute_types)
17
+ end
18
+
19
+ # Returns the type of the attribute with the given name, after applying
20
+ # all modifiers. This method is the only valid source of information for
21
+ # anything related to the types of a model's attributes. This method will
22
+ # access the database and load the model's schema if it is required.
23
+ #
24
+ # The return value of this method will implement the interface described
25
+ # by ActiveModel::Type::Value (though the object itself may not subclass
26
+ # it).
27
+ #
28
+ # +attr_name+ The name of the attribute to retrieve the type for. Must be
29
+ # a string
30
+ def type_for_attribute(attr_name, &block)
31
+ if block
32
+ attribute_types.fetch(attr_name, &block)
33
+ else
34
+ attribute_types[attr_name]
35
+ end
36
+ end
37
+
38
+ def _default_attributes # :nodoc:
39
+ @default_attributes ||= AttributeSet.new({})
40
+ end
41
+
42
+ private
43
+
44
+ def schema_loaded?
45
+ defined?(@schema_loaded) && @schema_loaded
46
+ end
47
+
48
+ def load_schema
49
+ unless schema_loaded?
50
+ load_schema!
51
+ end
52
+ end
53
+
54
+ def load_schema!
55
+ @schema_loaded = true
56
+ end
57
+
58
+ def reload_schema_from_cache
59
+ @attribute_types = nil
60
+ @default_attributes = nil
61
+ @attributes_builder = nil
62
+ @schema_loaded = false
63
+ @attribute_names = nil
64
+ @yaml_encoder = nil
65
+ direct_descendants.each do |descendant|
66
+ descendant.send(:reload_schema_from_cache)
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end