u-attributes 2.0.1 → 2.1.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bc53b00949ac8d0d8d8572443936ba537a637569be77e0b105a0b98f2d1ae9d0
4
- data.tar.gz: 94f42f6da3c2cba68f6565e1bb413b5692bd700baa077555cd421e4aeb82e2ff
3
+ metadata.gz: d105f8057ad000b717972d8b2b5624d3f5d7608256f82402878ae0929c6462c7
4
+ data.tar.gz: 20f7b0132c445194fb3189b81c7c29c482a2fbeffdb1574444f833ede5619308
5
5
  SHA512:
6
- metadata.gz: e5617a5fd34458de945dae2874bed24013e682871f19b70063e734156f2528e8b249e615632f90c978c203abf876560de9717940f9b81e48271f84a956456199
7
- data.tar.gz: 50b19483c9e8f88169101306be67a34e4e8c6a26358b187fd119b41ca26e83d461619d6a149bfc898ab7b1208c6af6791b58acf039a3d92d0e8f912e007232b9
6
+ metadata.gz: 7d5e1d415a6b94dfa9c90aad9f8700829696d1e1b2fa054d6bf62c9eac477f618604c59695b55c3a18dd2405b28c4af63c886057f2ef740cba747f19b2273706
7
+ data.tar.gz: 4493325bbeed30c82c13690ad35d87926a9913b3b33d18cc0ef2dedae8eca2e976fc43d2485bb65715b2cb6c51bc383dfd8b0c59b50885bd5099624b568455d6
data/Gemfile CHANGED
@@ -1,5 +1,7 @@
1
1
  source 'https://rubygems.org'
2
2
 
3
+ gem 'u-case', '~> 4.0'
4
+
3
5
  activemodel_version = ENV.fetch('ACTIVEMODEL_VERSION', '6.1')
4
6
 
5
7
  activemodel = case activemodel_version
@@ -18,9 +20,16 @@ if activemodel_version < '6.1'
18
20
  gem 'activesupport', activemodel, require: false
19
21
  end
20
22
 
23
+ simplecov_version =
24
+ case RUBY_VERSION
25
+ when /\A2.[23]/ then '~> 0.17.1'
26
+ when /\A2.4/ then '~> 0.18.5'
27
+ else '~> 0.19'
28
+ end
29
+
21
30
  group :test do
22
31
  gem 'minitest', activemodel_version < '4.1' ? '~> 4.2' : '~> 5.0'
23
- gem 'simplecov', require: false
32
+ gem 'simplecov', simplecov_version, require: false
24
33
  end
25
34
 
26
35
  # Specify your gem's dependencies in u-attributes.gemspec
