u-attributes 2.1.0 → 2.5.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -0,0 +1,132 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Micro::Attributes
4
+ module Features
5
+ module Accept
6
+ def attributes_errors
7
+ @__attributes_errors
8
+ end
9
+
10
+ def attributes_errors?
11
+ !@__attributes_errors.empty?
12
+ end
13
+
14
+ def rejected_attributes
15
+ @__rejected_attributes ||= attributes_errors.keys
16
+ end
17
+
18
+ def accepted_attributes
19
+ @__accepted_attributes ||= defined_attributes - rejected_attributes
20
+ end
21
+
22
+ def rejected_attributes?
23
+ attributes_errors?
24
+ end
25
+
26
+ def accepted_attributes?
27
+ !rejected_attributes?
28
+ end
29
+
30
+ private
31
+
32
+ def __call_before_attributes_assign
33
+ @__attributes_errors = {}
34
+ end
35
+
36
+ KeepProc = -> validation_data { validation_data[0] == :accept && validation_data[1] == Proc }
37
+
38
+ def __attribute_assign(key, initialize_value, attribute_data)
39
+ validation_data = attribute_data[1]
40
+
41
+ value_to_assign = FetchValueToAssign.(initialize_value, attribute_data[0], KeepProc.(validation_data))
42
+
43
+ value = __attributes[key] = instance_variable_set("@#{key}", value_to_assign)
44
+
45
+ __attribute_accept_or_reject(key, value, validation_data) if !validation_data.empty?
46
+ end
47
+
48
+ def __attribute_accept_or_reject(key, value, validation_data)
49
+ context = Context.with(key, value, validation_data)
50
+
51
+ error_msg = context.rejection_message(Validate.call(context))
52
+
53
+ @__attributes_errors[key] = error_msg if error_msg
54
+ end
55
+
56
+ Context = Struct.new(:key, :value, :validation, :expected, :allow_nil, :rejection) do
57
+ def self.with(key, value, data)
58
+ new(key, value, data[0], data[1], data[2], data[3])
59
+ end
60
+
61
+ def allow_nil?
62
+ allow_nil && value.nil?
63
+ end
64
+
65
+ def accept?
66
+ validation == :accept
67
+ end
68
+
69
+ def rejection_message(default_msg)
70
+ return unless default_msg
71
+
72
+ return default_msg unless rejection || expected.respond_to?(:rejection_message)
73
+
74
+ rejection_msg = rejection || expected.rejection_message
75
+
76
+ return rejection_msg unless rejection_msg.is_a?(Proc)
77
+
78
+ rejection_msg.arity == 0 ? rejection_msg.call : rejection_msg.call(key)
79
+ end
80
+ end
81
+
82
+ module Validate
83
+ module Callable
84
+ MESSAGE = 'is invalid'.freeze
85
+
86
+ def self.call?(exp); exp.respond_to?(:call); end
87
+ def self.call(exp, val); exp.call(val); end
88
+ def self.accept_failed(_exp); MESSAGE; end
89
+ def self.reject_failed(_exp); MESSAGE; end
90
+ end
91
+
92
+ module KindOf
93
+ def self.call?(exp); exp.is_a?(Class) || exp.is_a?(Module); end
94
+ def self.call(exp, val); val.kind_of?(exp); end
95
+ def self.accept_failed(exp); "expected to be a kind of #{exp}"; end
96
+ def self.reject_failed(exp); "expected to not be a kind of #{exp}"; end
97
+ end
98
+
99
+ module Predicate
100
+ QUESTION_MARK = '?'.freeze
101
+
102
+ def self.call?(exp); exp.is_a?(Symbol) && exp.to_s.end_with?(QUESTION_MARK); end
103
+ def self.call(exp, val); val.public_send(exp); end
104
+ def self.accept_failed(exp); "expected to be #{exp}"; end
105
+ def self.reject_failed(exp); "expected to not be #{exp}"; end
106
+ end
107
+
108
+ def self.with(expected)
109
+ return Callable if Callable.call?(expected)
110
+ return KindOf if KindOf.call?(expected)
111
+ return Predicate if Predicate.call?(expected)
112
+ end
113
+
114
+ def self.call(context)
115
+ return if context.allow_nil?
116
+
117
+ validate = self.with(expected = context.expected)
118
+
119
+ return unless validate
120
+
121
+ truthy = validate.call(expected, context.value)
122
+
123
+ return truthy ? nil : validate.accept_failed(expected) if context.accept?
124
+
125
+ validate.reject_failed(expected) if truthy
126
+ end
127
+ end
128
+
129
+ private_constant :KeepProc, :Context, :Validate
130
+ end
131
+ end
132
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Micro::Attributes
4
+ module Features
5
+ module Accept
6
+
7
+ module Strict
8
+ ATTRIBUTES_REJECTED = "One or more attributes were rejected. Errors:\n".freeze
9
+
10
+ def __call_after_attributes_assign
11
+ return unless attributes_errors?
12
+
13
+ __raise_error_if_found_attributes_errors
14
+ end
15
+
16
+ def __raise_error_if_found_attributes_errors
17
+ raise ArgumentError, [
18
+ ATTRIBUTES_REJECTED,
19
+ attributes_errors.map { |key, msg| "* #{key.inspect} #{msg}" }.join("\n")
20
+ ].join
21
+ end
22
+ end
23
+
24
+ end
25
+ end
26
+ end