u-attributes 2.0.1 → 2.4.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.
- checksums.yaml +4 -4
- data/.travis.yml +1 -2
- data/Gemfile +10 -1
- data/README.md +274 -47
- data/lib/micro/attributes.rb +69 -20
- data/lib/micro/attributes/diff.rb +26 -14
- data/lib/micro/attributes/features.rb +72 -77
- data/lib/micro/attributes/features/activemodel_validations.rb +2 -2
- data/lib/micro/attributes/features/initialize/strict.rb +6 -26
- data/lib/micro/attributes/features/keys_as_symbol.rb +31 -0
- data/lib/micro/attributes/macros.rb +58 -10
- data/lib/micro/attributes/utils.rb +48 -9
- data/lib/micro/attributes/version.rb +1 -1
- data/lib/micro/attributes/with.rb +107 -25
- data/u-attributes.gemspec +1 -1
- metadata +3 -2
data/lib/micro/attributes.rb
CHANGED
@@ -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, :
|
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.
|
61
|
-
|
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 =
|
89
|
+
hash = self.class.__attributes_keys__(arg)
|
69
90
|
|
70
|
-
|
91
|
+
__attributes_missing!(hash)
|
92
|
+
|
93
|
+
__attributes_assign(hash)
|
71
94
|
end
|
72
95
|
|
73
96
|
private
|
74
97
|
|
75
|
-
def
|
76
|
-
|
98
|
+
def extract_attributes_from(other)
|
99
|
+
Utils::ExtractAttribute.from(other, keys: defined_attributes)
|
77
100
|
end
|
78
101
|
|
79
|
-
def
|
80
|
-
|
102
|
+
def __attribute_key(value)
|
103
|
+
self.class.__attribute_key__(value)
|
81
104
|
end
|
82
105
|
|
83
|
-
def
|
84
|
-
|
85
|
-
|
106
|
+
def __attributes
|
107
|
+
@__attributes ||= {}
|
108
|
+
end
|
86
109
|
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
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
|
-
|
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
|
-
|
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_access(name))
|
33
39
|
else
|
34
|
-
result = @differences[name
|
35
|
-
result ? result[
|
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
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
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 :
|
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
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
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
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
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
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
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
|
-
|
60
|
+
def self.fetch_keys(args)
|
61
|
+
keys = Array(args).dup.map { |name| fetch_key(name) }
|
63
62
|
|
64
|
-
|
65
|
-
return name unless name.is_a?(Hash)
|
63
|
+
raise ArgumentError, INVALID_NAME if keys.empty? || !(keys - KEYS).empty?
|
66
64
|
|
67
|
-
|
65
|
+
yield(keys)
|
68
66
|
end
|
69
67
|
|
70
|
-
def
|
71
|
-
|
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
|
81
|
-
|
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
|
85
|
-
|
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
|
-
|
90
|
-
end
|
81
|
+
option = keys.sort.join('_')
|
91
82
|
|
92
|
-
|
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
|
-
|
97
|
-
|
87
|
+
def all
|
88
|
+
@all ||= self.with(Options::KEYS)
|
89
|
+
end
|
98
90
|
|
99
|
-
|
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
|
-
|
103
|
-
|
104
|
-
|
97
|
+
def without(names)
|
98
|
+
Options.fetch_keys(names) do |keys|
|
99
|
+
keys = Options.without_keys(keys)
|
105
100
|
|
106
|
-
|
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
|
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
|
-
|
8
|
-
|
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
|
-
|
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
|