u-attributes 2.2.0 → 2.7.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.
@@ -15,7 +15,8 @@ module Micro
15
15
 
16
16
  base.class_eval do
17
17
  private_class_method :__attributes, :__attribute_reader
18
- private_class_method :__attribute_assign, :__attributes_data_to_assign
18
+ private_class_method :__attribute_assign, :__attributes_groups
19
+ private_class_method :__attributes_required_add, :__attributes_data_to_assign
19
20
  end
20
21
 
21
22
  def base.inherited(subclass)
@@ -37,8 +38,8 @@ module Micro
37
38
  Features.all
38
39
  end
39
40
 
40
- def attribute?(name)
41
- self.class.attribute?(name)
41
+ def attribute?(name, include_all = false)
42
+ self.class.attribute?(name, include_all)
42
43
  end
43
44
 
44
45
  def attribute(name)
@@ -52,59 +53,108 @@ module Micro
52
53
  def attribute!(name, &block)
53
54
  attribute(name) { |name| return block ? block[name] : name }
54
55
 
55
- raise NameError, "undefined attribute `#{name}"
56
+ raise NameError, __attribute_access_error_message(name)
57
+ end
58
+
59
+ def defined_attributes(option = nil)
60
+ return self.class.attributes_by_visibility if option == :by_visibility
61
+
62
+ @defined_attributes ||= self.class.attributes
56
63
  end
57
64
 
58
65
  def attributes(*names)
59
66
  return __attributes if names.empty?
60
67
 
61
- names.each_with_object({}) do |name, memo|
62
- memo[name] = attribute(name) if attribute?(name)
68
+ options = names.last.is_a?(Hash) ? names.pop : Kind::Empty::HASH
69
+
70
+ names.flatten!
71
+
72
+ without_option = Array(options.fetch(:without, Kind::Empty::ARRAY))
73
+
74
+ keys = names.empty? ? defined_attributes - without_option.map { |value| __attribute_key(value) } : names - without_option
75
+
76
+ data = keys.each_with_object({}) { |key, memo| memo[key] = attribute(key) if attribute?(key) }
77
+
78
+ with_option = Array(options.fetch(:with, Kind::Empty::ARRAY))
79
+
80
+ unless with_option.empty?
81
+ extra = with_option.each_with_object({}) { |key, memo| memo[__attribute_key(key)] = public_send(key) }
82
+
83
+ data.merge!(extra)
63
84
  end
64
- end
65
85
 
66
- def defined_attributes
67
- @defined_attributes ||= self.class.attributes
86
+ Utils::Hashes.keys_as(options[:keys_as], data)
68
87
  end
69
88
 
70
89
  protected
71
90
 
72
91
  def attributes=(arg)
73
- hash = Utils::Hashes.stringify_keys(arg)
92
+ hash = self.class.__attributes_keys_transform__(arg)
74
93
 
75
94
  __attributes_missing!(hash)
76
95
 
96
+ __call_before_attributes_assign
77
97
  __attributes_assign(hash)
98
+ __call_after_attributes_assign
99
+
100
+ __attributes
78
101
  end
79
102
 
80
103
  private
81
104
 
105
+ def __call_before_attributes_assign; end
106
+ def __call_after_attributes_assign; end
107
+
82
108
  def extract_attributes_from(other)
83
109
  Utils::ExtractAttribute.from(other, keys: defined_attributes)
84
110
  end
85
111
 
112
+ def __attribute_access_error_message(name)
113
+ return "tried to access a private attribute `#{name}" if attribute?(name, true)
114
+
115
+ "undefined attribute `#{name}"
116
+ end
117
+
118
+ def __attribute_key(value)
119
+ self.class.__attribute_key_transform__(value)
120
+ end
121
+
86
122
  def __attributes
87
123
  @__attributes ||= {}
88
124
  end
89
125
 
90
- FetchValueToAssign = -> (value, default) do
91
- if default.is_a?(Proc)
92
- default.arity > 0 ? default.call(value) : default.call
93
- else
94
- value.nil? ? default : value
95
- end
126
+ FetchValueToAssign = -> (value, attribute_data, keep_proc = false) do
127
+ default = attribute_data[0]
128
+
129
+ value_to_assign =
130
+ if default.is_a?(Proc) && !keep_proc
131
+ default.arity > 0 ? default.call(value) : default.call
132
+ else
133
+ value.nil? ? default : value
134
+ end
135
+
136
+ return value_to_assign unless to_freeze = attribute_data[2]
137
+ return value_to_assign.freeze if to_freeze == true
138
+ return value_to_assign.dup.freeze if to_freeze == :after_dup
139
+ return value_to_assign.clone.freeze if to_freeze == :after_clone
140
+
141
+ raise NotImplementedError
96
142
  end
97
143
 
98
144
  def __attributes_assign(hash)
