u-attributes 2.7.0 → 3.0.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: c330847c820a244e35afcd702510d791a72822677ad97a4050b37dc0a9653a00
4
- data.tar.gz: 66f7e86e1c1579f64d39e77c54eac7fab03551dbbc8fd6ce02438fab817c0939
3
+ metadata.gz: '01848f333334d9af69fbd608407e9e287ba9411305c2307fba64bb5bd85d076c'
4
+ data.tar.gz: f1778463cfdeeb64595a2a803fe93b720f587b65c62a95abf4693d7a10227330
5
5
  SHA512:
6
- metadata.gz: d06af39a1601f4ba9496e5daec023cb13d1944f0ab2b986320a365788df2208fecf1122c73d023c669647fb3f826eca15d0f4bf132872c6b84459bd13c8037ce
7
- data.tar.gz: 16bd3897ce372901db54506761dffbc3ba42a72e6700fabc0db83fedad6ab8eaf9bb69f1d98c26e4c0eee809311d51926431807fde187af3b80a6cdba2d73f78
6
+ metadata.gz: 1bb8672adcf5c2221ead65d0fbcb1523a74d960c20056c8e989404d63a6941ed5bb0a51a3e4acae4dc635ad24ff5e446ba349cc65b5e6b38a9093f51ea93b50c
7
+ data.tar.gz: 135eb428c82e85a8a5a197c3f9e1170df2c9dcd33aa6e89162a292c79055299bf5db614497dbc51fb926ba16dec2028a5791e5d3faf9103f46746d9c059db0ae
@@ -0,0 +1,66 @@
1
+ name: Ruby
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - main
7
+
8
+ pull_request:
9
+
10
+ jobs:
11
+ test:
12
+ runs-on: ubuntu-latest
13
+ name: Ruby ${{ matrix.ruby }}
14
+ permissions:
15
+ contents: read
16
+ strategy:
17
+ fail-fast: false
18
+ matrix:
19
+ ruby: ["2.7", "3.0", "3.1", "3.2", "3.3", "3.4", "4.0", head]
20
+ steps:
21
+ - uses: actions/checkout@v4
22
+ with:
23
+ persist-credentials: false
24
+ - name: Set up Ruby
25
+ uses: ruby/setup-ruby@v1
26
+ with:
27
+ ruby-version: ${{ matrix.ruby }}
28
+ - name: Install bundler
29
+ run: gem install bundler -v 2.4.22
30
+ if: ${{ matrix.ruby == '2.7' || matrix.ruby == '3.0' }}
31
+ - name: Bundle install
32
+ run: bundle install
33
+ - name: Setup project
34
+ run: bundle exec appraisal install
35
+ - name: Run baseline tests (no activemodel)
36
+ run: bundle exec rake test
37
+ - name: Run tests for Rails 6.0
38
+ run: bundle exec appraisal rails-6-0 rake test
39
+ if: ${{ matrix.ruby == '2.7' || matrix.ruby == '3.0' }}
40
+ - name: Run tests for Rails 6.1
41
+ run: bundle exec appraisal rails-6-1 rake test
42
+ if: ${{ matrix.ruby == '2.7' || matrix.ruby == '3.0' }}
43
+ - name: Run tests for Rails 7.0
44
+ run: bundle exec appraisal rails-7-0 rake test
45
+ if: ${{ matrix.ruby == '2.7' || matrix.ruby == '3.0' || matrix.ruby == '3.1' || matrix.ruby == '3.2' || matrix.ruby == '3.3' }}
46
+ - name: Run tests for Rails 7.1
47
+ run: bundle exec appraisal rails-7-1 rake test
48
+ if: ${{ matrix.ruby == '2.7' || matrix.ruby == '3.0' || matrix.ruby == '3.1' || matrix.ruby == '3.2' || matrix.ruby == '3.3' }}
49
+ - name: Run tests for Rails 7.2
50
+ run: bundle exec appraisal rails-7-2 rake test
51
+ if: ${{ matrix.ruby == '3.1' || matrix.ruby == '3.2' || matrix.ruby == '3.3' || matrix.ruby == '3.4' }}
52
+ - name: Run tests for Rails 8.0
53
+ run: bundle exec appraisal rails-8-0 rake test
54
+ if: ${{ matrix.ruby == '3.2' || matrix.ruby == '3.3' || matrix.ruby == '3.4' }}
55
+ - name: Run tests for Rails 8.1
56
+ run: bundle exec appraisal rails-8-1 rake test
57
+ if: ${{ matrix.ruby == '3.3' || matrix.ruby == '3.4' || matrix.ruby == '4.0' }}
58
+ - name: Run tests for Rails edge
59
+ run: bundle exec appraisal rails-edge rake test
60
+ if: ${{ matrix.ruby == '4.0' || matrix.ruby == 'head' }}
61
+ - name: Upload coverage to Qlty
62
+ uses: qltysh/qlty-action/coverage@v2
63
+ if: ${{ matrix.ruby == '3.4' && !github.base_ref }}
64
+ with:
65
+ token: ${{ secrets.QLTY_COVERAGE_TOKEN }}
66
+ files: coverage/.resultset.json
data/.gitignore CHANGED
@@ -1,3 +1,5 @@
1
+ .DS_Store
2
+
1
3
  /.bundle/
2
4
  /.yardoc
3
5
  /_yardoc/
@@ -6,4 +8,10 @@
6
8
  /pkg/
7
9
  /spec/reports/
8
10
  /tmp/
11
+
9
12
  Gemfile.lock
