u-attributes 2.0.1 → 2.4.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.
@@ -14,7 +14,8 @@ module Micro
14
14
  base.extend(::Micro::Attributes.const_get(:Macros))
15
15
 
16
16
  base.class_eval do
17
- private_class_method :__attributes, :__attribute_set, :__attribute_reader
17
+ private_class_method :__attributes, :__attribute_reader
18
+ private_class_method :__attribute_assign, :__attributes_data_to_assign
18
19
  end
19
20
 
20
21
  def base.inherited(subclass)
@@ -54,48 +55,96 @@ module Micro
54
55
  raise NameError, "undefined attribute `#{name}"
55
56
  end
56
57
 
58
+ def defined_attributes
59
+ @defined_attributes ||= self.class.attributes
60
+ end
61
+
57
62
  def attributes(*names)
58
63
  return __attributes if names.empty?
59
64
 
60
- names.each_with_object({}) do |name, memo|
61
- memo[name] = attribute(name) if attribute?(name)
65
+ options = names.last.is_a?(Hash) ? names.pop : Kind::Empty::HASH
66
+
67
+ names.flatten!
68
+
69
+ without_option = Array(options.fetch(:without, Kind::Empty::ARRAY))
70
+
71
+ keys = names.empty? ? defined_attributes - without_option.map { |value| __attribute_key(value) } : names - without_option
72
+
73
+ data = keys.each_with_object({}) { |key, memo| memo[key] = attribute(key) if attribute?(key) }
74
+
75
+ with_option = Array(options.fetch(:with, Kind::Empty::ARRAY))
76
+
77
+ unless with_option.empty?
78
+ extra = with_option.each_with_object({}) { |key, memo| memo[__attribute_key(key)] = public_send(key) }
79
+
80
+ data.merge!(extra)
62
81
  end
82
+
83
+ Utils::Hashes.keys_as(options[:keys_as], data)
63
84
  end
64
85
 
65
86
  protected
66
87
 
67
88
  def attributes=(arg)
68
- hash = Utils.stringify_hash_keys(arg)
89
+ hash = self.class.__attributes_keys__(arg)
69
90
 
70
- __attributes_set(hash, self.class.__attributes_data__)
91
+ __attributes_missing!(hash)
92
+
93
+ __attributes_assign(hash)
71
94
  end
72
95
 
73
96
  private
74
97
 
75
- def __attributes
76
- @__attributes ||= {}
98
+ def extract_attributes_from(other)
99
+ Utils::ExtractAttribute.from(other, keys: defined_attributes)
77
100
  end
78
101
 
79
- def __attribute_set(name, value)
80
- __attributes[name] = instance_variable_set("@#{name}", value) if attribute?(name)
102
+ def __attribute_key(value)
103
+ self.class.__attribute_key__(value)
81
104
  end
82
105
 
83
- def __attributes_set(hash, att_data)
84
- att_data.each do |key, default|
85
- value = hash[key]
106
+ def __attributes
107
+ @__attributes ||= {}
108
+ end
86
109
 
87
- final_value =
88
- if default.respond_to?(:call)
89
- callable = default.is_a?(Proc) ? default : default.method(:call)
90
- callable.arity > 0 ? callable.call(value) : callable.call
91
- else
92
- value.nil? ? default : value
93
- end
110
+ FetchValueToAssign = -> (value, default) do
111
+ if default.is_a?(Proc)
112
+ default.arity > 0 ? default.call(value) : default.call
113
+ else
114
+ value.nil? ? default : value
115
+ end
116
+ end
94
117
 
95
- __attribute_set(key, final_value)
118
+ def __attributes_assign(hash)
119
+ self.class.__attributes_data__.each do |name, default|
120
+ __attribute_assign(name, FetchValueToAssign.(hash[name], default)) if attribute?(name)
96
121
  end
97
122
 
98
123
  __attributes.freeze
99
124
  end
125
+
126
+ def __attribute_assign(name, value)
127
+ __attributes[name] = instance_variable_set("@#{name}", value)
128
+ end
129
+
130
+ MISSING_KEYWORD = 'missing keyword'.freeze
131
+ MISSING_KEYWORDS = 'missing keywords'.freeze
132
+
133
+ def __attributes_missing!(hash)
134
+ required_keys = self.class.__attributes_required__
135
+
136
+ return if required_keys.empty?
137
+
138
+ missing_keys = required_keys.map { |name| ":#{name}" if !hash.key?(name) }
139
+ missing_keys.compact!
140
+
141
+ return if missing_keys.empty?
142
+
143
+ label = missing_keys.size == 1 ? MISSING_KEYWORD : MISSING_KEYWORDS
144
+
145
+ raise ArgumentError, "#{label}: #{missing_keys.join(', ')}"
146
+ end
147
+
148
+ private_constant :FetchValueToAssign, :MISSING_KEYWORD, :MISSING_KEYWORDS
100
149
  end
