u-attributes 2.0.1 → 2.4.0

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