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.
- checksums.yaml +4 -4
- data/.travis.yml +1 -2
- data/README.md +180 -41
- data/lib/micro/attributes.rb +43 -23
- data/lib/micro/attributes/diff.rb +26 -14
- data/lib/micro/attributes/features.rb +128 -75
- data/lib/micro/attributes/features/accept.rb +132 -0
- data/lib/micro/attributes/features/accept/strict.rb +26 -0
- data/lib/micro/attributes/features/activemodel_validations.rb +54 -10
- data/lib/micro/attributes/features/initialize.rb +3 -8
- data/lib/micro/attributes/features/keys_as_symbol.rb +31 -0
- data/lib/micro/attributes/macros.rb +80 -13
- data/lib/micro/attributes/utils.rb +47 -14
- data/lib/micro/attributes/version.rb +1 -1
- data/u-attributes.gemspec +1 -1
- metadata +5 -4
- data/assets/u-attributes_logo_v1.png +0 -0
- data/lib/micro/attributes/with.rb +0 -100
@@ -3,15 +3,20 @@
|
|
3
3
|
module Micro::Attributes
|
4
4
|
module Diff
|
5
5
|
class Changes
|
6
|
-
|
7
|
-
|
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
|
-
|
14
|
-
|
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
|
38
|
+
differences.has_key?(key_transform(name))
|
33
39
|
else
|
34
|
-
result = @differences[name
|
35
|
-
result ? result[
|
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
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
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 :
|
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
|
-
|
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
|
-
|
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
|
-
|
44
|
-
|
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
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
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
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
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
|
-
|
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
|
-
|
65
|
-
return name unless name.is_a?(Hash)
|
97
|
+
name = str == ACTIVEMODEL_VALIDATION ? Name::ACTIVEMODEL_VALIDATIONS : str
|
66
98
|
|
67
|
-
|
99
|
+
KEYS_TO_MODULES.key?(name) ? name : NAMES_TO_KEYS[name]
|
100
|
+
end
|
68
101
|
end
|
69
102
|
|
70
|
-
|
71
|
-
|
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
|
-
|
111
|
+
raise ArgumentError, INVALID_NAME if keys.empty? || !(keys - KEYS).empty?
|
74
112
|
|
75
|
-
|
76
|
-
features.map! { |name| name.to_s.downcase }
|
77
|
-
features.uniq
|
113
|
+
yield(keys)
|
78
114
|
end
|
79
115
|
|
80
|
-
def
|
81
|
-
|
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
|
85
|
-
|
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
|
-
|
128
|
+
def self.fetch_module_by_keys(combination)
|
129
|
+
key = BuildKey.call(combination)
|
88
130
|
|
89
|
-
|
131
|
+
KEYS_TO_MODULES.fetch(key)
|
90
132
|
end
|
91
133
|
|
92
|
-
|
93
|
-
|
94
|
-
|
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
|
-
|
97
|
-
|
142
|
+
def with(names)
|
143
|
+
Options.fetch_keys(names) do |keys|
|
144
|
+
Options.remove_base_if_has_strict(keys)
|
98
145
|
|
99
|
-
|
146
|
+
Options.fetch_module_by_keys(keys)
|
100
147
|
end
|
148
|
+
end
|
101
149
|
|
102
|
-
|
103
|
-
|
104
|
-
|
150
|
+
def without(names)
|
151
|
+
Options.fetch_keys(names) do |keys|
|
152
|
+
keys_to_fetch = Options.without_keys(keys)
|
105
153
|
|
106
|
-
|
107
|
-
|
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
|