u-attributes 2.2.0 → 2.7.0

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