13
+
14
+ gemfiles/*.lock
15
+ gemfiles/.bundle/
16
+
17
+ tags
@@ -0,0 +1,8 @@
1
+ {
2
+ "cSpell.enabled": true,
3
+ "cSpell.ignoreWords": [
4
+ "paambaati",
5
+ "resultset",
6
+ "simplecov"
7
+ ]
8
+ }
data/Appraisals ADDED
@@ -0,0 +1,84 @@
1
+ if RUBY_VERSION < "3.1"
2
+ appraise "rails-6-0" do
3
+ group :test do
4
+ gem "logger", "~> 1.6", ">= 1.6.6"
5
+ gem "stringio", "~> 3.2"
6
+
7
+ gem "minitest", "5.26.1"
8
+ gem "activemodel", "~> 6.0.0"
9
+ end
10
+ end
11
+
12
+ appraise "rails-6-1" do
13
+ group :test do
14
+ gem "logger", "~> 1.6", ">= 1.6.6"
15
+ gem "stringio", "~> 3.2"
16
+
17
+ gem "minitest", "5.26.1"
18
+ gem "activemodel", "~> 6.1.0"
19
+ end
20
+ end
21
+ end
22
+
23
+ if RUBY_VERSION >= "2.7" && RUBY_VERSION < "3.4"
24
+ appraise "rails-7-0" do
25
+ group :test do
26
+ gem "logger", "~> 1.6", ">= 1.6.6"
27
+ gem "stringio", "~> 3.2"
28
+ gem "securerandom", "~> 0.3.2"
29
+
30
+ gem "minitest", "5.26.1"
31
+ gem "activemodel", "~> 7.0.0"
32
+ end
33
+ end
34
+
35
+ appraise "rails-7-1" do
36
+ group :test do
37
+ gem "logger", "~> 1.6", ">= 1.6.6"
38
+ gem "stringio", "~> 3.2"
39
+ gem "securerandom", "~> 0.3.2"
40
+
41
+ gem "minitest", "5.26.1"
42
+ gem "activemodel", "~> 7.1.0"
43
+ end
44
+ end
45
+ end
46
+
47
+ if RUBY_VERSION >= "3.1" && RUBY_VERSION < "4.0"
48
+ appraise "rails-7-2" do
49
+ group :test do
50
+ gem "minitest", "~> 5.27"
51
+ gem "activemodel", "~> 7.2.0"
52
+ end
53
+ end
54
+ end
55
+
56
+ if RUBY_VERSION >= "3.2" && RUBY_VERSION < "4.0"
57
+ appraise "rails-8-0" do
58
+ group :test do
59
+ gem "ostruct", "~> 0.6.3"
60
+ gem "minitest", "~> 5.27"
61
+ gem "activemodel", "~> 8.0.0"
62
+ end
63
+ end
64
+ end
65
+
66
+ if RUBY_VERSION >= "3.3.0"
67
+ minitest_version = (RUBY_VERSION >= "4.0.0") ? "~> 6.0" : "~> 5.27"
68
+
69
+ appraise "rails-8-1" do
70
+ group :test do
71
+ gem "ostruct", "~> 0.6.3"
72
+ gem "minitest", minitest_version
73
+ gem "activemodel", "~> 8.1.0"
74
+ end
75
+ end
76
+
77
+ appraise "rails-edge" do
78
+ group :test do
79
+ gem "ostruct", "~> 0.6.3"
80
+ gem "minitest", minitest_version
81
+ gem "activemodel", github: "rails/rails", branch: "main"
82
+ end
83
+ end
84
+ end
data/Gemfile CHANGED
@@ -1,41 +1,18 @@
1
- source 'https://rubygems.org'
1
+ source "https://rubygems.org"
2
2
 
3
- gem 'u-case', '~> 4.0'
3
+ git_source(:github) { |repo_name| "https://github.com/#{repo_name}" }
4
4
 
5
- activemodel_version = ENV['ACTIVEMODEL_VERSION']
5
+ # Specify your gem's dependencies in u-attributes.gemspec
6
+ gemspec
6
7
 
7
- activemodel = case activemodel_version
8
- when '3.2' then '3.2.22'
9
- when '4.0' then '4.0.13'
10
- when '4.1' then '4.1.16'
11
- when '4.2' then '4.2.11'
12
- when '5.0' then '5.0.7'
13
- when '5.1' then '5.1.7'
14
- when '5.2' then '5.2.4'
15
- when '6.0' then '6.0.3.4'
16
- when '6.1' then '6.1.2'
17
- end
8
+ gem "rake", "~> 13.0"
18
9
 
19
- simplecov_version =
20
- case RUBY_VERSION
21
- when /\A2.[23]/ then '0.17.1'
22
- when /\A2.4/ then '~> 0.18.5'
23
- else '~> 0.19'
24
- end
10
+ gem "u-case", "~> 4.5", ">= 4.5.1"
25
11
 
26
12
  group :test do
27
- if activemodel_version
28
- gem 'activesupport', activemodel, require: false
29
- gem 'activemodel', activemodel, require: false
30
- gem 'minitest', activemodel_version < '4.1' ? '~> 4.2' : '~> 5.0'
31
- else
32
- gem 'minitest', '~> 5.0'
33
- end
34
-
35
- gem 'simplecov', simplecov_version, require: false
13
+ gem "logger"
14
+ gem "stringio"
15
+ gem "minitest", "~> 5.0"
16
+ gem "ostruct", "~> 0.6.3" if RUBY_VERSION >= "3.5"
17
+ gem "simplecov", "~> 0.22.0", require: false
36
18
  end
37
-
38
- gem 'rake', '~> 13.0'
39
-
40
- # Specify your gem's dependencies in u-attributes.gemspec
41
- gemspec
data/README.md CHANGED
@@ -1,28 +1,16 @@
1
1
  <p align="center">
2
- <img src="./assets/u-attributes_logo_v1.png" alt='Create "immutable" objects. No setters, just getters!'>
3
-
2
+ <h1 align="center" id="-attributes"><img src="./assets/u-attributes_logo_v1.png" alt="μ-attributes" height="60"></h1>
4
3
  <p align="center"><i>Create "immutable" objects with no setters, just getters.</i></p>
5
- <br>
6
- </p>
7
-
8
- <p align="center">
9
- <img src="https://img.shields.io/badge/ruby->%3D%202.2.0-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>
4
+ <p align="center">
5
+ <a href="https://badge.fury.io/rb/u-attributes"><img src="https://badge.fury.io/rb/u-attributes.svg" alt="Gem Version" height="18"></a>
6
+ <a href="https://github.com/serradura/u-attributes/actions/workflows/ci.yml"><img alt="Build Status" src="https://github.com/serradura/u-attributes/actions/workflows/ci.yml/badge.svg"></a>
7
+ <br/>
8
+ <a href="https://qlty.sh/gh/serradura/projects/u-attributes"><img src="https://qlty.sh/gh/serradura/projects/u-attributes/maintainability.svg" alt="Maintainability" /></a>
9
+ <a href="https://qlty.sh/gh/serradura/projects/u-attributes"><img src="https://qlty.sh/gh/serradura/projects/u-attributes/coverage.svg" alt="Code Coverage" /></a>
10
+ <br/>
11
+ <img src="https://img.shields.io/badge/Ruby%20%3E%3D%202.7%2C%20%3C%3D%20Head-ruby.svg?colorA=444&colorB=333" alt="Ruby">
12
+ <img src="https://img.shields.io/badge/Rails%20%3E%3D%206.0%2C%20%3C%3D%20Edge-rails.svg?colorA=444&colorB=333" alt="Rails">
13
+ </p>
26
14
  </p>
27
15
 
28
16
  This gem allows you to define "immutable" objects, when using it your objects will only have getters and no setters.
@@ -32,9 +20,9 @@ So, if you change [[1](#with_attribute)] [[2](#with_attributes)] an attribute of
32
20
 
33
21
  Version | Documentation
34
22
  ---------- | -------------
35
- unreleased | https://github.com/serradura/u-case/blob/main/README.md
36
- 2.7.0 | https://github.com/serradura/u-case/blob/v2.x/README.md
37
- 1.2.0 | https://github.com/serradura/u-case/blob/v1.x/README.md
23
+ unreleased | https://github.com/serradura/u-attributes/blob/main/README.md
24
+ 3.0.0 | https://github.com/serradura/u-attributes/blob/v3.x/README.md
25
+ 2.8.0 | https://github.com/serradura/u-attributes/blob/v2.x/README.md
38
26
 
39
27
  # Table of contents <!-- omit in toc -->
40
28
  - [Installation](#installation)
@@ -46,6 +34,8 @@ unreleased | https://github.com/serradura/u-case/blob/main/README.md
46
34
  - [Is it possible to define an attribute as required?](#is-it-possible-to-define-an-attribute-as-required)
47
35
  - [`Micro::Attributes#attribute`](#microattributesattribute)
48
36
  - [`Micro::Attributes#attribute!`](#microattributesattribute-1)
37
+ - [Attribute visibility (`private:`, `protected:`)](#attribute-visibility-private-protected)
38
+ - [Freezing attribute values (`freeze:`)](#freezing-attribute-values-freeze)
49
39
  - [How to define multiple attributes?](#how-to-define-multiple-attributes)
50
40
  - [`Micro::Attributes.with(:initialize)`](#microattributeswithinitialize)
51
41
  - [`#with_attribute()`](#with_attribute)
@@ -70,6 +60,7 @@ unreleased | https://github.com/serradura/u-case/blob/main/README.md
70
60
  - [`Micro::Attributes.without`](#microattributeswithout)
71
61
  - [Picking all the features](#picking-all-the-features)
72
62
  - [Extensions](#extensions)
63
+ - [Accept extension](#accept-extension)
73
64
  - [`ActiveModel::Validation` extension](#activemodelvalidation-extension)
74
65
  - [`.attribute()` options](#attribute-options)
75
66
  - [Diff extension](#diff-extension)
@@ -86,16 +77,29 @@ unreleased | https://github.com/serradura/u-case/blob/main/README.md
86
77
  Add this line to your application's Gemfile and `bundle install`:
87
78
 
88
79
  ```ruby
89
- gem 'u-attributes'
80
+ gem 'u-attributes', '~> 3.0'
90
81
  ```
91
82
 
92
83
  # Compatibility
93
84
 
94
- | u-attributes | branch | ruby | activemodel |
95
- | -------------- | ------- | -------- | ------------- |
96
- | unreleased | main | >= 2.2.0 | >= 3.2, < 7 |
97
- | 2.7.0 | v2.x | >= 2.2.0 | >= 3.2, < 7 |
98
- | 1.2.0 | v1.x | >= 2.2.0 | >= 3.2, < 6.1 |
85
+ | u-attributes | branch | ruby | activemodel |
86
+ | ---------------- | ------ | -------- | -------------- |
87
+ | unreleased | main | >= 2.7 | >= 6.0 |
88
+ | 3.0.0 | v3.x | >= 2.7 | >= 6.0 |
89
+ | 2.8.0 | v2.x | >= 2.2.0 | >= 3.2, <= 8.1 |
90
+
91
+ This library is tested (CI matrix) against:
92
+
93
+ | Ruby / Rails | 6.0 | 6.1 | 7.0 | 7.1 | 7.2 | 8.0 | 8.1 | Edge |
94
+ |--------------|-----|-----|-----|-----|-----|-----|-----|------|
95
+ | 2.7 | ✅ | ✅ | ✅ | ✅ | | | | |
96
+ | 3.0 | ✅ | ✅ | ✅ | ✅ | | | | |
97
+ | 3.1 | | | ✅ | ✅ | ✅ | | | |
98
+ | 3.2 | | | ✅ | ✅ | ✅ | ✅ | | |
99
+ | 3.3 | | | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
100
+ | 3.4 | | | | | ✅ | ✅ | ✅ | ✅ |
101
+ | 4.x | | | | | | | ✅ | ✅ |
102
+ | Head | | | | | | | ✅ | ✅ |
99
103
 
100
104
  > **Note**: The activemodel is an optional dependency, this module [can be enabled](#activemodelvalidation-extension) to validate the attributes.
101
105
 
@@ -157,8 +161,10 @@ person.name # John Doe
157
161
 
158
162
  #### How to extract attributes from an object or hash?
159
163
 
160
- You can extract attributes using the `extract_attributes_from` method, it will try to fetch attributes from the
161
- object using either the `object[attribute_key]` accessor or the reader method `object.attribute_key`.
164
+ You can extract attributes using the `extract_attributes_from` method. For each attribute name it
165
+ will first call the reader method (`object.attribute_key`) when available, and fall back to the
166
+ hash accessor (`object[attribute_key]`) otherwise. The reader method has priority because it lets
167
+ the source object encapsulate any computed/derived value.
162
168
 
163
169
  ```ruby
164
170
  class Person
@@ -251,6 +257,106 @@ person.attribute!('foo') { |value| value } # NameError (undefined attribute `foo
251
257
 
252
258
  [⬆️ Back to Top](#table-of-contents-)
253
259
 
260
+ ### Attribute visibility (`private:`, `protected:`)
261
+
262
+ By default every attribute reader is `public`. Use the `private: true` or `protected: true`
263
+ options to restrict the reader's visibility — useful for things like passwords, tokens, and any
264
+ internal value you don't want to expose on the public API.
265
+
266
+ Private/protected attributes are also excluded from the public attribute set (`#attributes`,
267
+ `.attributes`, `#attribute?`), so they don't leak through serialization or enumeration. To check
268
+ or fetch them explicitly, pass `true` as the second argument to `#attribute?` (or use
269
+ `#attribute!`).
270
+
271
+ ```ruby
272
+ require 'digest'
273
+
274
+ class User::SignUpParams
275
+ include Micro::Attributes.with(:initialize)
276
+
277
+ TrimString = ->(value) { String(value).strip }
278
+
279
+ attribute :email, default: TrimString
280
+ attributes :password, :password_confirmation, default: TrimString, private: true
281
+
282
+ def password_digest
283
+ return unless password == password_confirmation
284
+
285
+ Digest::SHA256.hexdigest(password)
286
+ end
287
+ end
288
+
289
+ User::SignUpParams.attributes # ["email", "password", "password_confirmation"]
290
+ User::SignUpParams.attributes_by_visibility # { public: ["email"], private: ["password", "password_confirmation"], protected: [] }
291
+
292
+ user = User::SignUpParams.new(
293
+ email: 'email@example.com',
294
+ password: 'secret',
295
+ password_confirmation: 'secret'
296
+ )
297
+
298
+ user.attributes # { "email" => "email@example.com" }
299
+
300
+ user.attribute?('email') # true
301
+ user.attribute?('password') # false (not in the public set)
302
+ user.attribute?('password', true) # true (use the second arg to look at all attributes)
303
+
304
+ user.attribute('password') # nil (returns nil instead of leaking the value)
305
+ user.attribute!('password') # NameError ("tried to access a private attribute `password")
306
+
307
+ user.password # NoMethodError (private method `password' called for ...)
308
+ ```
309
+
310
+ - `private:` and `protected:` map directly to Ruby's method-visibility semantics on the reader.
311
+ - The visibility configuration is preserved on inheritance.
312
+ - Works with the `:keys_as_symbol` extension (`attributes_by_visibility` will return the keys in
313
+ the configured type).
314
+
315
+ The class-level `attributes_by_visibility` method returns a hash with `:public`, `:private`, and
316
+ `:protected` keys so you can introspect how each attribute was declared.
317
+
318
+ [⬆️ Back to Top](#table-of-contents-)
319
+
320
+ ### Freezing attribute values (`freeze:`)
321
+
322
+ Use the `freeze:` option to make sure the value stored in the attribute can't be mutated after
323
+ the object is built. Three modes are supported:
324
+
325
+ | Value | Behavior |
326
+ | -------------- | --------------------------------------------------------------------- |
327
+ | `true` | Calls `value.freeze` on the incoming value. The original is frozen. |
328
+ | `:after_dup` | `value.dup.freeze` — freezes a shallow copy; the original stays free. |
329
+ | `:after_clone` | `value.clone.freeze` — same as above but uses `#clone` (preserves singleton methods, frozen state, tainted state, etc.). |
330
+
331
+ ```ruby
332
+ class Person
333
+ include Micro::Attributes.with(:initialize)
334
+
335
+ attribute :name, freeze: true
336
+ attribute :address, freeze: :after_dup
337
+ attribute :payload, freeze: :after_clone
338
+ end
339
+
340
+ raw_name = +"Rodrigo"
341
+
342
+ person = Person.new(
343
+ name: raw_name,
344
+ address: 'Av. Paulista',
345
+ payload: { id: 1 }
346
+ )
347
+
348
+ person.name.frozen? # true
349
+ raw_name.frozen? # true -> freeze: true mutates the original
350
+
351
+ person.address.frozen? # true
352
+ 'Av. Paulista'.frozen? # depends on the source string; the duplicate is what's frozen
353
+ ```
354
+
355
+ `freeze:` is applied after the default value resolution, so the frozen value reflects whatever
356
+ the attribute ends up holding (raw value, default, or callable-default result).
357
+
358
+ [⬆️ Back to Top](#table-of-contents-)
359
+
254
360
  ## How to define multiple attributes?
255
361
 
256
362
  Use `.attributes` with a list of attribute names.
@@ -272,7 +378,24 @@ person.name # nil
272
378
  person.age # 32
273
379
  ```
274
380
 
275
- > **Note:** This method can't define default values. To do this, use the `#attribute()` method.
381
+ You can also pass a trailing options hash and every attribute in the list will be declared with
382
+ those options. This is the canonical way to declare several attributes that share the same
383
+ configuration (default value, visibility, freezing, validations, etc.).
384
+
385
+ ```ruby
386
+ class User::SignUpParams
387
+ include Micro::Attributes.with(:initialize, :accept)
388
+
389
+ TrimString = ->(value) { String(value).strip }
390
+
391
+ attribute :email, default: TrimString
392
+ attributes :password, :password_confirmation, reject: :empty?, default: TrimString, private: true
393
+ end
394
+ ```
395
+
396
+ > **Note:** Unlike `.attribute`, this method accepts a shared options hash but defines all listed
397
+ > attributes with the same configuration. If you need different defaults/options per attribute,
398
+ > use `#attribute()` once per attribute.
276
399
 
277
400
  [⬆️ Back to Top](#table-of-contents-)
278
401
 
@@ -641,6 +764,19 @@ Micro::Attributes.without(:diff) # will load :activemodel_validations, :keys_as_
641
764
  Micro::Attributes.without(initialize: :strict) # will load :activemodel_validations, :diff and :keys_as_symbol
642
765
  ```
643
766
 
767
+ You can also pair `:accept` with any other feature, and switch into strict mode by passing the
768
+ hash form `accept: :strict`:
769
+
770
+ ```ruby
771
+ Micro::Attributes.with(:accept)
772
+
773
+ Micro::Attributes.with(:accept, :diff, :initialize)
774
+
775
+ Micro::Attributes.with(:accept, :activemodel_validations, :diff, :keys_as_symbol)
776
+
777
+ Micro::Attributes.with(:diff, :keys_as_symbol, initialize: :strict, accept: :strict)
778
+ ```
779
+
644
780
  ## Picking all the features
645
781
 
646
782
  ```ruby
@@ -648,13 +784,167 @@ Micro::Attributes.with_all_features
648
784
 
649
785
  # This method returns the same of:
650
786
 
651
- Micro::Attributes.with(:activemodel_validations, :diff, :keys_as_symbol, initialize: :strict)
787
+ Micro::Attributes.with(:accept, :activemodel_validations, :diff, :keys_as_symbol, initialize: :strict)
652
788
  ```
653
789
 
654
790
  [⬆️ Back to Top](#table-of-contents-)
655
791
 
656
792
  ## Extensions
657
793
 
794
+ ### Accept extension
795
+
796
+ The `:accept` extension adds a lightweight, dependency-free validation mechanism. Use the
797
+ `accept:` / `reject:` options on an attribute to validate the assigned value, and inspect the
798
+ result through `#attributes_errors`, `#accepted_attributes`, and `#rejected_attributes`.
799
+
800
+ ```ruby
801
+ class User
802
+ include Micro::Attributes.with(:initialize, :accept)
803
+
804
+ attribute :age, accept: Integer, allow_nil: true
805
+ attribute :name, accept: -> v { v.is_a?(String) && !v.empty? }, default: 'John Doe'
806
+ attribute :email, accept: :present?
807
+ end
808
+
809
+ user = User.new({})
810
+
811
+ user.attributes_errors? # false
812
+ user.accepted_attributes? # true
813
+ user.rejected_attributes? # false
814
+
815
+ User.new(age: 'twenty', email: nil).tap do |bad|
816
+ bad.attributes_errors? # true
817
+ bad.attributes_errors # { "age" => "expected to be a kind of Integer", "email" => "expected to be present?" }
818
+ bad.accepted_attributes # ["name"]
819
+ bad.rejected_attributes # ["age", "email"]
820
+ end
821
+ ```
822
+
823
+ #### What can `accept:` / `reject:` receive?
824
+
825
+ | Type | `accept:` means | `reject:` means |
826
+ | --------------- | -------------------------------------------- | ------------------------------------------------ |
827
+ | `Class`/`Module`| `value.kind_of?(expected)` must be true | `value.kind_of?(expected)` must be false |
828
+ | Predicate `:sym?` (ends with `?`) | `value.public_send(:sym?)` must be true | `value.public_send(:sym?)` must be false |
829
+ | Anything callable (proc, lambda, object responding to `#call`) | result of `expected.call(value)` must be truthy | result of `expected.call(value)` must be falsy |
830
+
831
+ Default rejection messages follow the pattern below; you can override them with
832
+ `rejection_message:` (see further down).
833
+
834
+ ```ruby
835
+ attribute :name, accept: :present? # "expected to be present?"
836
+ attribute :name, reject: :empty? # "expected to not be empty?"
837
+ attribute :name, accept: String # "expected to be a kind of String"
838
+ attribute :name, reject: String # "expected to not be a kind of String"
839
+ attribute :name, accept: ->(v) { v } # "is invalid"
840
+ ```
841
+
842
+ #### `allow_nil:` option
843
+
844
+ Skip validation when the incoming value is `nil`.
845
+
846
+ ```ruby
847
+ class User
848
+ include Micro::Attributes.with(:initialize, :accept)
849
+
850
+ attribute :age, accept: Integer, allow_nil: true
851
+ end
852
+
853
+ User.new(age: nil).attributes_errors? # false
854
+ User.new(age: 21).attributes_errors? # false
855
+ User.new(age: 'x').attributes_errors? # true
856
+ ```
857
+
858
+ #### `rejection_message:` option
859
+
860
+ Customize the error message either with a String or with a callable. A callable receives the
861
+ attribute name as its first argument, so the same builder can be reused across attributes (handy
862
+ for i18n).
863
+
864
+ ```ruby
865
+ class User
866
+ include Micro::Attributes.with(:initialize, :accept)
867
+
868
+ attribute :name, accept: String, rejection_message: 'must be a string'
869
+ attribute :age, accept: Integer, rejection_message: ->(key) { "#{key} must be an integer" }
870
+ end
871
+
872
+ User.new(name: 1, age: 'x').attributes_errors
873
+ # => { "name" => "must be a string", "age" => "age must be an integer" }
874
+ ```
875
+
876
+ Callable validators can also expose a `#rejection_message` method themselves, and it will be used
877
+ as the default message for that validator:
878
+
879
+ ```ruby
880
+ class FilledString
881
+ def call(value)
882
+ value.is_a?(String) && !value.empty?
883
+ end
884
+
885
+ def rejection_message
886
+ ->(key) { "#{key} can't be an empty string" }
887
+ end
888
+ end
889
+
890
+ class User
891
+ include Micro::Attributes.with(:initialize, :accept)
892
+
893
+ attribute :name, accept: FilledString.new
894
+ end
895
+ ```
896
+
897
+ #### Strict mode (`accept: :strict`)
898
+
899
+ Use `Micro::Attributes.with(accept: :strict)` to raise as soon as any attribute is rejected,
900
+ instead of collecting errors silently.
901
+
902
+ ```ruby
903
+ class User
904
+ include Micro::Attributes.with(initialize: :strict, accept: :strict)
905
+
906
+ attribute :age, accept: Integer
907
+ attribute :name, accept: ->(v) { v.is_a?(String) && !v.empty? }, default: 'John doe'
908
+ end
909
+
910
+ User.new(age: 'x', name: nil)
911
+ # ArgumentError:
912
+ # One or more attributes were rejected. Errors:
913
+ # * :age expected to be a kind of Integer
914
+ # * :name is invalid
915
+ ```
916
+
917
+ #### Interaction with other features
918
+
919
+ - Validation runs **after** the default value resolution, so defaults are validated like any
920
+ regular value.
921
+ - When combined with the [ActiveModel::Validation extension](#activemodelvalidation-extension),
922
+ the `:accept` checks run first; AM validations only run if every attribute is accepted.
923
+ - `accept:` plays nicely with [`freeze:`](#freezing-attribute-values-freeze) and
924
+ [`private:`/`protected:`](#attribute-visibility-private-protected). See the combined example
925
+ below.
926
+
927
+ ```ruby
928
+ require 'digest'
929
+
930
+ class User::SignUpParams
931
+ include Micro::Attributes.with(:initialize, accept: :strict)
932
+
933
+ TrimString = ->(value) { String(value).strip }
934
+
935
+ attribute :email, default: TrimString,
936
+ accept: ->(s) { s =~ /\A.+@.+\..+\z/ }, freeze: :after_dup
937
+ attributes :password, :password_confirmation, default: TrimString,
938
+ reject: :empty?, private: true
939
+
940
+ def password_digest
941
+ Digest::SHA256.hexdigest(password) if password == password_confirmation
942
+ end
943
+ end
944
+ ```
945
+
946
+ [⬆️ Back to Top](#table-of-contents-)
947
+
658
948
  ### `ActiveModel::Validation` extension
659
949
 
660
950
  If your application uses ActiveModel as a dependency (like a regular Rails app). You will be enabled to use the `activemodel_validations` extension.
data/Rakefile CHANGED
@@ -7,4 +7,33 @@ Rake::TestTask.new(:test) do |t|
7
7
  t.test_files = FileList['test/**/*_test.rb']
