u-attributes 2.0.1 → 2.4.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/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
|