101
150
  end
@@ -3,15 +3,20 @@
3
3
  module Micro::Attributes
4
4
  module Diff
5
5
  class Changes
6
- TO = 'to'.freeze
7
- FROM = 'from'.freeze
6
+ FROM_TO_SYM = [:from, :to].freeze
7
+ FROM_TO_STR = ['from'.freeze, 'to'.freeze].freeze
8
8
  FROM_TO_ERROR = 'pass the attribute name with the :from and :to values'.freeze
9
9
 
10
10
  attr_reader :from, :to, :differences
11
11
 
12
12
  def initialize(from:, to:)
13
- raise ArgumentError, "expected an instance of #{from.class}" unless to.is_a?(from.class)
14
- @from, @to = from, to
13
+ @from_class = from.class
14
+
15
+ @from, @to = from, Kind::Of.(@from_class, to)
16
+
17
+ @from_key, @to_key =
18
+ @from_class.attributes_access == :symbol ? FROM_TO_SYM : FROM_TO_STR
19
+
15
20
  @differences = diff(from.attributes, to.attributes).freeze
16
21
  end
17
22
 
@@ -27,26 +32,33 @@ module Micro::Attributes
27
32
  def changed?(name = nil, from: nil, to: nil)
28
33
  if name.nil?
29
34
  return present? if from.nil? && to.nil?
35
+
30
36
  raise ArgumentError, FROM_TO_ERROR
31
37
  elsif from.nil? && to.nil?
32
- differences.has_key?(name.to_s)
38
+ differences.has_key?(key_access(name))
33
39
  else
34
- result = @differences[name.to_s]
35
- result ? result[FROM] == from && result[TO] == to : false
40
+ result = @differences[key_access(name)]
41
+ result ? result[@from_key] == from && result[@to_key] == to : false
36
42
  end
37
43
  end
38
44
 
39
45
  private
40
46
 
41
- def diff(from_attributes, to_attributes)
42
- @from_attributes, @to_attributes = from_attributes, to_attributes
43
- @from_attributes.each_with_object({}) do |(from_key, from_val), acc|
44
- to_value = @to_attributes[from_key]
45
- acc[from_key] = {FROM => from_val, TO => to_value}.freeze if from_val != to_value
47
+ def key_access(key)
48
+ @from_class.__attribute_key__(key)
49
+ end
50
+
51
+ def diff(from_attributes, to_attributes)
52
+ @from_attributes, @to_attributes = from_attributes, to_attributes
53
+
54
+ @from_attributes.each_with_object({}) do |(from_key, from_val), acc|
55
+ to_value = @to_attributes[from_key]
56
+
57
+ acc[from_key] = {@from_key => from_val, @to_key => to_value}.freeze if from_val != to_value
58
+ end
46
59
  end
47
- end
48
60
 
49
- private_constant :TO, :FROM, :FROM_TO_ERROR
61
+ private_constant :FROM_TO_SYM, :FROM_TO_STR, :FROM_TO_ERROR
50
62
  end
51
63
  end
52
64
  end
@@ -7,105 +7,100 @@ module Micro
7
7
  module Features
8
8
  extend self
9
9
 
10
- STRICT_INITIALIZE = 'strict_initialize'.freeze
11
-
12
- ALL_VISIBLE = [
13
- DIFF = 'diff'.freeze,
14
- INITIALIZE = 'initialize'.freeze,
15
- ACTIVEMODEL_VALIDATIONS = 'activemodel_validations'.freeze
16
- ].sort.freeze
17
-
18
- ALL = (ALL_VISIBLE + [STRICT_INITIALIZE]).sort.freeze
19
-
20
- INVALID_NAME = [
21
- 'Invalid feature name! Available options: ',
22
- ALL_VISIBLE.map { |feature_name| ":#{feature_name}" }.join(', ')
23
- ].join
24
-
25
- OPTIONS = {
26
- # Features
27
- DIFF => With::Diff,
28
- INITIALIZE => With::Initialize,
29
- STRICT_INITIALIZE => With::StrictInitialize,
30
- ACTIVEMODEL_VALIDATIONS => With::ActiveModelValidations,
31
- # Combinations
32
- 'diff:initialize' => With::DiffAndInitialize,
33
- 'diff:strict_initialize' => With::DiffAndStrictInitialize,
34
- 'activemodel_validations:diff' => With::ActiveModelValidationsAndDiff,
35
- 'activemodel_validations:initialize' => With::ActiveModelValidationsAndInitialize,
36
- 'activemodel_validations:strict_initialize' => With::ActiveModelValidationsAndStrictInitialize,
37
- 'activemodel_validations:diff:initialize' => With::ActiveModelValidationsAndDiffAndInitialize,
38
- 'activemodel_validations:diff:strict_initialize' => With::ActiveModelValidationsAndDiffAndStrictInitialize
39
- }.freeze
40
-
41
- private_constant :OPTIONS, :INVALID_NAME
42
-
43
- def all
44
- @all ||= self.with(ALL)
10
+ module Name
11
+ ALL = [
12
+ DIFF = 'diff'.freeze,
13
+ INITIALIZE = 'initialize'.freeze,
14
+ KEYS_AS_SYMBOL = 'keys_as_symbol'.freeze,
15
+ ACTIVEMODEL_VALIDATIONS = 'activemodel_validations'.freeze
16
+ ].sort.freeze
45
17
  end