8
8
  end
9
9
 
10
- task :default => :test
10
+ require 'appraisal/task'
11
+
12
+ Appraisal::Task.new
13
+
14
+ desc 'Run the full test suite against every supported Rails version'
15
+ task :matrix do
16
+ appraisals =
17
+ if RUBY_VERSION < '3.1'
18
+ %w[rails-6-0 rails-6-1 rails-7-0 rails-7-1]
19
+ elsif RUBY_VERSION < '3.2'
20
+ %w[rails-7-0 rails-7-1 rails-7-2]
21
+ elsif RUBY_VERSION < '3.3'
22
+ %w[rails-7-0 rails-7-1 rails-7-2 rails-8-0]
23
+ elsif RUBY_VERSION < '3.4'
24
+ %w[rails-7-0 rails-7-1 rails-7-2 rails-8-0 rails-8-1 rails-edge]
25
+ elsif RUBY_VERSION < '4.0'
26
+ %w[rails-7-2 rails-8-0 rails-8-1 rails-edge]
27
+ else
28
+ %w[rails-8-1 rails-edge]
29
+ end
30
+
31
+ # Run the no-activemodel baseline first
32
+ sh 'bundle exec rake test'
33
+
34
+ appraisals.each do |appraisal|
35
+ sh "bundle exec appraisal #{appraisal} rake test"
36
+ end
37
+ end
38
+
39
+ task default: :test
data/bin/matrix ADDED
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ ruby -v
7
+
8
+ rm -f Gemfile.lock
9
+
10
+ bundle install
11
+
12
+ bundle exec appraisal update
13
+
14
+ bundle exec rake matrix
15
+
16
+ ruby -v
data/bin/setup CHANGED
@@ -3,6 +3,10 @@ set -euo pipefail
3
3
  IFS=$'\n\t'
