u-attributes 2.2.0 → 2.7.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.sh +33 -19
- data/.travis.yml +12 -7
- data/Gemfile +15 -10
- data/README.md +192 -43
- data/lib/micro/attributes.rb +70 -20
- 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 +53 -9
- 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 +146 -27
- data/lib/micro/attributes/utils.rb +42 -22
- data/lib/micro/attributes/version.rb +1 -1
- data/test.sh +7 -3
- data/u-attributes.gemspec +5 -6
- metadata +14 -14
- data/assets/u-attributes_logo_v1.png +0 -0
- data/lib/micro/attributes/with.rb +0 -100
@@ -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, 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
|
@@ -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
|
|
@@ -22,11 +54,23 @@ module Micro::Attributes
|
|
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::Symbol[value]
|
14
|
+
end
|
15
|
+
|
16
|
+
def __attribute_key_transform__(value)
|
17
|
+
value
|
18
|
+
end
|
19
|
+
|
20
|
+
def __attributes_keys_transform__(hash)
|
21
|
+
Kind::Hash[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)
|
@@ -80,11 +191,19 @@ module Micro
|
|
80
191
|
if arg.is_a?(String) || arg.is_a?(Symbol)
|
81
192
|
__attribute_assign(arg, false, options)
|
82
193
|
else
|
83
|
-
|
194
|
+
Kind::KIND.error!('String/Symbol'.freeze, arg)
|
84
195
|
end
|
85
196
|
end
|
86
197
|
end
|
87
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
|
+
|
88
207
|
# NOTE: can't be renamed! It is used by u-case v4.
|
89
208
|
module ForSubclasses
|
90
209
|
WRONG_NUMBER_OF_ARGS = 'wrong number of arguments (given 0, expected 1 or more)'.freeze
|
@@ -96,7 +215,7 @@ module Micro
|
|
96
215
|
private_constant :WRONG_NUMBER_OF_ARGS
|
97
216
|
end
|
98
217
|
|
99
|
-
private_constant :ForSubclasses
|
218
|
+
private_constant :Options, :ForSubclasses
|
100
219
|
end
|
101
220
|
|
102
221
|
private_constant :Macros
|