u-attributes 1.1.1 → 2.1.1

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.
data/Rakefile CHANGED
@@ -1,10 +1,10 @@
1
- require "bundler/gem_tasks"
2
- require "rake/testtask"
1
+ require 'bundler/gem_tasks'
2
+ require 'rake/testtask'
3
3
 
4
4
  Rake::TestTask.new(:test) do |t|
5
- t.libs << "test"
6
- t.libs << "lib"
7
- t.test_files = FileList["test/**/*_test.rb"]
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
@@ -1,14 +1,14 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
- require "bundler/setup"
4
- require "micro/attributes"
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 "pry"
10
+ # require 'pry'
11
11
  # Pry.start
12
12
 
13
- require "irb"
13
+ require 'irb'
14
14
  IRB.start(__FILE__)
@@ -1,35 +1,30 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "micro/attributes/version"
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 :__attributes_data, :__attributes
15
- private_class_method :__attributes_def, :__attributes_set
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.attributes(self.attributes_data({}))
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.feature(name)
42
- self.with(name)
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 "micro/attributes/with"
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
- ALL = [
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
- ALL.map { |feature_name| ":#{feature_name}" }.join(', ')
22
+ ALL_VISIBLE.map { |feature_name| ":#{feature_name}" }.join(', ')
20
23
  ].join
21
24
 
22
25
  OPTIONS = {
@@ -56,48 +59,53 @@ module Micro
56
59
  end
57
60
  end
58
61
 
59
- def options(init, diff, activemodel_validations)
60
- [init].tap do |options|
61
- options << :diff if diff
62
- options << :activemodel_validations if activemodel_validations
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
63
68
  end
64
- end
65
69
 
66
- private
70
+ def normalize_names(args)
71
+ names = Array(args).dup
67
72
 
68
- def normalize_names(args)
69
- Array(args).map { |arg| arg.to_s.downcase }.uniq
70
- end
73
+ last_feature = fetch_feature_name(names.pop)
71
74
 
72
- def valid_names?(names)
73
- names.all? { |name| ALL.include?(name) }
74
- end
75
+ features = names.empty? ? [last_feature] : names + [last_feature]
76
+ features.map! { |name| name.to_s.downcase }
77
+ features.uniq
78
+ end
75
79
 
76
- def valid_names!(args)
77
- names = normalize_names(args)
80
+ def valid_names?(names)
81
+ names.all? { |name| ALL.include?(name) }
82
+ end
78
83
 
79
- raise ArgumentError, INVALID_NAME if args.empty? || !valid_names?(names)
84
+ def valid_names!(args)
85
+ names = normalize_names(args)
80
86
 
81
- yield(names)
82
- end
87
+ raise ArgumentError, INVALID_NAME if names.empty? || !valid_names?(names)
83
88
 
84
- def an_initialize?(name)
85
- name == INITIALIZE || name == STRICT_INITIALIZE
86
- end
89
+ yield(names)
90
+ end
91
+
92
+ def an_initialize?(name)
93
+ name == INITIALIZE || name == STRICT_INITIALIZE
94
+ end
87
95
 
88
- def delete_initialize_if_has_strict_initialize(names)
89
- return unless names.include?(STRICT_INITIALIZE)
96
+ def delete_initialize_if_has_strict_initialize(names)
97
+ return unless names.include?(STRICT_INITIALIZE)
90
98
 
91
- names.delete_if { |name| name == INITIALIZE }
92
- end
99
+ names.delete_if { |name| name == INITIALIZE }
100
+ end
93
101
 
94
- def except_options(names_to_exclude)
95
- (ALL - names_to_exclude).tap do |names|
96
- names.delete_if { |name| an_initialize?(name) } if names_to_exclude.include?(INITIALIZE)
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)
97
105
 
98
- delete_initialize_if_has_strict_initialize(names)
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
- if !@@__active_model_load_error && !@@__active_model_required
13
- begin
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 ::ActiveModel::VERSION::STRING >= V32
25
- base.class_eval(<<-RUBY)
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
- private_constant :V32
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