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.
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 = {
@@ -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
- def options(init, diff, activemodel_validations)
59
- [init].tap do |options|
60
- options << :diff if diff
61
- 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
62
68
  end
63
- end
64
69
 
65
- private
70
+ def normalize_names(args)
71
+ names = Array(args).dup
66
72
 
67
- def normalize_names(args)
68
- Array(args).map { |arg| arg.to_s.downcase }.uniq
69
- end
73
+ last_feature = fetch_feature_name(names.pop)
70
74
 
71
- def valid_names?(names)
72
- names.all? { |name| ALL.include?(name) }
73
- end
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
- def valid_names!(args)
76
- names = normalize_names(args)
80
+ def valid_names?(names)
81
+ names.all? { |name| ALL.include?(name) }
82
+ end
77
83
 
78
- raise ArgumentError, INVALID_NAME if args.empty? || !valid_names?(names)
84
+ def valid_names!(args)
85
+ names = normalize_names(args)
79
86
 
80
- yield(names)
81
- end
87
+ raise ArgumentError, INVALID_NAME if names.empty? || !valid_names?(names)
82
88
 
83
- def has_strict_initialize?(names)
84
- names.include?(STRICT_INITIALIZE)
85
- end
89
+ yield(names)
90
+ end
86
91
 
87
- def any_kind_of_initialize?(names)
88
- names.include?(INITIALIZE) || has_strict_initialize?(names)
89
- end
92
+ def an_initialize?(name)
93
+ name == INITIALIZE || name == STRICT_INITIALIZE
94
+ end
90
95
 
91
- def an_initialize?(name)
92
- name == INITIALIZE || name == STRICT_INITIALIZE
93
- end
96
+ def delete_initialize_if_has_strict_initialize(names)
97
+ return unless names.include?(STRICT_INITIALIZE)
94
98
 
95
- def except_options(names_to_exclude)
96
- (ALL - names_to_exclude).tap do |names|
97
- names.delete_if { |name| an_initialize?(name) } if any_kind_of_initialize?(names_to_exclude)
98
- names.delete_if { |name| name == INITIALIZE } if has_strict_initialize?(names)
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
- 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