46
18
 
47
- def with(args)
48
- valid_names!(args) do |names|
49
- delete_initialize_if_has_strict_initialize(names)
50
-
51
- OPTIONS.fetch(names.sort.join(':'))
19
+ module Options
20
+ KEYS = [
21
+ DIFF = 'Diff'.freeze,
22
+ INIT = 'Init'.freeze,
23
+ INIT_STRICT = 'InitStrict'.freeze,
24
+ KEYS_AS_SYMBOL = 'KeysAsSymbol'.freeze,
25
+ AM_VALIDATIONS = 'AMValidations'.freeze
26
+ ].sort.freeze
27
+
28
+ NAMES_TO_KEYS = {
29
+ Name::DIFF => DIFF,
30
+ Name::INITIALIZE => INIT,
31
+ Name::KEYS_AS_SYMBOL => KEYS_AS_SYMBOL,
32
+ Name::ACTIVEMODEL_VALIDATIONS => AM_VALIDATIONS
33
+ }.freeze
34
+
35
+ KEYS_TO_MODULES = {
36
+ DIFF => With::Diff,
37
+ INIT => With::Initialize,
38
+ INIT_STRICT => With::StrictInitialize,
39
+ KEYS_AS_SYMBOL => With::KeysAsSymbol,
40
+ AM_VALIDATIONS => With::ActiveModelValidations
41
+ }.freeze
42
+
43
+ def self.fetch_key(arg)
44
+ if arg.is_a?(Hash)
45
+ INIT_STRICT if arg[:initialize] == :strict
46
+ else
47
+ name = String(arg)
48
+
49
+ return name if KEYS_TO_MODULES.key?(name)
50
+
51
+ NAMES_TO_KEYS[name]
52
+ end
52
53
  end
53
- end
54
54
 
55
- def without(args)
56
- valid_names!(args) do |names_to_exclude|
57
- names = except_options(names_to_exclude)
58
- names.empty? ? ::Micro::Attributes : self.with(names)
59
- end
60
- end
55
+ INVALID_NAME = [
56
+ 'Invalid feature name! Available options: ',
57
+ Name::ALL.map { |feature_name| ":#{feature_name}" }.join(', ')
58
+ ].join
61
59
 
62
- private
60
+ def self.fetch_keys(args)
61
+ keys = Array(args).dup.map { |name| fetch_key(name) }
63
62
 
64
- def fetch_feature_name(name)
65
- return name unless name.is_a?(Hash)
63
+ raise ArgumentError, INVALID_NAME if keys.empty? || !(keys - KEYS).empty?
66
64
 
67
- STRICT_INITIALIZE if name[:initialize] == :strict
65
+ yield(keys)
68
66
  end
69
67
 
70
- def normalize_names(args)
71
- names = Array(args).dup
72
-
73
- last_feature = fetch_feature_name(names.pop)
74
-
75
- features = names.empty? ? [last_feature] : names + [last_feature]
76
- features.map! { |name| name.to_s.downcase }
77
- features.uniq
68
+ def self.remove_init_keys(keys, if_has_init_in:)
69
+ keys.delete_if { |key| key == INIT || key == INIT_STRICT } if if_has_init_in.include?(INIT)
78
70
  end
79
71
 
80
- def valid_names?(names)
81
- names.all? { |name| ALL.include?(name) }
72
+ def self.without_keys(keys_to_exclude)
73
+ (KEYS - keys_to_exclude).tap do |keys|
74
+ remove_init_keys(keys, if_has_init_in: keys_to_exclude)
75
+ end
82
76
  end