99
- self.class.__attributes_data__.each do |name, default|
100
- __attribute_assign(name, FetchValueToAssign.(hash[name], default)) if attribute?(name)
145
+ self.class.__attributes_data__.each do |name, attribute_data|
146
+ __attribute_assign(name, hash[name], attribute_data) if attribute?(name, true)
101
147
  end
102
148
 
103
149
  __attributes.freeze
104
150
  end
105
151
 
106
- def __attribute_assign(name, value)
107
- __attributes[name] = instance_variable_set("@#{name}", value)
152
+ def __attribute_assign(name, initialize_value, attribute_data)
153
+ value_to_assign = FetchValueToAssign.(initialize_value, attribute_data)
154
+
155
+ ivar_value = instance_variable_set("@#{name}", value_to_assign)
156
+
157
+ __attributes[name] = ivar_value if attribute_data[3] == :public
108
158
  end
109
159
 
110
160
  MISSING_KEYWORD = 'missing keyword'.freeze
@@ -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_transform(name))
33
39
  else
34
- result = @differences[name.to_s]
35
- result ? result[FROM] == from && result[TO] == to : false
40
+ result = @differences[key_transform(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_transform(key)
48
+ @from_class.__attribute_key_transform__(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
@@ -1,111 +1,164 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'micro/attributes/with'
4
-
5
3
  module Micro
6
4
  module Attributes
5
+ module With
6
+ end
7
+
7
8
  module Features
8
- extend self
9
+ require 'micro/attributes/features/diff'
10
+ require 'micro/attributes/features/accept'
11
+ require 'micro/attributes/features/accept/strict'
12
+ require 'micro/attributes/features/initialize'
13
+ require 'micro/attributes/features/initialize/strict'
14
+ require 'micro/attributes/features/keys_as_symbol'
15
+ require 'micro/attributes/features/activemodel_validations'
9
16
 
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
17
+ extend self
42
18
 
43
- def all
44
- @all ||= self.with(ALL)
19
+ module Name
20
+ ALL = [
21
+ DIFF = 'diff'.freeze,
22
+ ACCEPT = 'accept'.freeze,
23
+ INITIALIZE = 'initialize'.freeze,
24
+ KEYS_AS_SYMBOL = 'keys_as_symbol'.freeze,
25
+ ACTIVEMODEL_VALIDATIONS = 'activemodel_validations'.freeze
26
+ ].sort.freeze
45
27
  end
46
28
 
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(':'))
29
+ module Options
30
+ KEYS = [
31
+ DIFF = 'Diff'.freeze,
32
+ INIT = 'Initialize'.freeze,
33
+ ACCEPT = 'Accept'.freeze,
34
+ INIT_STRICT = 'InitializeStrict'.freeze,
35
+ ACCEPT_STRICT = 'AcceptStrict'.freeze,
36
+ KEYS_AS_SYMBOL = 'KeysAsSymbol'.freeze,
37
+ AM_VALIDATIONS = 'ActiveModelValidations'.freeze
38
+ ].sort.freeze
39
+
40
+ KEYS_TO_FEATURES = {
41
+ DIFF => Features::Diff,
42
+ INIT => Features::Initialize,
43
+ ACCEPT => Features::Accept,
44
+ INIT_STRICT => Features::Initialize::Strict,
45
+ ACCEPT_STRICT => Features::Accept::Strict,
46
+ KEYS_AS_SYMBOL => Features::KeysAsSymbol,
47
+ AM_VALIDATIONS => Features::ActiveModelValidations
48
+ }.freeze
49
+
50
+ NAMES_TO_KEYS = {
51
+ Name::DIFF => DIFF,
52
+ Name::ACCEPT => ACCEPT,
53
+ Name::INITIALIZE => INIT,
54
+ Name::KEYS_AS_SYMBOL => KEYS_AS_SYMBOL,
55
+ Name::ACTIVEMODEL_VALIDATIONS => AM_VALIDATIONS
56
+ }.freeze
57
+
58
+ INIT_INIT_STRICT = "#{INIT}_#{INIT_STRICT}".freeze
59
+ ACCEPT_ACCEPT_STRICT = "#{ACCEPT}_#{ACCEPT_STRICT}".freeze
60
+
61
+ BuildKey = -> combination do
62
+ combination.sort.join('_')
63
+ .sub(INIT_INIT_STRICT, INIT_STRICT)
64
+ .sub(ACCEPT_ACCEPT_STRICT, ACCEPT_STRICT)
52
65
  end
53
- end
54
66
 
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)
67
+ KEYS_TO_MODULES = begin
68
+ combinations = (1..KEYS.size).map { |n| KEYS.combination(n).to_a }.flatten(1).sort_by { |i| "#{i.size}#{i.join}" }
69
+ combinations.delete_if { |combination| combination.include?(INIT_STRICT) && !combination.include?(INIT) }
70
+ combinations.delete_if { |combination| combination.include?(ACCEPT_STRICT) && !combination.include?(ACCEPT) }
71
+ combinations.each_with_object({}) do |combination, features|
72
+ included = [
73
+ 'def self.included(base)',
74
+ ' base.send(:include, ::Micro::Attributes)',
75
+ combination.map { |key| " base.send(:include, ::#{KEYS_TO_FEATURES[key].name})" },
76
+ 'end'
77
+ ].flatten.join("\n")
78
+
79
+ key = BuildKey.call(combination)
80
+
81
+ With.const_set(key, Module.new.tap { |mod| mod.instance_eval(included) })
82
+
83
+ features[key] = With.const_get(key, false)
84
+ end.freeze
59
85
  end
60
- end
61
86
 
62
- private
87
+ ACTIVEMODEL_VALIDATION = 'activemodel_validation'.freeze
88
+
89
+ def self.fetch_key(arg)
90
+ if arg.is_a?(Hash)
91
+ return ACCEPT_STRICT if arg[:accept] == :strict
92
+
93
+ INIT_STRICT if arg[:initialize] == :strict
94
+ else
95
+ str = String(arg)
63
96
 
64
- def fetch_feature_name(name)
65
- return name unless name.is_a?(Hash)
97
+ name = str == ACTIVEMODEL_VALIDATION ? Name::ACTIVEMODEL_VALIDATIONS : str
66
98
 
67
- STRICT_INITIALIZE if name[:initialize] == :strict
99
+ KEYS_TO_MODULES.key?(name) ? name : NAMES_TO_KEYS[name]
100
+ end
68
101
  end
69
102
 
70
- def normalize_names(args)
71
- names = Array(args).dup
103
+ INVALID_NAME = [
104
+ 'Invalid feature name! Available options: ',
105
+ Name::ALL.map { |feature_name| ":#{feature_name}" }.join(', ')
106
+ ].join
107
+
108
+ def self.fetch_keys(args)
109
+ keys = Array(args).dup.map { |name| fetch_key(name) }
72
110
 
73
- last_feature = fetch_feature_name(names.pop)
111
+ raise ArgumentError, INVALID_NAME if keys.empty? || !(keys - KEYS).empty?
74
112
 
75
- features = names.empty? ? [last_feature] : names + [last_feature]
76
- features.map! { |name| name.to_s.downcase }
77
- features.uniq
113
+ yield(keys)
78
114
  end
79
115
 
80
- def valid_names?(names)
81
- names.all? { |name| ALL.include?(name) }
116
+ def self.remove_base_if_has_strict(keys)
117
+ keys.delete_if { |key| key == INIT } if keys.include?(INIT_STRICT)
118
+ keys.delete_if { |key| key == ACCEPT } if keys.include?(ACCEPT_STRICT)
82
119
  end
83
120
 
84
- def valid_names!(args)
85
- names = normalize_names(args)
121
+ def self.without_keys(keys_to_exclude)
122
+ keys = (KEYS - keys_to_exclude)
123
+ keys.delete_if { |key| key == INIT || key == INIT_STRICT } if keys_to_exclude.include?(INIT)
124
+ keys.delete_if { |key| key == ACCEPT || key == ACCEPT_STRICT } if keys_to_exclude.include?(ACCEPT)
125
+ keys
126
+ end
86
127
 
87
- raise ArgumentError, INVALID_NAME if names.empty? || !valid_names?(names)
128
+ def self.fetch_module_by_keys(combination)
129
+ key = BuildKey.call(combination)
88
130
 
89
- yield(names)
131
+ KEYS_TO_MODULES.fetch(key)
90
132
  end
91
133
 
92
- def an_initialize?(name)
93
- name == INITIALIZE || name == STRICT_INITIALIZE
94
- end
134
+ private_constant :KEYS_TO_FEATURES, :NAMES_TO_KEYS, :INVALID_NAME
135
+ private_constant :INIT_INIT_STRICT, :ACCEPT_ACCEPT_STRICT, :BuildKey
136
+ end
137
+
138
+ def all
139
+ @all ||= self.with(Options::KEYS)
140
+ end
95
141
 
96
- def delete_initialize_if_has_strict_initialize(names)
97
- return unless names.include?(STRICT_INITIALIZE)
142
+ def with(names)
143
+ Options.fetch_keys(names) do |keys|
144
+ Options.remove_base_if_has_strict(keys)
98
145
 
99
- names.delete_if { |name| name == INITIALIZE }
146
+ Options.fetch_module_by_keys(keys)
100
147
  end
148
+ end
101
149
 
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)
150
+ def without(names)
151
+ Options.fetch_keys(names) do |keys|
152
+ keys_to_fetch = Options.without_keys(keys)
105
153
 
106
- delete_initialize_if_has_strict_initialize(names)
107
- end
154
+ return ::Micro::Attributes if keys_to_fetch.empty?
155
+
156
+ Options.fetch_module_by_keys(keys_to_fetch)
108
157
  end
158
+ end
159
+
160
+ private_constant :Name
109
161
  end
162
+
110
163
  end
111
164
  end