u-attributes 1.1.0 → 2.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/.travis.sh +9 -2
- data/.travis.yml +4 -3
- data/Gemfile +11 -2
- data/LICENSE.txt +2 -2
- data/README.md +427 -282
- data/Rakefile +5 -5
- data/assets/u-attributes_logo_v1.png +0 -0
- data/bin/console +4 -4
- data/lib/micro/attributes.rb +95 -42
- data/lib/micro/attributes/diff.rb +52 -0
- data/lib/micro/attributes/features.rb +45 -37
- data/lib/micro/attributes/features/activemodel_validations.rb +17 -23
- data/lib/micro/attributes/features/diff.rb +2 -50
- data/lib/micro/attributes/features/initialize.rb +5 -0
- data/lib/micro/attributes/features/initialize/strict.rb +19 -0
- data/lib/micro/attributes/macros.rb +53 -23
- data/lib/micro/attributes/utils.rb +25 -0
- data/lib/micro/attributes/version.rb +1 -1
- data/lib/micro/attributes/with.rb +5 -5
- data/u-attributes.gemspec +19 -12
- metadata +48 -11
- data/Gemfile.lock +0 -29
- data/lib/micro/attributes/attributes_utils.rb +0 -21
- data/lib/micro/attributes/features/strict_initialize.rb +0 -39
data/Rakefile
CHANGED
@@ -1,10 +1,10 @@
|
|
1
|
-
require
|
2
|
-
require
|
1
|
+
require 'bundler/gem_tasks'
|
2
|
+
require 'rake/testtask'
|
3
3
|
|
4
4
|
Rake::TestTask.new(:test) do |t|
|
5
|
-
t.libs <<
|
6
|
-
t.libs <<
|
7
|
-
t.test_files = FileList[
|
5
|
+
t.libs << 'test'
|
6
|
+
t.libs << 'lib'
|
7
|
+
t.test_files = FileList['test/**/*_test.rb']
|
8
8
|
end
|
9
9
|
|
10
10
|
task :default => :test
|
Binary file
|
data/bin/console
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
2
|
|
3
|
-
require
|
4
|
-
require
|
3
|
+
require 'bundler/setup'
|
4
|
+
require 'micro/attributes'
|
5
5
|
|
6
6
|
# You can add fixtures and/or initialization code here to make experimenting
|
7
7
|
# with your gem easier. You can also use a different console, if you like.
|
8
8
|
|
9
9
|
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
-
# require
|
10
|
+
# require 'pry'
|
11
11
|
# Pry.start
|
12
12
|
|
13
|
-
require
|
13
|
+
require 'irb'
|
14
14
|
IRB.start(__FILE__)
|
data/lib/micro/attributes.rb
CHANGED
@@ -1,35 +1,30 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require
|
4
|
-
require "micro/attributes/attributes_utils"
|
5
|
-
require "micro/attributes/macros"
|
6
|
-
require "micro/attributes/features"
|
3
|
+
require 'kind'
|
7
4
|
|
8
5
|
module Micro
|
9
6
|
module Attributes
|
7
|
+
require 'micro/attributes/version'
|
8
|
+
require 'micro/attributes/utils'
|
9
|
+
require 'micro/attributes/diff'
|
10
|
+
require 'micro/attributes/macros'
|
11
|
+
require 'micro/attributes/features'
|
12
|
+
|
10
13
|
def self.included(base)
|
11
14
|
base.extend(::Micro::Attributes.const_get(:Macros))
|
12
15
|
|
13
16
|
base.class_eval do
|
14
|
-
private_class_method :
|
15
|
-
private_class_method :
|
16
|
-
private_class_method :__attribute_reader, :__attribute_set
|
17
|
+
private_class_method :__attributes, :__attribute_reader
|
18
|
+
private_class_method :__attribute_assign, :__attributes_data_to_assign
|
17
19
|
end
|
18
20
|
|
19
21
|
def base.inherited(subclass)
|
20
|
-
subclass.
|
22
|
+
subclass.__attributes_set_after_inherit__(self.__attributes_data__)
|
23
|
+
|
21
24
|
subclass.extend ::Micro::Attributes.const_get('Macros::ForSubclasses'.freeze)
|
22
25
|
end
|
23
26
|
end
|
24
27
|
|
25
|
-
def self.to_initialize(diff: false, activemodel_validations: false)
|
26
|
-
features(*Features.options(:initialize, diff, activemodel_validations))
|
27
|
-
end
|
28
|
-
|
29
|
-
def self.to_initialize!(diff: false, activemodel_validations: false)
|
30
|
-
features(*Features.options(:strict_initialize, diff, activemodel_validations))
|
31
|
-
end
|
32
|
-
|
33
28
|
def self.without(*names)
|
34
29
|
Features.without(names)
|
35
30
|
end
|
@@ -38,32 +33,8 @@ module Micro
|
|
38
33
|
Features.with(names)
|
39
34
|
end
|
40
35
|
|
41
|
-
def self.
|
42
|
-
|
43
|
-
end
|
44
|
-
|
45
|
-
def self.features(*names)
|
46
|
-
names.empty? ? Features.all : Features.with(names)
|
47
|
-
end
|
48
|
-
|
49
|
-
protected def attributes=(arg)
|
50
|
-
self.class
|
51
|
-
.attributes_data(AttributesUtils.hash_argument!(arg))
|
52
|
-
.each { |name, value| __attribute_set(name, value) }
|
53
|
-
|
54
|
-
__attributes.freeze
|
55
|
-
end
|
56
|
-
|
57
|
-
private def __attributes
|
58
|
-
@__attributes ||= {}
|
59
|
-
end
|
60
|
-
|
61
|
-
private def __attribute_set(name, value)
|
62
|
-
__attributes[name] = instance_variable_set("@#{name}", value) if attribute?(name)
|
63
|
-
end
|
64
|
-
|
65
|
-
def attributes
|
66
|
-
__attributes
|
36
|
+
def self.with_all_features
|
37
|
+
Features.all
|
67
38
|
end
|
68
39
|
|
69
40
|
def attribute?(name)
|
@@ -83,5 +54,87 @@ module Micro
|
|
83
54
|
|
84
55
|
raise NameError, "undefined attribute `#{name}"
|
85
56
|
end
|
57
|
+
|
58
|
+
def attributes(*names)
|
59
|
+
return __attributes if names.empty?
|
60
|
+
|
61
|
+
names.each_with_object({}) do |name, memo|
|
62
|
+
memo[name] = attribute(name) if attribute?(name)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def defined_attributes
|
67
|
+
@defined_attributes ||= self.class.attributes
|
68
|
+
end
|
69
|
+
|
70
|
+
protected
|
71
|
+
|
72
|
+
def attributes=(arg)
|
73
|
+
hash = Utils.stringify_hash_keys(arg)
|
74
|
+
|
75
|
+
__attributes_missing!(hash)
|
76
|
+
|
77
|
+
__attributes_assign(hash)
|
78
|
+
end
|
79
|
+
|
80
|
+
private
|
81
|
+
|
82
|
+
ExtractAttribute = -> (other, key) {
|
83
|
+
return Utils::HashAccess.(other, key) if other.respond_to?(:[])
|
84
|
+
|
85
|
+
other.public_send(key) if other.respond_to?(key)
|
86
|
+
}
|
87
|
+
|
88
|
+
def extract_attributes_from(other)
|
89
|
+
defined_attributes.each_with_object({}) do |key, memo|
|
90
|
+
memo[key] = ExtractAttribute.(other, key)
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
def __attributes
|
95
|
+
@__attributes ||= {}
|
96
|
+
end
|
97
|
+
|
98
|
+
FetchValueToAssign = -> (value, default) do
|
99
|
+
if default.respond_to?(:call)
|
100
|
+
callable = default.is_a?(Proc) ? default : default.method(:call)
|
101
|
+
|
102
|
+
callable.arity > 0 ? callable.call(value) : callable.call
|
103
|
+
else
|
104
|
+
value.nil? ? default : value
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
def __attributes_assign(hash)
|
109
|
+
self.class.__attributes_data__.each do |name, default|
|
110
|
+
__attribute_assign(name, FetchValueToAssign.(hash[name], default)) if attribute?(name)
|
111
|
+
end
|
112
|
+
|
113
|
+
__attributes.freeze
|
114
|
+
end
|
115
|
+
|
116
|
+
def __attribute_assign(name, value)
|
117
|
+
__attributes[name] = instance_variable_set("@#{name}", value)
|
118
|
+
end
|
119
|
+
|
120
|
+
MISSING_KEYWORD = 'missing keyword'.freeze
|
121
|
+
MISSING_KEYWORDS = 'missing keywords'.freeze
|
122
|
+
|
123
|
+
def __attributes_missing!(hash)
|
124
|
+
required_keys = self.class.__attributes_required__
|
125
|
+
|
126
|
+
return if required_keys.empty?
|
127
|
+
|
128
|
+
missing_keys = required_keys.map { |name| ":#{name}" if !hash.key?(name) }
|
129
|
+
missing_keys.compact!
|
130
|
+
|
131
|
+
return if missing_keys.empty?
|
132
|
+
|
133
|
+
label = missing_keys.size == 1 ? MISSING_KEYWORD : MISSING_KEYWORDS
|
134
|
+
|
135
|
+
raise ArgumentError, "#{label}: #{missing_keys.join(', ')}"
|
136
|
+
end
|
137
|
+
|
138
|
+
private_constant :FetchValueToAssign, :MISSING_KEYWORD, :MISSING_KEYWORDS
|
86
139
|
end
|
87
140
|
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Micro::Attributes
|
4
|
+
module Diff
|
5
|
+
class Changes
|
6
|
+
TO = 'to'.freeze
|
7
|
+
FROM = 'from'.freeze
|
8
|
+
FROM_TO_ERROR = 'pass the attribute name with the :from and :to values'.freeze
|
9
|
+
|
10
|
+
attr_reader :from, :to, :differences
|
11
|
+
|
12
|
+
def initialize(from:, to:)
|
13
|
+
raise ArgumentError, "expected an instance of #{from.class}" unless to.is_a?(from.class)
|
14
|
+
@from, @to = from, to
|
15
|
+
@differences = diff(from.attributes, to.attributes).freeze
|
16
|
+
end
|
17
|
+
|
18
|
+
def empty?
|
19
|
+
@differences.empty?
|
20
|
+
end
|
21
|
+
alias_method :blank?, :empty?
|
22
|
+
|
23
|
+
def present?
|
24
|
+
!empty?
|
25
|
+
end
|
26
|
+
|
27
|
+
def changed?(name = nil, from: nil, to: nil)
|
28
|
+
if name.nil?
|
29
|
+
return present? if from.nil? && to.nil?
|
30
|
+
raise ArgumentError, FROM_TO_ERROR
|
31
|
+
elsif from.nil? && to.nil?
|
32
|
+
differences.has_key?(name.to_s)
|
33
|
+
else
|
34
|
+
result = @differences[name.to_s]
|
35
|
+
result ? result[FROM] == from && result[TO] == to : false
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
def diff(from_attributes, to_attributes)
|
42
|
+
@from_attributes, @to_attributes = from_attributes, to_attributes
|
43
|
+
@from_attributes.each_with_object({}) do |(from_key, from_val), acc|
|
44
|
+
to_value = @to_attributes[from_key]
|
45
|
+
acc[from_key] = {FROM => from_val, TO => to_value}.freeze if from_val != to_value
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
private_constant :TO, :FROM, :FROM_TO_ERROR
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -1,22 +1,25 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require
|
3
|
+
require 'micro/attributes/with'
|
4
4
|
|
5
5
|
module Micro
|
6
6
|
module Attributes
|
7
7
|
module Features
|
8
8
|
extend self
|
9
9
|
|
10
|
-
|
10
|
+
STRICT_INITIALIZE = 'strict_initialize'.freeze
|
11
|
+
|
12
|
+
ALL_VISIBLE = [
|
11
13
|
DIFF = 'diff'.freeze,
|
12
14
|
INITIALIZE = 'initialize'.freeze,
|
13
|
-
STRICT_INITIALIZE = 'strict_initialize'.freeze,
|
14
15
|
ACTIVEMODEL_VALIDATIONS = 'activemodel_validations'.freeze
|
15
16
|
].sort.freeze
|
16
17
|
|
18
|
+
ALL = (ALL_VISIBLE + [STRICT_INITIALIZE]).sort.freeze
|
19
|
+
|
17
20
|
INVALID_NAME = [
|
18
21
|
'Invalid feature name! Available options: ',
|
19
|
-
|
22
|
+
ALL_VISIBLE.map { |feature_name| ":#{feature_name}" }.join(', ')
|
20
23
|
].join
|
21
24
|
|
22
25
|
OPTIONS = {
|
@@ -32,8 +35,7 @@ module Micro
|
|
32
35
|
'activemodel_validations:initialize' => With::ActiveModelValidationsAndInitialize,
|
33
36
|
'activemodel_validations:strict_initialize' => With::ActiveModelValidationsAndStrictInitialize,
|
34
37
|
'activemodel_validations:diff:initialize' => With::ActiveModelValidationsAndDiffAndInitialize,
|
35
|
-
'activemodel_validations:diff:strict_initialize' => With::ActiveModelValidationsAndDiffAndStrictInitialize
|
36
|
-
ALL.join(':') => With::ActiveModelValidationsAndDiffAndStrictInitialize
|
38
|
+
'activemodel_validations:diff:strict_initialize' => With::ActiveModelValidationsAndDiffAndStrictInitialize
|
37
39
|
}.freeze
|
38
40
|
|
39
41
|
private_constant :OPTIONS, :INVALID_NAME
|
@@ -44,6 +46,8 @@ module Micro
|
|
44
46
|
|
45
47
|
def with(args)
|
46
48
|
valid_names!(args) do |names|
|
49
|
+
delete_initialize_if_has_strict_initialize(names)
|
50
|
+
|
47
51
|
OPTIONS.fetch(names.sort.join(':'))
|
48
52
|
end
|
49
53
|
end
|
@@ -55,49 +59,53 @@ module Micro
|
|
55
59
|
end
|
56
60
|
end
|
57
61
|
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
+
private
|
63
|
+
|
64
|
+
def fetch_feature_name(name)
|
65
|
+
return name unless name.is_a?(Hash)
|
66
|
+
|
67
|
+
STRICT_INITIALIZE if name[:initialize] == :strict
|
62
68
|
end
|
63
|
-
end
|
64
69
|
|
65
|
-
|
70
|
+
def normalize_names(args)
|
71
|
+
names = Array(args).dup
|
66
72
|
|
67
|
-
|
68
|
-
Array(args).map { |arg| arg.to_s.downcase }.uniq
|
69
|
-
end
|
73
|
+
last_feature = fetch_feature_name(names.pop)
|
70
74
|
|
71
|
-
|
72
|
-
|
73
|
-
|
75
|
+
features = names.empty? ? [last_feature] : names + [last_feature]
|
76
|
+
features.map! { |name| name.to_s.downcase }
|
77
|
+
features.uniq
|
78
|
+
end
|
74
79
|
|
75
|
-
|
76
|
-
|
80
|
+
def valid_names?(names)
|
81
|
+
names.all? { |name| ALL.include?(name) }
|
82
|
+
end
|
77
83
|
|
78
|
-
|
84
|
+
def valid_names!(args)
|
85
|
+
names = normalize_names(args)
|
79
86
|
|
80
|
-
|
81
|
-
end
|
87
|
+
raise ArgumentError, INVALID_NAME if names.empty? || !valid_names?(names)
|
82
88
|
|
83
|
-
|
84
|
-
|
85
|
-
end
|
89
|
+
yield(names)
|
90
|
+
end
|
86
91
|
|
87
|
-
|
88
|
-
|
89
|
-
|
92
|
+
def an_initialize?(name)
|
93
|
+
name == INITIALIZE || name == STRICT_INITIALIZE
|
94
|
+
end
|
90
95
|
|
91
|
-
|
92
|
-
|
93
|
-
end
|
96
|
+
def delete_initialize_if_has_strict_initialize(names)
|
97
|
+
return unless names.include?(STRICT_INITIALIZE)
|
94
98
|
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
+
names.delete_if { |name| name == INITIALIZE }
|
100
|
+
end
|
101
|
+
|
102
|
+
def except_options(names_to_exclude)
|
103
|
+
(ALL - names_to_exclude).tap do |names|
|
104
|
+
names.delete_if { |name| an_initialize?(name) } if names_to_exclude.include?(INITIALIZE)
|
105
|
+
|
106
|
+
delete_initialize_if_has_strict_initialize(names)
|
107
|
+
end
|
99
108
|
end
|
100
|
-
end
|
101
109
|
end
|
102
110
|
end
|
103
111
|
end
|
@@ -3,36 +3,30 @@
|
|
3
3
|
module Micro::Attributes
|
4
4
|
module Features
|
5
5
|
module ActiveModelValidations
|
6
|
-
@@__active_model_required = false
|
7
|
-
@@__active_model_load_error = false
|
8
|
-
|
9
|
-
V32 = '3.2'
|
10
|
-
|
11
6
|
def self.included(base)
|
12
|
-
|
13
|
-
|
14
|
-
require 'active_model'
|
15
|
-
rescue LoadError => e
|
16
|
-
@@__active_model_load_error = true
|
17
|
-
end
|
18
|
-
@@__active_model_required = true
|
19
|
-
end
|
7
|
+
begin
|
8
|
+
require 'active_model'
|
20
9
|
|
21
|
-
unless @@__active_model_load_error
|
22
10
|
base.send(:include, ::ActiveModel::Validations)
|
11
|
+
base.extend(ClassMethods)
|
12
|
+
rescue LoadError
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
module ClassMethods
|
17
|
+
def __call_after_attribute_assign__(attr_name, options)
|
18
|
+
validate, validates = options.values_at(:validate, :validates)
|
23
19
|
|
24
|
-
if
|
25
|
-
|
26
|
-
def initialize(arg)
|
27
|
-
self.attributes=arg
|
28
|
-
run_validations!
|
29
|
-
end
|
30
|
-
RUBY
|
31
|
-
end
|
20
|
+
self.validate(validate) if validate
|
21
|
+
self.validates(attr_name, validates) if validates
|
32
22
|
end
|
33
23
|
end
|
34
24
|
|
35
|
-
|
25
|
+
private
|
26
|
+
|
27
|
+
def __call_after_micro_attribute
|
28
|
+
run_validations! if respond_to?(:run_validations!, true)
|
29
|
+
end
|
36
30
|
end
|
37
31
|
end
|
38
32
|
end
|
@@ -3,59 +3,11 @@
|
|
3
3
|
module Micro::Attributes
|
4
4
|
module Features
|
5
5
|
module Diff
|
6
|
-
class Changes
|
7
|
-
TO = 'to'.freeze
|
8
|
-
FROM = 'from'.freeze
|
9
|
-
FROM_TO_ERROR = 'pass the attribute name with the :from and :to values'.freeze
|
10
|
-
|
11
|
-
attr_reader :from, :to, :differences
|
12
|
-
|
13
|
-
def initialize(from:, to:)
|
14
|
-
raise ArgumentError, "expected an instance of #{from.class}" unless to.is_a?(from.class)
|
15
|
-
@from, @to = from, to
|
16
|
-
@differences = diff(from.attributes, to.attributes).freeze
|
17
|
-
end
|
18
|
-
|
19
|
-
def empty?
|
20
|
-
@differences.empty?
|
21
|
-
end
|
22
|
-
alias_method :blank?, :empty?
|
23
|
-
|
24
|
-
def present?
|
25
|
-
!empty?
|
26
|
-
end
|
27
|
-
|
28
|
-
def changed?(name = nil, from: nil, to: nil)
|
29
|
-
if name.nil?
|
30
|
-
return present? if from.nil? && to.nil?
|
31
|
-
raise ArgumentError, FROM_TO_ERROR
|
32
|
-
elsif from.nil? && to.nil?
|
33
|
-
differences.has_key?(name.to_s)
|
34
|
-
else
|
35
|
-
result = @differences[name.to_s]
|
36
|
-
result ? result[FROM] == from && result[TO] == to : false
|
37
|
-
end
|
38
|
-
end
|
39
|
-
|
40
|
-
private
|
41
|
-
|
42
|
-
def diff(from_attributes, to_attributes)
|
43
|
-
@from_attributes, @to_attributes = from_attributes, to_attributes
|
44
|
-
@from_attributes.each_with_object({}) do |(from_key, from_val), acc|
|
45
|
-
to_value = @to_attributes[from_key]
|
46
|
-
acc[from_key] = {FROM => from_val, TO => to_value}.freeze if from_val != to_value
|
47
|
-
end
|
48
|
-
end
|
49
|
-
|
50
|
-
private_constant :TO, :FROM, :FROM_TO_ERROR
|
51
|
-
end
|
52
|
-
|
53
6
|
def diff_attributes(to)
|
54
|
-
return Changes.new(from: self, to: to) if to.is_a?(::Micro::Attributes)
|
7
|
+
return Micro::Attributes::Diff::Changes.new(from: self, to: to) if to.is_a?(::Micro::Attributes)
|
8
|
+
|
55
9
|
raise ArgumentError, "#{to.inspect} must implement Micro::Attributes"
|
56
10
|
end
|
57
|
-
|
58
|
-
private_constant :Changes
|
59
11
|
end
|
60
12
|
end
|
61
13
|
end
|