4
4
  set -vx
5
5
 
6
+ rm -f Gemfile.lock
7
+
6
8
  bundle install
7
9
 
10
+ bundle exec appraisal install
11
+
8
12
  # Do any other automated setup that you need to do here
@@ -0,0 +1,15 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "rake", "~> 13.0"
6
+ gem "u-case", "~> 4.5", ">= 4.5.1"
7
+
8
+ group :test do
9
+ gem "minitest", "~> 6.0"
10
+ gem "simplecov", "~> 0.22.0", require: false
11
+ gem "ostruct", "~> 0.6.3"
12
+ gem "activemodel", "~> 8.1.0"
13
+ end
14
+
15
+ gemspec path: "../"
@@ -0,0 +1,15 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "rake", "~> 13.0"
6
+ gem "u-case", "~> 4.5", ">= 4.5.1"
7
+
8
+ group :test do
9
+ gem "minitest", "~> 6.0"
10
+ gem "simplecov", "~> 0.22.0", require: false
11
+ gem "ostruct", "~> 0.6.3"
12
+ gem "activemodel", branch: "main", git: "https://github.com/rails/rails"
13
+ end
14
+
15
+ gemspec path: "../"
@@ -35,10 +35,10 @@ module Micro::Attributes
35
35
 
36
36
  KeepProc = -> validation_data { validation_data[0] == :accept && validation_data[1] == Proc }
