u-attributes 2.0.1 → 2.1.0

Sign up to get free protection for your applications and to get access to all the features.
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