tallty_duck_record 1.0.0

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