37
37
 
38
- def __attribute_assign(key, initialize_value, attribute_data)
38
+ def __attribute_assign(key, init_hash, attribute_data)
39
39
  validation_data = attribute_data[1]
40
40
 
41
- value_to_assign = FetchValueToAssign.(initialize_value, attribute_data, KeepProc.(validation_data))
41
+ value_to_assign = FetchValueToAssign.(init_hash, init_hash[key], attribute_data, KeepProc.(validation_data))
42
42
 
43
43
  value = __attributes[key] = instance_variable_set("@#{key}", value_to_assign)
44
44
 
@@ -179,6 +179,16 @@ module Micro
179
179
  __attribute_assign(name, false, options)
180
180
  end
181
181
 
182
+ RaiseKindError = ->(expected, given) do
183
+ if (util = Kind.const_get(:KIND, false)) && util.respond_to?(:error!)
184
+ util.error!(expected, given)
185
+ else
186
+ raise Kind::Error.new(expected, given, label: nil)
187
+ end
188
+ end
189
+
190
+ private_constant :RaiseKindError
191
+
182
192
  def attributes(*args)
183
193
  return __attributes.to_a if args.empty?
184
194
 
@@ -191,7 +201,7 @@ module Micro
191
201
  if arg.is_a?(String) || arg.is_a?(Symbol)
