u-attributes 2.2.0 → 2.7.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
data/lib/micro/attributes.rb
CHANGED
@@ -15,7 +15,8 @@ module Micro
|
|
15
15
|
|
16
16
|
base.class_eval do
|
17
17
|
private_class_method :__attributes, :__attribute_reader
|
18
|
-
private_class_method :__attribute_assign, :
|
18
|
+
private_class_method :__attribute_assign, :__attributes_groups
|
19
|
+
private_class_method :__attributes_required_add, :__attributes_data_to_assign
|
19
20
|
end
|
20
21
|
|
21
22
|
def base.inherited(subclass)
|
@@ -37,8 +38,8 @@ module Micro
|
|
37
38
|
Features.all
|
38
39
|
end
|
39
40
|
|
40
|
-
def attribute?(name)
|
41
|
-
self.class.attribute?(name)
|
41
|
+
def attribute?(name, include_all = false)
|
42
|
+
self.class.attribute?(name, include_all)
|
42
43
|
end
|
43
44
|
|
44
45
|
def attribute(name)
|
@@ -52,59 +53,108 @@ module Micro
|
|
52
53
|
def attribute!(name, &block)
|
53
54
|
attribute(name) { |name| return block ? block[name] : name }
|
54
55
|
|
55
|
-
raise NameError,
|
56
|
+
raise NameError, __attribute_access_error_message(name)
|
57
|
+
end
|
58
|
+
|
59
|
+
def defined_attributes(option = nil)
|
60
|
+
return self.class.attributes_by_visibility if option == :by_visibility
|
61
|
+
|
62
|
+
@defined_attributes ||= self.class.attributes
|
56
63
|
end
|
57
64
|
|
58
65
|
def attributes(*names)
|
59
66
|
return __attributes if names.empty?
|
60
67
|
|
61
|
-
names.
|
62
|
-
|
68
|
+
options = names.last.is_a?(Hash) ? names.pop : Kind::Empty::HASH
|
69
|
+
|
70
|
+
names.flatten!
|
71
|
+
|
72
|
+
without_option = Array(options.fetch(:without, Kind::Empty::ARRAY))
|
73
|
+
|
74
|
+
keys = names.empty? ? defined_attributes - without_option.map { |value| __attribute_key(value) } : names - without_option
|
75
|
+
|
76
|
+
data = keys.each_with_object({}) { |key, memo| memo[key] = attribute(key) if attribute?(key) }
|
77
|
+
|
78
|
+
with_option = Array(options.fetch(:with, Kind::Empty::ARRAY))
|
79
|
+
|
80
|
+
unless with_option.empty?
|
81
|
+
extra = with_option.each_with_object({}) { |key, memo| memo[__attribute_key(key)] = public_send(key) }
|
82
|
+
|
83
|
+
data.merge!(extra)
|
63
84
|
end
|
64
|
-
end
|
65
85
|
|
66
|
-
|
67
|
-
@defined_attributes ||= self.class.attributes
|
86
|
+
Utils::Hashes.keys_as(options[:keys_as], data)
|
68
87
|
end
|
69
88
|
|
70
89
|
protected
|
71
90
|
|
72
91
|
def attributes=(arg)
|
73
|
-
hash =
|
92
|
+
hash = self.class.__attributes_keys_transform__(arg)
|
74
93
|
|
75
94
|
__attributes_missing!(hash)
|
76
95
|
|
96
|
+
__call_before_attributes_assign
|
77
97
|
__attributes_assign(hash)
|
98
|
+
__call_after_attributes_assign
|
99
|
+
|
100
|
+
__attributes
|
78
101
|
end
|
79
102
|
|
80
103
|
private
|
81
104
|
|
105
|
+
def __call_before_attributes_assign; end
|
106
|
+
def __call_after_attributes_assign; end
|
107
|
+
|
82
108
|
def extract_attributes_from(other)
|
83
109
|
Utils::ExtractAttribute.from(other, keys: defined_attributes)
|
84
110
|
end
|
85
111
|
|
112
|
+
def __attribute_access_error_message(name)
|
113
|
+
return "tried to access a private attribute `#{name}" if attribute?(name, true)
|
114
|
+
|
115
|
+
"undefined attribute `#{name}"
|
116
|
+
end
|
117
|
+
|
118
|
+
def __attribute_key(value)
|
119
|
+
self.class.__attribute_key_transform__(value)
|
120
|
+
end
|
121
|
+
|
86
122
|
def __attributes
|
87
123
|
@__attributes ||= {}
|
88
124
|
end
|
89
125
|
|
90
|
-
FetchValueToAssign = -> (value,
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
126
|
+
FetchValueToAssign = -> (value, attribute_data, keep_proc = false) do
|
127
|
+
default = attribute_data[0]
|
128
|
+
|
129
|
+
value_to_assign =
|
130
|
+
if default.is_a?(Proc) && !keep_proc
|
131
|
+
default.arity > 0 ? default.call(value) : default.call
|
132
|
+
else
|
133
|
+
value.nil? ? default : value
|
134
|
+
end
|
135
|
+
|
136
|
+
return value_to_assign unless to_freeze = attribute_data[2]
|
137
|
+
return value_to_assign.freeze if to_freeze == true
|
138
|
+
return value_to_assign.dup.freeze if to_freeze == :after_dup
|
139
|
+
return value_to_assign.clone.freeze if to_freeze == :after_clone
|
140
|
+
|
141
|
+
raise NotImplementedError
|
96
142
|
end
|
97
143
|
|
98
144
|
def __attributes_assign(hash)
|
99
|
-
self.class.__attributes_data__.each do |name,
|
100
|
-
__attribute_assign(name,
|
145
|
+
self.class.__attributes_data__.each do |name, attribute_data|
|
146
|
+
__attribute_assign(name, hash[name], attribute_data) if attribute?(name, true)
|
101
147
|
end
|
102
148
|
|
103
149
|
__attributes.freeze
|
104
150
|
end
|
105
151
|
|
106
|
-
def __attribute_assign(name,
|
107
|
-
|
152
|
+
def __attribute_assign(name, initialize_value, attribute_data)
|
153
|
+
value_to_assign = FetchValueToAssign.(initialize_value, attribute_data)
|
154
|
+
|
155
|
+
ivar_value = instance_variable_set("@#{name}", value_to_assign)
|
156
|
+
|
157
|
+
__attributes[name] = ivar_value if attribute_data[3] == :public
|
108
158
|
end
|
109
159
|
|
110
160
|
MISSING_KEYWORD = 'missing keyword'.freeze
|
@@ -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
|