u-attributes 2.1.1 → 2.6.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/README.md +180 -41
- data/lib/micro/attributes.rb +69 -29
- 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 +150 -27
- 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,13 +3,45 @@
|
|
3
3
|
module Micro::Attributes
|
4
4
|
module Features
|
5
5
|
module ActiveModelValidations
|
6
|
-
|
7
|
-
|
8
|
-
|
6
|
+
module Standard
|
7
|
+
private def __call_after_attributes_assign
|
8
|
+
run_validations!
|
9
|
+
end
|
10
|
+
end
|
9
11
|
|
10
|
-
|
11
|
-
|
12
|
-
|
12
|
+
module CheckActivemodelValidationErrors
|
13
|
+
private def __check_activemodel_validation_errors
|
14
|
+
return if errors.blank?
|
15
|
+
|
16
|
+
errors_hash = errors.to_hash
|
17
|
+
|
18
|
+
defined_attributes.each do |key|
|
19
|
+
value = Utils::Hashes.assoc(errors_hash, key)
|
20
|
+
|
21
|
+
@__attributes_errors[key] = value.join(', ') if value.present?
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
module WithAccept
|
27
|
+
include CheckActivemodelValidationErrors
|
28
|
+
|
29
|
+
private def __call_after_attributes_assign
|
30
|
+
run_validations! unless attributes_errors?
|
31
|
+
|
32
|
+
__check_activemodel_validation_errors
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
module WithAcceptStrict
|
37
|
+
include CheckActivemodelValidationErrors
|
38
|
+
|
39
|
+
private def __call_after_attributes_assign
|
40
|
+
__raise_error_if_found_attributes_errors if attributes_errors?
|
41
|
+
|
42
|
+
run_validations!
|
43
|
+
|
44
|
+
__check_activemodel_validation_errors
|
13
45
|
end
|
14
46
|
end
|
15
47
|
|
@@ -18,15 +50,27 @@ module Micro::Attributes
|
|
18
50
|
validate, validates = options.values_at(:validate, :validates)
|
19
51
|
|
20
52
|
self.validate(validate) if validate
|
21
|
-
self.validates(attr_name, validates) if validates
|
53
|
+
self.validates(attr_name, validates.dup) if validates
|
22
54
|
end
|
23
55
|
end
|
24
56
|
|
25
|
-
|
57
|
+
def self.included(base)
|
58
|
+
begin
|
59
|
+
require 'active_model'
|
60
|
+
|
61
|
+
base.send(:include, ::ActiveModel::Validations)
|
62
|
+
base.extend(ClassMethods)
|
26
63
|
|
27
|
-
|
28
|
-
|
64
|
+
case
|
65
|
+
when base <= Features::Accept::Strict then base.send(:include, WithAcceptStrict)
|
66
|
+
when base <= Features::Accept then base.send(:include, WithAccept)
|
67
|
+
else base.send(:include, Standard)
|
68
|
+
end
|
69
|
+
rescue LoadError
|
29
70
|
end
|
71
|
+
end
|
72
|
+
|
73
|
+
private_constant :Standard, :CheckActivemodelValidationErrors, :WithAccept, :WithAcceptStrict
|
30
74
|
end
|
31
75
|
end
|
32
76
|
end
|
@@ -7,22 +7,17 @@ module Micro::Attributes
|
|
7
7
|
base.class_eval(<<-RUBY)
|
8
8
|
def initialize(arg)
|
9
9
|
self.attributes = arg
|
10
|
-
__call_after_micro_attribute
|
11
10
|
end
|
12
11
|
RUBY
|
13
12
|
end
|
14
13
|
|
15
|
-
def with_attribute(key, val)
|
16
|
-
self.class.new(attributes.merge(key => val))
|
17
|
-
end
|
18
|
-
|
19
14
|
def with_attributes(arg)
|
20
15
|
self.class.new(attributes.merge(arg))
|
21
16
|
end
|
22
17
|
|
23
|
-
|
24
|
-
|
25
|
-
|
18
|
+
def with_attribute(key, val)
|
19
|
+
with_attributes(key => val)
|
20
|
+
end
|
26
21
|
end
|
27
22
|
end
|
28
23
|
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_key_check__(value)
|
13
|
+
Kind::Of.(::Symbol, value)
|
14
|
+
end
|
15
|
+
|
16
|
+
def __attribute_key_transform__(value)
|
17
|
+
value
|
18
|
+
end
|
19
|
+
|
20
|
+
def __attributes_keys_transform__(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
|
@@ -3,65 +3,176 @@
|
|
3
3
|
module Micro
|
4
4
|
module Attributes
|
5
5
|
module Macros
|
6
|
+
module Options
|
7
|
+
PERMITTED = [
|
8
|
+
:default, :required, :freeze, :protected, :private, # for all
|
9
|
+
:validate, :validates, # for ext: activemodel_validations
|
10
|
+
:accept, :reject, :allow_nil, :rejection_message # for ext: accept
|
11
|
+
].freeze
|
12
|
+
|
13
|
+
INVALID_MESSAGE = [
|
14
|
+
"Found one or more invalid options: %{invalid_options}\n\nThe valid ones are: ",
|
15
|
+
PERMITTED.map { |key| ":#{key}" }.join(', ')
|
16
|
+
].join.freeze
|
17
|
+
|
18
|
+
def self.check(opt)
|
19
|
+
invalid_keys = opt.keys - PERMITTED
|
20
|
+
|
21
|
+
return if invalid_keys.empty?
|
22
|
+
|
23
|
+
invalid_options = { invalid_options: invalid_keys.inspect.tr('[', '').tr(']', '') }
|
24
|
+
|
25
|
+
raise ArgumentError, (INVALID_MESSAGE % invalid_options)
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.for_accept(opt)
|
29
|
+
allow_nil = opt[:allow_nil]
|
30
|
+
rejection_message = opt[:rejection_message]
|
31
|
+
|
32
|
+
return [:accept, opt[:accept], allow_nil, rejection_message] if opt.key?(:accept)
|
33
|
+
return [:reject, opt[:reject], allow_nil, rejection_message] if opt.key?(:reject)
|
34
|
+
|
35
|
+
Kind::Empty::ARRAY
|
36
|
+
end
|
37
|
+
|
38
|
+
ALL = 0
|
39
|
+
PUBLIC = 1
|
40
|
+
PRIVATE = 2
|
41
|
+
PROTECTED = 3
|
42
|
+
REQUIRED = 4
|
43
|
+
|
44
|
+
def self.visibility_index(opt)
|
45
|
+
return PRIVATE if opt[:private]
|
46
|
+
return PROTECTED if opt[:protected]
|
47
|
+
PUBLIC
|
48
|
+
end
|
49
|
+
|
50
|
+
VISIBILITY_NAMES = { PUBLIC => :public, PRIVATE => :private, PROTECTED => :protected }.freeze
|
51
|
+
|
52
|
+
def self.visibility_name_from_index(visibility_index)
|
53
|
+
VISIBILITY_NAMES[visibility_index]
|
54
|
+
end
|
55
|
+
|
56
|
+
def self.private?(visibility); visibility == PRIVATE; end
|
57
|
+
def self.protected?(visibility); visibility == PROTECTED; end
|
58
|
+
end
|
59
|
+
|
6
60
|
def attributes_are_all_required?
|
7
61
|
false
|
8
62
|
end
|
9
63
|
|
64
|
+
def attributes_access
|
65
|
+
:indifferent
|
66
|
+
end
|
67
|
+
|
68
|
+
def __attributes_groups
|
69
|
+
@__attributes_groups ||= [
|
70
|
+
Set.new, # all
|
71
|
+
Set.new, # public
|
72
|
+
[], # private
|
73
|
+
[], # protected
|
74
|
+
Set.new, # required
|
75
|
+
]
|
76
|
+
end
|
77
|
+
|
78
|
+
def __attributes; __attributes_groups[Options::ALL]; end
|
79
|
+
|
80
|
+
def __attributes_public; __attributes_groups[Options::PUBLIC]; end
|
81
|
+
|
82
|
+
def __attributes_required__; __attributes_groups[Options::REQUIRED]; end
|
83
|
+
|
84
|
+
def __attribute_key_check__(value)
|
85
|
+
value
|
86
|
+
end
|
87
|
+
|
88
|
+
def __attribute_key_transform__(value)
|
89
|
+
value.to_s
|
90
|
+
end
|
91
|
+
|
92
|
+
def __attributes_keys_transform__(hash)
|
93
|
+
Utils::Hashes.stringify_keys(hash)
|
94
|
+
end
|
95
|
+
|
10
96
|
# NOTE: can't be renamed! It is used by u-case v4.
|
11
97
|
def __attributes_data__
|
12
98
|
@__attributes_data__ ||= {}
|
13
99
|
end
|
14
100
|
|
15
|
-
def
|
16
|
-
|
101
|
+
def __attribute_reader(name, visibility_index)
|
102
|
+
attr_reader(name)
|
103
|
+
|
104
|
+
__attributes.add(name)
|
105
|
+
__attributes_groups[visibility_index] << name
|
106
|
+
|
107
|
+
private(name) if Options.private?(visibility_index)
|
108
|
+
protected(name) if Options.protected?(visibility_index)
|
17
109
|
end
|
18
110
|
|
19
|
-
def __attributes_required_add(name,
|
20
|
-
if
|
111
|
+
def __attributes_required_add(name, opt, hasnt_default)
|
112
|
+
if opt[:required] || (attributes_are_all_required? && hasnt_default)
|
21
113
|
__attributes_required__.add(name)
|
22
114
|
end
|
23
115
|
|
24
116
|
nil
|
25
117
|
end
|
26
118
|
|
27
|
-
def __attributes_data_to_assign(name,
|
28
|
-
hasnt_default = !
|
119
|
+
def __attributes_data_to_assign(name, opt, visibility_index)
|
120
|
+
hasnt_default = !opt.key?(:default)
|
29
121
|
|
30
|
-
hasnt_default ? __attributes_required_add(name,
|
31
|
-
end
|
122
|
+
default = hasnt_default ? __attributes_required_add(name, opt, hasnt_default) : opt[:default]
|
32
123
|
|
33
|
-
|
34
|
-
|
124
|
+
[
|
125
|
+
default,
|
126
|
+
Options.for_accept(opt),
|
127
|
+
opt[:freeze],
|
128
|
+
Options.visibility_name_from_index(visibility_index)
|
129
|
+
]
|
35
130
|
end
|
36
131
|
|
37
|
-
def
|
38
|
-
__attributes.add(name)
|
132
|
+
def __call_after_attribute_assign__(attr_name, options); end
|
39
133
|
|
40
|
-
|
41
|
-
|
134
|
+
def __attribute_assign(key, can_overwrite, opt)
|
135
|
+
name = __attribute_key_check__(__attribute_key_transform__(key))
|
42
136
|
|
43
|
-
|
44
|
-
name = key.to_s
|
45
|
-
has_attribute = attribute?(name)
|
137
|
+
Options.check(opt)
|
46
138
|
|
47
|
-
|
139
|
+
has_attribute = attribute?(name, true)
|
48
140
|
|
49
|
-
|
141
|
+
visibility_index = Options.visibility_index(opt)
|
50
142
|
|
51
|
-
|
52
|
-
end
|
143
|
+
__attribute_reader(name, visibility_index) unless has_attribute
|
53
144
|
|
54
|
-
|
145
|
+
if can_overwrite || !has_attribute
|
146
|
+
__attributes_data__[name] = __attributes_data_to_assign(name, opt, visibility_index)
|
147
|
+
end
|
148
|
+
|
149
|
+
__call_after_attribute_assign__(name, opt)
|
150
|
+
end
|
55
151
|
|
56
152
|
# NOTE: can't be renamed! It is used by u-case v4.
|
57
153
|
def __attributes_set_after_inherit__(arg)
|
58
154
|
arg.each do |key, val|
|
59
|
-
|
155
|
+
opt = {}
|
156
|
+
|
157
|
+
default = val[0]
|
158
|
+
accept_key, accept_val = val[1]
|
159
|
+
freeze, visibility = val[2], val[3]
|
160
|
+
|
161
|
+
opt[:default] = default if default
|
162
|
+
opt[accept_key] = accept_val if accept_key
|
163
|
+
opt[:freeze] = freeze if freeze
|
164
|
+
opt[visibility] = true if visibility != :public
|
165
|
+
|
166
|
+
__attribute_assign(key, true, opt || Kind::Empty::HASH)
|
60
167
|
end
|
61
168
|
end
|
62
169
|
|
63
|
-
def attribute?(name)
|
64
|
-
|
170
|
+
def attribute?(name, include_all = false)
|
171
|
+
key = __attribute_key_transform__(name)
|
172
|
+
|
173
|
+
return __attributes.member?(key) if include_all
|
174
|
+
|
175
|
+
__attributes_public.member?(key)
|
65
176
|
end
|
66
177
|
|
67
178
|
def attribute(name, options = Kind::Empty::HASH)
|
@@ -72,15 +183,27 @@ module Micro
|
|
72
183
|
return __attributes.to_a if args.empty?
|
73
184
|
|
74
185
|
args.flatten!
|
186
|
+
|
187
|
+
options =
|
188
|
+
args.size > 1 && args.last.is_a?(::Hash) ? args.pop : Kind::Empty::HASH
|
189
|
+
|
75
190
|
args.each do |arg|
|
76
191
|
if arg.is_a?(String) || arg.is_a?(Symbol)
|
77
|
-
__attribute_assign(arg, false,
|
192
|
+
__attribute_assign(arg, false, options)
|
78
193
|
else
|
79
194
|
raise Kind::Error.new('String/Symbol'.freeze, arg)
|
80
195
|
end
|
81
196
|
end
|
82
197
|
end
|
83
198
|
|
199
|
+
def attributes_by_visibility
|
200
|
+
{
|
201
|
+
public: __attributes_groups[Options::PUBLIC].to_a,
|
202
|
+
private: __attributes_groups[Options::PRIVATE].dup,
|
203
|
+
protected: __attributes_groups[Options::PROTECTED].dup
|
204
|
+
}
|
205
|
+
end
|
206
|
+
|
84
207
|
# NOTE: can't be renamed! It is used by u-case v4.
|
85
208
|
module ForSubclasses
|
86
209
|
WRONG_NUMBER_OF_ARGS = 'wrong number of arguments (given 0, expected 1 or more)'.freeze
|
@@ -92,7 +215,7 @@ module Micro
|
|
92
215
|
private_constant :WRONG_NUMBER_OF_ARGS
|
93
216
|
end
|
94
217
|
|
95
|
-
private_constant :ForSubclasses
|
218
|
+
private_constant :Options, :ForSubclasses
|
96
219
|
end
|
97
220
|
|
98
221
|
private_constant :Macros
|
@@ -1,25 +1,58 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
module Micro
|
4
|
-
module
|
5
|
-
module
|
6
|
-
def self.
|
7
|
-
|
3
|
+
module Micro::Attributes
|
4
|
+
module Utils
|
5
|
+
module Hashes
|
6
|
+
def self.kind(hash)
|
7
|
+
Kind::Of.(::Hash, hash)
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.stringify_keys(arg)
|
11
|
+
hash = kind(arg)
|
8
12
|
|
9
13
|
return hash if hash.empty?
|
14
|
+
return hash.transform_keys(&:to_s) if hash.respond_to?(:transform_keys)
|
10
15
|
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
+
hash.each_with_object({}) { |(key, val), memo| memo[key.to_s] = val }
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.symbolize_keys(arg)
|
20
|
+
hash = kind(arg)
|
21
|
+
|
22
|
+
return hash if hash.empty?
|
23
|
+
return hash.transform_keys(&:to_sym) if hash.respond_to?(:transform_keys)
|
24
|
+
|
25
|
+
hash.each_with_object({}) { |(key, val), memo| memo[key.to_sym] = val }
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.keys_as(type, hash)
|
29
|
+
return kind(hash) unless type
|
30
|
+
|
31
|
+
return symbolize_keys(hash) if type == Symbol || type == :symbol
|
32
|
+
return stringify_keys(hash) if type == String || type == :string
|
33
|
+
|
34
|
+
raise ArgumentError, 'argument must be one of these values: :symbol, :string, Symbol, String'.freeze
|
35
|
+
end
|
36
|
+
|
37
|
+
def self.assoc(hash, key)
|
38
|
+
value = hash[key.to_s]
|
39
|
+
|
40
|
+
value.nil? ? hash[key.to_sym] : value
|
16
41
|
end
|
42
|
+
end
|
43
|
+
|
44
|
+
module ExtractAttribute
|
45
|
+
def self.call(object, key:)
|
46
|
+
return object.public_send(key) if object.respond_to?(key)
|
17
47
|
|
18
|
-
|
19
|
-
|
48
|
+
Hashes.assoc(object, key) if object.respond_to?(:[])
|
49
|
+
end
|
20
50
|
|
21
|
-
|
22
|
-
|
51
|
+
def self.from(object, keys:)
|
52
|
+
Kind::Of.(::Array, keys).each_with_object({}) do |key, memo|
|
53
|
+
memo[key] = call(object, key: key)
|
54
|
+
end
|
55
|
+
end
|
23
56
|
end
|
24
57
|
end
|
25
58
|
end
|