data/README.md CHANGED
@@ -1,21 +1,41 @@
1
- ![Ruby](https://img.shields.io/badge/ruby-2.2+-ruby.svg?colorA=99004d&colorB=cc0066)
2
- [![Gem](https://img.shields.io/gem/v/u-attributes.svg?style=flat-square)](https://rubygems.org/gems/u-attributes)
3
- [![Build Status](https://travis-ci.com/serradura/u-attributes.svg?branch=main)](https://travis-ci.com/serradura/u-attributes)
4
- [![Maintainability](https://api.codeclimate.com/v1/badges/b562e6b877a9edf4dbf6/maintainability)](https://codeclimate.com/github/serradura/u-attributes/maintainability)
5
- [![Test Coverage](https://api.codeclimate.com/v1/badges/b562e6b877a9edf4dbf6/test_coverage)](https://codeclimate.com/github/serradura/u-attributes/test_coverage)
1
+ <p align="center">
2
+ <img src="./assets/u-attributes_logo_v1.png" alt='Create "immutable" objects. No setters, just getters!'>
6
3
 
7
- μ-attributes (Micro::Attributes) <!-- omit in toc -->
8
- ================================
4
+ <p align="center"><i>Create "immutable" objects. No setters, just getters!</i></p>
5
+ <br>
6
+ </p>
7
+
8
+ <p align="center">
9
+ <img src="https://img.shields.io/badge/ruby-2.2+-ruby.svg?colorA=99004d&colorB=cc0066" alt="Ruby">
10
+
11
+ <a href="https://rubygems.org/gems/u-attributes">
12
+ <img alt="Gem" src="https://img.shields.io/gem/v/u-attributes.svg?style=flat-square">
13
+ </a>
14
+
15
+ <a href="https://travis-ci.com/serradura/u-attributes">
16
+ <img alt="Build Status" src="https://travis-ci.com/serradura/u-attributes.svg?branch=main">
17
+ </a>
18
+
19
+ <a href="https://codeclimate.com/github/serradura/u-attributes/maintainability">
20
+ <img alt="Maintainability" src="https://api.codeclimate.com/v1/badges/b562e6b877a9edf4dbf6/maintainability">
21
+ </a>
22
+
23
+ <a href="https://codeclimate.com/github/serradura/u-attributes/test_coverage">
24
+ <img alt="Test Coverage" src="https://api.codeclimate.com/v1/badges/b562e6b877a9edf4dbf6/test_coverage">
25
+ </a>
26
+ </p>
9
27
 
10
28
  This gem allows you to define "immutable" objects, and your objects will have only getters and no setters.
11
29
  So, if you change [[1](#with_attribute)] [[2](#with_attributes)] some object attribute, you will have a new object instance. That is, you transform the object instead of modifying it.
12
30
 
13
- ## Table of contents <!-- omit in toc -->
31
+ # Table of contents <!-- omit in toc -->
14
32
  - [Installation](#installation)
15
33
  - [Compatibility](#compatibility)
16
34
  - [Usage](#usage)
17
35
  - [How to define attributes?](#how-to-define-attributes)
18
36
  - [`Micro::Attributes#attributes=`](#microattributesattributes)
37
+ - [How to extract attributes from an object or hash?](#how-to-extract-attributes-from-an-object-or-hash)
38
+ - [Is it possible to define an attribute as required?](#is-it-possible-to-define-an-attribute-as-required)
19
39
  - [`Micro::Attributes#attribute`](#microattributesattribute)
20
40
  - [`Micro::Attributes#attribute!`](#microattributesattribute-1)
21
41
  - [How to define multiple attributes?](#how-to-define-multiple-attributes)
@@ -55,7 +75,7 @@ gem 'u-attributes'
55
75
 
56
76
  | u-attributes | branch | ruby | activemodel |
57
77
  | -------------- | ------- | -------- | ------------- |
58
- | 2.0.0 | main | >= 2.2.0 | >= 3.2, < 6.1 |
78
+ | 2.1.0 | main | >= 2.2.0 | >= 3.2, < 6.1 |
59
79
  | 1.2.0 | v1.x | >= 2.2.0 | >= 3.2, < 6.1 |
60
80
 
61
81
  > **Note**: The activemodel is an optional dependency, this module [can be enabled](#activemodelvalidation-extension) to validate the attributes.
@@ -66,9 +86,9 @@ gem 'u-attributes'
66
86
 
67
87
  ## How to define attributes?
68
88
 
69
- ```ruby
70
- # By default you must to define the class constructor.
89
+ By default, you must define the class constructor.
71
90
 
91
+ ```ruby
72
92
  class Person
73
93
  include Micro::Attributes
74
94
 
@@ -116,6 +136,66 @@ person.age # 20
116
136
  person.name # John Doe
117
137
  ```
118
138
 
139
+ #### How to extract attributes from an object or hash?
140
+
141
+ You can extract attributes using the `extract_attributes_from` method, it will try to fetch attributes from the
142
+ object using either the `object[attribute_key]` accessor or the reader method `object.attribute_key`.
143
+
144
+ ```ruby
145
+ class Person
146
+ include Micro::Attributes
147
+
148
+ attribute :age
149
+ attribute :name, default: 'John Doe'
150
+
151
+ def initialize(user:)
152
+ self.attributes = extract_attributes_from(user)
153
+ end
154
+ end
155
+
156
+ # extracting from an object
157
+
158
+ class User
159
+ attr_accessor :age, :name
160
+ end
161
+
162
+ user = User.new
163
+ user.age = 20
164
+
165
+ person = Person.new(user: user)
166
+
167
+ person.age # 20
168
+ person.name # John Doe
169
+
170
+ # extracting from a hash
171
+
172
+ another_person = Person.new(user: { age: 55, name: 'Julia Not Roberts' })
173
+
174
+ another_person.age # 55
175
+ another_person.name # Julia Not Roberts
176
+ ```
177
+
178
+ #### Is it possible to define an attribute as required?
179
+
180
+ You only need to use the `required: true` option.
181
+
182
+ But to this work, you need to assign the attributes using the [`#attributes=`](#microattributesattributes) method or the extensions: [initialize](#initialize-extension), [activemodel_validations](#activemodelvalidation-extension).
183
+
184
+ ```ruby
185
+ class Person
186
+ include Micro::Attributes
187
+
188
+ attribute :age
189
+ attribute :name, required: true
190
+
191
+ def initialize(attributes)
192
+ self.attributes = attributes
193
+ end
194
+ end
195
+
196
+ Person.new(age: 32) # ArgumentError (missing keyword: :name)
197
+ ```
198
+
119
199
  [⬆️ Back to Top](#table-of-contents-)
120
200
 
121
201
  ### `Micro::Attributes#attribute`
@@ -185,7 +265,7 @@ Use `Micro::Attributes.with(:initialize)` to define a constructor to assign the
185
265
  class Person
186
266
  include Micro::Attributes.with(:initialize)
187
267
 
188
- attribute :age
268
+ attribute :age, required: true
189
269
  attribute :name, default: 'John Doe'
190
270
  end
191
271
 
@@ -258,7 +338,9 @@ end
258
338
 
259
339
  ## The strict initializer
260
340
 
261
- Use `.with(initialize: :strict)` to forbids an instantiation without all the attribute keywords. e.g.
341
+ Use `.with(initialize: :strict)` to forbids an instantiation without all the attribute keywords.
342
+
343
+ In other words, it is equivalent to you define all the attributes using the [`required: true` option](#is-it-possible-to-define-an-attribute-as-required).
262
344
 
263
345
  ```ruby
264
346
  class StrictPerson
@@ -369,6 +451,12 @@ Person.attribute?(:foo) # false
369
451
 
370
452
  person = Person.new(age: 20)
371
453
 
454
+ #---------------------#
455
+ # #defined_attributes #
456
+ #---------------------#
457
+
458
+ person.defined_attributes # ['name', 'age']
459
+
372
460
  #---------------#
373
461
  # #attribute?() #
374
462
  #---------------#
@@ -14,7 +14,8 @@ module Micro
14
14
  base.extend(::Micro::Attributes.const_get(:Macros))
15
15
 
16
16
  base.class_eval do
17
- private_class_method :__attributes, :__attribute_set, :__attribute_reader
17
+ private_class_method :__attributes, :__attribute_reader
18
+ private_class_method :__attribute_assign, :__attributes_data_to_assign
18
19
  end
19
20
 
20
21
  def base.inherited(subclass)
@@ -62,40 +63,78 @@ module Micro
62
63
  end
63
64
  end
64
65
 
66
+ def defined_attributes
67
+ @defined_attributes ||= self.class.attributes
68
+ end
69
+
65
70
  protected
66
71
 
67
72
  def attributes=(arg)
68
73
  hash = Utils.stringify_hash_keys(arg)
69
74
 
70
- __attributes_set(hash, self.class.__attributes_data__)
75
+ __attributes_missing!(hash)
76
+
77
+ __attributes_assign(hash)
71
78
  end
72
79
 
73
80
  private
74
81
 
75
- def __attributes
76
- @__attributes ||= {}
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
77
92
  end
78
93
 
79
- def __attribute_set(name, value)
80
- __attributes[name] = instance_variable_set("@#{name}", value) if attribute?(name)
94
+ def __attributes
95
+ @__attributes ||= {}
81
96
  end
82
97
 
83
- def __attributes_set(hash, att_data)
84
- att_data.each do |key, default|
85
- value = hash[key]
98
+ FetchValueToAssign = -> (value, default) do
99
+ if default.respond_to?(:call)
100
+ callable = default.is_a?(Proc) ? default : default.method(:call)
86
101
 
87
- final_value =
88
- if default.respond_to?(:call)
89
- callable = default.is_a?(Proc) ? default : default.method(:call)
90
- callable.arity > 0 ? callable.call(value) : callable.call
91
- else
92
- value.nil? ? default : value
93
- end
102
+ callable.arity > 0 ? callable.call(value) : callable.call
103
+ else
104
+ value.nil? ? default : value
105
+ end
106
+ end
94
107
 
95
- __attribute_set(key, final_value)
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)
96
111
  end
97
112
 
98
113
  __attributes.freeze
99
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
100
139
  end
101
140
  end
@@ -14,7 +14,7 @@ module Micro::Attributes
14
14
  end
15
15
 
16
16
  module ClassMethods
17
- def __call_after_attribute_set__(attr_name, options)
17
+ def __call_after_attribute_assign__(attr_name, options)
18
18
  validate, validates = options.values_at(:validate, :validates)
19
19
 
20
20
  self.validate(validate) if validate
@@ -4,35 +4,15 @@ module Micro::Attributes
4
4
  module Features
5
5
  module Initialize
6
6
  module Strict
7
- MISSING_KEYWORD = 'missing keyword'.freeze
8
- MISSING_KEYWORDS = 'missing keywords'.freeze
9
-
10
- protected def attributes=(arg)
11
- arg_hash = Utils.stringify_hash_keys(arg)
12
- att_data = self.class.__attributes_data__
13
-
14
- attributes_missing!(ref: att_data, arg: arg_hash)
15
-
16
- __attributes_set(arg_hash, att_data)
17
- end
18
-
19
- private def attributes_missing!(ref:, arg:)
20
- missing_keys = attributes_missing(ref, arg)
21
-
22
- return if missing_keys.empty?
23
-
24
- label = missing_keys.size == 1 ? MISSING_KEYWORD : MISSING_KEYWORDS
25
-
26
- raise ArgumentError, "#{label}: #{missing_keys.join(', ')}"
27
- end
28
-
29
- private def attributes_missing(ref, arg)
30
- ref.each_with_object([]) do |(key, val), memo|
31
- memo << ":#{key}" if val.nil? && !arg.has_key?(key)
7
+ module ClassMethods
8
+ def attributes_are_all_required?
9
+ true
32
10
  end
33
11
  end
34
12
 
35
- private_constant :MISSING_KEYWORD, :MISSING_KEYWORDS
13
+ def self.included(base)
14
+ base.send(:extend, ClassMethods)
15
+ end
36
16
  end
37
17
  end
38
18
  end
@@ -3,10 +3,33 @@
3
3
  module Micro
4
4
  module Attributes
5
5
  module Macros
6
+ def attributes_are_all_required?
7
+ false
8
+ end
9
+
10
+ # NOTE: can't be renamed! It is used by u-case v4.
6
11
  def __attributes_data__
7
12
  @__attributes_data__ ||= {}
8
13
  end
9
14
 
15
+ def __attributes_required__
16
+ @__attributes_required__ ||= Set.new
17
+ end
18
+
19
+ def __attributes_required_add(name, is_required, hasnt_default)
20
+ if is_required || (attributes_are_all_required? && hasnt_default)
21
+ __attributes_required__.add(name)
22
+ end
23
+
24
+ nil
25
+ end
26
+
27
+ def __attributes_data_to_assign(name, options)
28
+ hasnt_default = !options.key?(:default)
29
+
30
+ hasnt_default ? __attributes_required_add(name, options[:required], hasnt_default) : options[:default]
31
+ end
32
+
10
33
  def __attributes
11
34
  @__attributes ||= Set.new
12
35
  end
@@ -17,20 +40,24 @@ module Micro
17
40
  attr_reader(name)
18
41
  end
19
42
 
20
- def __attribute_set(key, can_overwrite, options)
43
+ def __attribute_assign(key, can_overwrite, options)
21
44
  name = key.to_s
22
45
  has_attribute = attribute?(name)
23
46
 
24
47
  __attribute_reader(name) unless has_attribute
25
- __attributes_data__[name] = options[:default] if can_overwrite || !has_attribute
26
48
 
27
- __call_after_attribute_set__(name, options)
49
+ __attributes_data__[name] = __attributes_data_to_assign(name, options) if can_overwrite || !has_attribute
50
+
51
+ __call_after_attribute_assign__(name, options)
28
52
  end
29
53
 
30
- def __call_after_attribute_set__(attr_name, options); end
54
+ def __call_after_attribute_assign__(attr_name, options); end
31
55
 
56
+ # NOTE: can't be renamed! It is used by u-case v4.
32
57
  def __attributes_set_after_inherit__(arg)
33
- arg.each { |key, val| __attribute_set(key, true, default: val) }
58
+ arg.each do |key, val|
59
+ __attribute_assign(key, true, val ? { default: val } : {})
60
+ end
34
61
  end
35
62
 
36
63
  def attribute?(name)
@@ -38,7 +65,7 @@ module Micro
38
65
  end
39
66
 
40
67
  def attribute(name, options = Kind::Empty::HASH)
41
- __attribute_set(name, false, options)
68
+ __attribute_assign(name, false, options)
42
69
  end
43
70
 
44
71
  def attributes(*args)
@@ -47,18 +74,19 @@ module Micro
47
74
  args.flatten!
48
75
  args.each do |arg|
49
76
  if arg.is_a?(String) || arg.is_a?(Symbol)
50
- __attribute_set(arg, false, Kind::Empty::HASH)
77
+ __attribute_assign(arg, false, Kind::Empty::HASH)
51
78
  else
52
79
  raise Kind::Error.new('String/Symbol'.freeze, arg)
53
80
  end
54
81
  end
55
82
  end
56
83
 
84
+ # NOTE: can't be renamed! It is used by u-case v4.
57
85
  module ForSubclasses
58
86
  WRONG_NUMBER_OF_ARGS = 'wrong number of arguments (given 0, expected 1 or more)'.freeze
59
87
 
60
88
  def attribute!(name, options = Kind::Empty::HASH)
61
- __attribute_set(name, true, options)
89
+ __attribute_assign(name, true, options)
62
90
  end
63
91
 
64
92
  private_constant :WRONG_NUMBER_OF_ARGS
@@ -14,6 +14,12 @@ module Micro
14
14
  hash.each_with_object({}) { |(key, val), memo| memo[key.to_s] = val }
15
15
  end
16
16
  end
17
+
18
+ HashAccess = -> (hash, key) {
19
+ return hash[key.to_s] unless hash[key.to_s].nil?
20
+
21
+ hash[key.to_sym]
22
+ }
17
23
  end
18
24
  end
19
25
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Micro
4
4
  module Attributes
5
- VERSION = '2.0.1'.freeze
5
+ VERSION = '2.1.0'.freeze
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: u-attributes
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.1
4
+ version: 2.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Rodrigo Serradura
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2020-08-21 00:00:00.000000000 Z
11
+ date: 2020-08-23 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: kind
@@ -76,6 +76,7 @@ files:
76
76
  - LICENSE.txt
77
77
  - README.md
78
78
  - Rakefile
79
+ - assets/u-attributes_logo_v1.png
79
80
  - bin/console
80
81
  - bin/setup
81
82
  - lib/micro/attributes.rb