192
202
  __attribute_assign(arg, false, options)
193
203
  else
194
- Kind::KIND.error!('String/Symbol'.freeze, arg)
204
+ RaiseKindError.call('String/Symbol'.freeze, arg)
195
205
  end
196
206
  end
197
207
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Micro
4
4
  module Attributes
5
- VERSION = '2.7.0'.freeze
5
+ VERSION = '3.0.0'.freeze
6
6
  end
7
7
  end
@@ -123,12 +123,16 @@ module Micro
123
123
  @__attributes ||= {}
124
124
  end
125
125
 
126
- FetchValueToAssign = -> (value, attribute_data, keep_proc = false) do
126
+ FetchValueToAssign = -> (init_hash, value, attribute_data, keep_proc = false) do
127
127
  default = attribute_data[0]
128
128
 
129
129
  value_to_assign =
130
130
  if default.is_a?(Proc) && !keep_proc
131
- default.arity > 0 ? default.call(value) : default.call
131
+ case default.arity
132
+ when 0 then default.call
133
+ when 2 then default.call(value, init_hash)
134
+ else default.call(value)
135
+ end
132
136
  else
133
137
  value.nil? ? default : value
134
138
  end
@@ -143,14 +147,14 @@ module Micro
143
147
 
144
148
  def __attributes_assign(hash)
