u-attributes 2.8.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: 75a44be0f1b71d41abdfd3e9c81f67527760a4a8f0202818fd7f04782680922b
4
- data.tar.gz: bbb26faf04daffd2e92b18eb4e363d85c7677ed5ed3fb09bda485d8b34c8296d
3
+ metadata.gz: '01848f333334d9af69fbd608407e9e287ba9411305c2307fba64bb5bd85d076c'
4
+ data.tar.gz: f1778463cfdeeb64595a2a803fe93b720f587b65c62a95abf4693d7a10227330
5
5
  SHA512:
6
- metadata.gz: 9e7da00d4786086edc9607533ef9c9632b10dd2200ddee29a46551d311c8e5ee8c5549d161f8ea6881c95c529abbdd4a2c48fd28635464606a7877b0d94a6488
7
- data.tar.gz: aa42ef9c78b7f60514ef062b78d1b6f4eb3589ce1d39ab13d6459d2a164e3dcfa8b4f54660598df76afe4b42b8087cf9060f57398609f59508952bfd49c0cda1
6
+ metadata.gz: 1bb8672adcf5c2221ead65d0fbcb1523a74d960c20056c8e989404d63a6941ed5bb0a51a3e4acae4dc635ad24ff5e446ba349cc65b5e6b38a9093f51ea93b50c
7
+ data.tar.gz: 135eb428c82e85a8a5a197c3f9e1170df2c9dcd33aa6e89162a292c79055299bf5db614497dbc51fb926ba16dec2028a5791e5d3faf9103f46746d9c059db0ae
@@ -1,27 +1,66 @@
1
+ name: Ruby
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - main
7
+
8
+ pull_request:
1
9
 
2
- name: build
3
- on: [push]
4
10
  jobs:
5
11
  test:
6
12
  runs-on: ubuntu-latest
13
+ name: Ruby ${{ matrix.ruby }}
14
+ permissions:
15
+ contents: read
7
16
  strategy:
17
+ fail-fast: false
8
18
  matrix:
9
- ruby: [2.2, 2.3, 2.4, 2.5, 2.6, 3.0]
19
+ ruby: ["2.7", "3.0", "3.1", "3.2", "3.3", "3.4", "4.0", head]
10
20
  steps:
11
- - uses: actions/checkout@v2
12
- - uses: ruby/setup-ruby@v1
21
+ - uses: actions/checkout@v4
22
+ with:
23
+ persist-credentials: false
24
+ - name: Set up Ruby
25
+ uses: ruby/setup-ruby@v1
13
26
  with:
14
27
  ruby-version: ${{ matrix.ruby }}
15
- bundler-cache: true
16
- - name: Test and generate coverage
17
- run: bin/test
18
- - name: Format coverage
19
- if: ${{ matrix.ruby >= 3 }}
20
- run: bin/prepare_coverage
21
- - uses: paambaati/codeclimate-action@v2.7.5
22
- if: ${{ matrix.ruby >= 3 }}
23
- env:
24
- CC_TEST_REPORTER_ID: 9015766fe58e0abd5ab58050ddfc67708e0b11430b3d3999ac17139ffcb4d3e7
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 }}
25
64
  with:
26
- debug: true
27
- coverageLocations: coverage/.resultset.json:simplecov
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
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.5', '>= 4.5.1'
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.21.2'
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://github.com/serradura/u-attributes/actions/workflows/ci.yml">
16
- <img alt="Build Status" src="https://github.com/serradura/u-attributes/actions/workflows/ci.yml/badge.svg">
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.8.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.8.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: "../"
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Micro
4
4
  module Attributes
5
- VERSION = '2.8.0'.freeze
5
+ VERSION = '3.0.0'.freeze
6
6
  end
7
7
  end
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.8.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-08-24 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
@@ -70,15 +83,17 @@ files:
70
83
  - ".github/workflows/ci.yml"
71
84
  - ".gitignore"
72
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
79
- - bin/prepare_coverage
93
+ - bin/matrix
80
94
  - bin/setup