83
77
 
84
- def valid_names!(args)
85
- names = normalize_names(args)
86
-
87
- raise ArgumentError, INVALID_NAME if names.empty? || !valid_names?(names)
78
+ def self.fetch_module_by_keys(keys)
79
+ keys.delete_if { |key| key == INIT } if keys.include?(INIT_STRICT)
88
80
 
89
- yield(names)
90
- end
81
+ option = keys.sort.join('_')
91
82
 
92
- def an_initialize?(name)
93
- name == INITIALIZE || name == STRICT_INITIALIZE
83
+ KEYS_TO_MODULES.fetch(option) { With.const_get(option, false) }
94
84
  end
85
+ end
95
86
 
96
- def delete_initialize_if_has_strict_initialize(names)
97
- return unless names.include?(STRICT_INITIALIZE)
87
+ def all
88
+ @all ||= self.with(Options::KEYS)
89
+ end
98
90
 
99
- names.delete_if { |name| name == INITIALIZE }
91
+ def with(names)
92
+ Options.fetch_keys(names) do |keys|
93
+ Options.fetch_module_by_keys(keys)
100
94
  end
95
+ end
101
96
 
102
- def except_options(names_to_exclude)
103
- (ALL - names_to_exclude).tap do |names|
104
- names.delete_if { |name| an_initialize?(name) } if names_to_exclude.include?(INITIALIZE)
97
+ def without(names)
98
+ Options.fetch_keys(names) do |keys|
99
+ keys = Options.without_keys(keys)
105
100
 
106
- delete_initialize_if_has_strict_initialize(names)
107
- end
101
+ keys.empty? ? ::Micro::Attributes : Options.fetch_module_by_keys(keys)
108
102
  end
103
+ end
109
104
  end
110
105
  end
111
106
  end
@@ -14,11 +14,11 @@ module Micro::Attributes
14
14
  end
15
15
 
16
16
  module ClassMethods
17
- def __call_after_attribute_set__(attr_name, options)
17
+ def __call_after_attribute_assign__(attr_name, options)
18
18
  validate, validates = options.values_at(:validate, :validates)
19
19
 
20
20
  self.validate(validate) if validate
21
- self.validates(attr_name, validates) if validates
21
+ self.validates(attr_name, validates.dup) if validates
22
22
  end
23
23
  end
24
24
 
@@ -4,35 +4,15 @@ module Micro::Attributes
4
4
  module Features
5
5
  module Initialize
6
6
  module Strict
7
- MISSING_KEYWORD = 'missing keyword'.freeze
8
- MISSING_KEYWORDS = 'missing keywords'.freeze
9
-
10
- protected def attributes=(arg)
11
- arg_hash = Utils.stringify_hash_keys(arg)
12
- att_data = self.class.__attributes_data__
13
-
14
- attributes_missing!(ref: att_data, arg: arg_hash)
15
-
16
- __attributes_set(arg_hash, att_data)
17
- end
18
-
19
- private def attributes_missing!(ref:, arg:)
20
- missing_keys = attributes_missing(ref, arg)
21
-
22
- return if missing_keys.empty?
23
-
24
- label = missing_keys.size == 1 ? MISSING_KEYWORD : MISSING_KEYWORDS
25
-
26
- raise ArgumentError, "#{label}: #{missing_keys.join(', ')}"
27
- end
28
-
29
- private def attributes_missing(ref, arg)
30
- ref.each_with_object([]) do |(key, val), memo|
31
- memo << ":#{key}" if val.nil? && !arg.has_key?(key)
7
+ module ClassMethods
8
+ def attributes_are_all_required?
9
+ true
32
10
  end
33
11
  end
34
12
 
35
- private_constant :MISSING_KEYWORD, :MISSING_KEYWORDS
13
+ def self.included(base)
14
+ base.send(:extend, ClassMethods)
15
+ end
36
16
  end
37
17
  end
38
18
  end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Micro::Attributes
4
+ module Features
5
+ module KeysAsSymbol
6
+
7
+ module ClassMethods
8
+ def attributes_access
9
+ :symbol
10
+ end
11
+
12
+ def __attribute_access__(value)
13
+ Kind::Of.(::Symbol, value)
14
+ end
15
+
16
+ def __attribute_key__(value)
17
+ value
18
+ end
19
+
20
+ def __attributes_keys__(hash)
21
+ Utils::Hashes.kind(hash)
22
+ end
23
+ end
24
+
25
+ def self.included(base)
26
+ base.send(:extend, ClassMethods)
27
+ end
28
+
29
+ end
30
+ end
31
+ end