145
149
  self.class.__attributes_data__.each do |name, attribute_data|
146
- __attribute_assign(name, hash[name], attribute_data) if attribute?(name, true)
150
+ __attribute_assign(name, hash, attribute_data) if attribute?(name, true)
147
151
  end
148
152
 
149
153
  __attributes.freeze
150
154
  end
151
155
 
152
- def __attribute_assign(name, initialize_value, attribute_data)
153
- value_to_assign = FetchValueToAssign.(initialize_value, attribute_data)
156
+ def __attribute_assign(name, init_hash, attribute_data)
157
+ value_to_assign = FetchValueToAssign.(init_hash, init_hash[name], attribute_data)
154
158
 
155
159
  ivar_value = instance_variable_set("@#{name}", value_to_assign)
156
160
 
data/u-attributes.gemspec CHANGED
@@ -25,10 +25,11 @@ Gem::Specification.new do |spec|
25
25
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
26
26
  spec.require_paths = ['lib']
27
27
 
28
- spec.required_ruby_version = '>= 2.2.0'
28
+ spec.required_ruby_version = '>= 2.7.0'
29
29
 
30
30
  spec.add_runtime_dependency 'kind', '>= 4.0', '< 6.0'
31
31
 
32
+ spec.add_development_dependency 'appraisal', '~> 2.5'
32
33
  spec.add_development_dependency 'bundler'
33
34
  spec.add_development_dependency 'rake', '~> 13.0'
34
35
  end
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: u-attributes
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.7.0
4
+ version: 3.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Rodrigo Serradura
8
- autorequire:
9
8
  bindir: exe
10
9
  cert_chain: []
11
- date: 2021-02-22 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: kind
@@ -30,6 +29,20 @@ dependencies:
30
29
  - - "<"
31
30
  - !ruby/object:Gem::Version
32
31
  version: '6.0'
32
+ - !ruby/object:Gem::Dependency
33
+ name: appraisal
34
+ requirement: !ruby/object:Gem::Requirement
35
+ requirements:
36
+ - - "~>"
37
+ - !ruby/object:Gem::Version
38
+ version: '2.5'
39
+ type: :development
40
+ prerelease: false
41
+ version_requirements: !ruby/object:Gem::Requirement
42
+ requirements:
43
+ - - "~>"
44
+ - !ruby/object:Gem::Version
45
+ version: '2.5'
33
46
  - !ruby/object:Gem::Dependency