81
- - bin/test
95
+ - gemfiles/rails_8_1.gemfile
96
+ - gemfiles/rails_edge.gemfile
82
97
  - lib/micro/attributes.rb
83
98
  - lib/micro/attributes/diff.rb
84
99
  - lib/micro/attributes/features.rb
@@ -98,7 +113,6 @@ homepage: https://github.com/serradura/u-attributes
98
113
  licenses:
99
114
  - MIT
100
115
  metadata: {}
101
- post_install_message:
102
116
  rdoc_options: []
103
117
  require_paths:
104
118
  - lib
@@ -106,15 +120,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
106
120
  requirements:
107
121
  - - ">="
108
122
  - !ruby/object:Gem::Version
109
- version: 2.2.0
123
+ version: 2.7.0
110
124
  required_rubygems_version: !ruby/object:Gem::Requirement
111
125
  requirements:
112
126
  - - ">="
113
127
  - !ruby/object:Gem::Version
114
128
  version: '0'
115
129
  requirements: []
116
- rubygems_version: 3.2.17
117
- signing_key:
130
+ rubygems_version: 4.0.7
118
131
  specification_version: 4
119
132
  summary: Create "immutable" objects with no setters, just getters.
120
133
  test_files: []
data/bin/prepare_coverage DELETED
@@ -1,27 +0,0 @@
1
- #!/usr/bin/env ruby
2
- # frozen_string_literal: true
3
- # Borrowed from https://gist.github.com/qortex/7e7c49f3731391a91ee898336183acef
4
-
5
- # Temporary hack to get CodeClimate to work with SimpleCov 0.18 JSON format until issue is fixed
6
- # upstream: https://github.com/codeclimate/test-reporter/issues/413
7
-
8
- require "json"
9
-
10
- filename = "coverage/.resultset.json"
11
- contents = JSON.parse(File.read(filename))
12
-
13
- def remove_lines_key(obj)
14
- case obj
15
- when Hash
16
- obj.transform_values do |val|
17
- val.is_a?(Hash) && val.key?("lines") ? val["lines"] : remove_lines_key(val)
18
- end
19
- else
20
- obj
21
- end
22
- end
23
-
24
- # overwrite
25
- File.write(filename, JSON.generate(remove_lines_key(contents)))
26
-
27
- puts Dir['coverage/.*.json']
data/bin/test DELETED
@@ -1,48 +0,0 @@
1
- #!/bin/bash
2
-
3
- set -e
4
-
5
- RUBY_V=$(ruby -v)
6
-
7
- function reset_gemfile_and_test {
8
- rm Gemfile.lock
9
-
10
- eval "$1 bundle update"
11
- eval "$1 bundle exec rake test"
12
- }
13
-
14
- function test_with_activemodel {
15
- reset_gemfile_and_test "ACTIVEMODEL_VERSION=$1"
16
- }
17
-
18
- RUBY_2_2345="ruby 2.[2345]."
19
- RUBY_2_234="ruby 2.[234]."
20
- RUBY_2_567="ruby 2.[567]."
21
- RUBY_2_2="ruby 2.2."
22
- RUBY_3_X="ruby 3.0."
23
-
24
- if [[ $RUBY_V =~ $RUBY_2_2345 ]]; then
25
- if [[ $RUBY_V =~ $RUBY_2_234 ]]; then
26
- reset_gemfile_and_test
27
- fi
28
-
29
- if [[ $RUBY_V =~ $RUBY_2_2 ]]; then
30
- test_with_activemodel "3.2"
31
- fi
32
-
33
- test_with_activemodel "4.0"
34
- test_with_activemodel "4.1"
35
- test_with_activemodel "4.2"
36
- test_with_activemodel "5.0"
37
- test_with_activemodel "5.1"
38
- test_with_activemodel "5.2"
39
- fi
40
-
41
- if [[ $RUBY_V =~ $RUBY_2_567 ]] || [[ $RUBY_V =~ $RUBY_3_X ]]; then
42
- gem install bundler -v ">= 2" --no-doc
43
-
44
- reset_gemfile_and_test
45
-
46
- test_with_activemodel "6.0"
47
- test_with_activemodel "6.1"
48
- fi