34
47
  name: bundler
35
48
  requirement: !ruby/object:Gem::Requirement
@@ -67,16 +80,20 @@ executables: []
67
80
  extensions: []
68
81
  extra_rdoc_files: []
69
82
  files:
83
+ - ".github/workflows/ci.yml"
70
84
  - ".gitignore"
71
- - ".travis.sh"
72
- - ".travis.yml"
85
+ - ".vscode/settings.json"
86
+ - Appraisals
73
87
  - CODE_OF_CONDUCT.md
74
88
  - Gemfile
75
89
  - LICENSE.txt
76
90
  - README.md
77
91
  - Rakefile
78
92
  - bin/console
93
+ - bin/matrix
79
94
  - bin/setup
95
+ - gemfiles/rails_8_1.gemfile
96
+ - gemfiles/rails_edge.gemfile
80
97
  - lib/micro/attributes.rb
81
98
  - lib/micro/attributes/diff.rb
82
99
  - lib/micro/attributes/features.rb
@@ -91,13 +108,11 @@ files:
91
108
  - lib/micro/attributes/utils.rb
92
109
  - lib/micro/attributes/version.rb
93
110
  - lib/u-attributes.rb
94
- - test.sh
95
111
  - u-attributes.gemspec
96
112
  homepage: https://github.com/serradura/u-attributes
97
113
  licenses:
98
114
  - MIT
99
115
  metadata: {}
100
- post_install_message:
101
116
  rdoc_options: []
102
117
  require_paths:
103
118
  - lib
@@ -105,15 +120,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
105
120
  requirements:
106
121
  - - ">="
107
122
  - !ruby/object:Gem::Version
108
- version: 2.2.0
123
+ version: 2.7.0
109
124
  required_rubygems_version: !ruby/object:Gem::Requirement
110
125
  requirements:
111
126
  - - ">="
112
127
  - !ruby/object:Gem::Version
113
128
  version: '0'
114
129
  requirements: []
115
- rubygems_version: 3.1.4
116
- signing_key:
130
+ rubygems_version: 4.0.7
117
131
  specification_version: 4
118
132
  summary: Create "immutable" objects with no setters, just getters.
119
133
  test_files: []
data/.travis.sh DELETED
@@ -1,45 +0,0 @@
1
- #!/bin/bash
2
-
3
- RUBY_V=$(ruby -v)
4
-
5
- function run_with_bundler {
6
- rm Gemfile.lock
7
-
8
- if [ ! -z "$1" ]; then
9
- bundle_cmd="bundle _$1_"
10
- else
11
- bundle_cmd="bundle"
12
- fi
13
-
14
- eval "$2 $bundle_cmd update"
15
- eval "$2 $bundle_cmd exec rake test"
16
- }
17
-
18
- function run_with_am_version_and_bundler {
19
- run_with_bundler "$2" "ACTIVEMODEL_VERSION=$1"
20
- }
21
-
22
- RUBY_2_2345="ruby 2.[2345]."
23
-
24
- if [[ $RUBY_V =~ $RUBY_2_2345 ]]; then
25
- run_with_bundler "$BUNDLER_V1"
26
-
27
- run_with_am_version_and_bundler "3.2" "$BUNDLER_V1"
28
- run_with_am_version_and_bundler "4.0" "$BUNDLER_V1"
29
- run_with_am_version_and_bundler "4.1" "$BUNDLER_V1"
30
- run_with_am_version_and_bundler "4.2" "$BUNDLER_V1"
31
- run_with_am_version_and_bundler "5.0" "$BUNDLER_V1"
32
- run_with_am_version_and_bundler "5.1" "$BUNDLER_V1"
33
- run_with_am_version_and_bundler "5.2" "$BUNDLER_V1"
34
- fi
35
-
36
- RUBY_2_567="ruby 2.[567]."
37
- RUBY_3_x_x="ruby 3.0."
38
-
39
- if [[ $RUBY_V =~ $RUBY_2_567 ]] || [[ $RUBY_V =~ $RUBY_3_x_x ]]; then
40
- gem install bundler -v ">= 2" --no-doc
41
-
42
- run_with_bundler
43
- run_with_am_version_and_bundler "6.0"
44
- run_with_am_version_and_bundler "6.1"
45
- fi
data/.travis.yml DELETED
@@ -1,33 +0,0 @@
1
- language: ruby
2
-
3
- cache:
4
- bundler: true
5
- directories:
6
- - /home/travis/.rvm/
7
-
8
- rvm:
9
- - 2.2.2
10
- - 2.3.0
11
- - 2.4.0
12
- - 2.5.0
13
- - 2.6.0
14
- - 2.7.0
15
- - 3.0.0
16
-
17
- env:
18
- - BUNDLER_V1="1.17.3"
19
-
20
- before_install:
21
- - gem install bundler -v "$BUNDLER_V1"
22
-
23
- install: bundle install --jobs=3 --retry=3
24
-
25
- before_script:
26
- - curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter
27
- - chmod +x ./cc-test-reporter
28
- - "./cc-test-reporter before-build"
29
-
30
- script: "./.travis.sh"
31
-
32
- after_success:
33
- - "./cc-test-reporter after-build -t simplecov"
data/test.sh DELETED
@@ -1,11 +0,0 @@
1
- #!/bin/bash
2
-
3
- source $(dirname $0)/.travis.sh
4
-
5
- echo ''
6
- echo 'Resetting Gemfile'
7
- echo ''
8
-
9
- rm Gemfile.lock
10
-
11
- bundle