errgonomic 0.10.2 → 0.11.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 +4 -4
- data/CHANGELOG.md +13 -0
- data/README.md +1 -1
- data/doctest_helper.rb +5 -0
- data/lib/errgonomic/rails/active_record_delegate_optional.rb +20 -1
- data/lib/errgonomic/rails/active_record_optional.rb +29 -4
- data/lib/errgonomic/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 94eb90cd1a2a6e07d42a5bc9318fc34cea84cff058c6d78299c2aa1673279b3e
|
|
4
|
+
data.tar.gz: c8516b096821a7a1902805687a51b7f07f1443adeccf50f3b85659006f431b61
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4a3803504c4789e6bf2df90f28348e90f72089121e2a2b06f2ba8ec28db7082286447ff07ca6c2c23cd5a41e38d86e31b454237ae3d2299ed4d1f97d0b57518a
|
|
7
|
+
data.tar.gz: c868b2d29da869091801aac2b5295b5a69a67ac405a9a14d38e62021048ff5133dfd288d61caadbfb783f512da3cd409fda19453c55a13610a8525af6a2b46fb
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [0.11.0] - 2026-09-16
|
|
4
|
+
|
|
5
|
+
This release makes a converted model's query methods and a delegated predicate answer the verdict a boolean caller asked for, rather than something an `if` reads as true.
|
|
6
|
+
|
|
7
|
+
### Upgrading from 0.10.2
|
|
8
|
+
|
|
9
|
+
An application that prepended its own `query_attribute` unwrap to work around a wrapped column's `attr?` answering true for `false`, `0` or `""` can delete it: the concern does it now, and a prepended copy is redundant rather than harmful. A `delegate_optional` whose method name ends in `?` returns `true` or `false` where it returned an Option, so `some?`, `unwrap_or` and the other combinators on that result are a `NoMethodError`. Read the boolean instead. The one loss is a `?` method that answers a value by design, which now answers `true`: declare it without the `?`, or write `some_and?` by hand. The seam to watch is `Object#to_option`, which lifts whatever it is handed, so `record.delegated?.to_option` is `Some(false)` for an absent target where it was `None`.
|
|
10
|
+
|
|
11
|
+
### Changes
|
|
12
|
+
|
|
13
|
+
- [Bug fix] A converted model's `query_attribute`, its private `attribute?` alias and the `attr?` methods Rails generates from them answer what the unconverted model answers for a wrapped column, so `note.pinned?` is `false` for an explicit `false` and `note.rank?` is `false` for a `0`. The concern overrides `query_attribute` to unwrap the reader's Option before handing the value to Rails' own cast. Previously the cast saw a `Some`, missed its `true` and `false, nil` arms and fell through to `!blank?`, where `Option#blank?` is `none?`, so every present value answered true ([#88](https://github.com/omc/errgonomic/issues/88))
|
|
14
|
+
- [Behavior change] `delegate_optional` with a method name ending in `?` answers `true` or `false` through `some_and?`, reading an absent target as `false`, where it answered `Some(true)`, `Some(false)` and `None` before. A `?` name asks for a verdict and no Option can serve as one, because every Option is truthy: `Some(false)` and `None` both took the true branch of an `if`, which is the silent wrong branch that `delegate ..., allow_nil: true` does not have. Every other name is lifted into an Option as before, and there is no opt-out keyword ([#89](https://github.com/omc/errgonomic/issues/89))
|
|
15
|
+
|
|
3
16
|
## [0.10.2] - 2026-09-10
|
|
4
17
|
|
|
5
18
|
This release makes every `Err` carry an error and removes `Option#ok`, the one method that built an `Err` without one.
|
data/README.md
CHANGED
|
@@ -403,7 +403,7 @@ The declaration reads as well above the include as below it, as `errgonomic_opti
|
|
|
403
403
|
|
|
404
404
|
`Model.errgonomic_optionals` reports which readers a model wrapped, including nullable foreign-key columns, so `book.author_id` is `Some(1)` alongside `book.author`. That is how to check that a conversion did what it meant to. A subclass reports what it inherited alongside anything it wrapped itself, so the report names every wrapped reader the class responds to; `Model.errgonomic_optional_names` is the set that class wrapped on its own.
|
|
405
405
|
|
|
406
|
-
`delegate_optional` is Rails' `delegate` with `allow_nil`, where the absent case is a `None` rather than a `nil`: `delegate_optional :name, to: :author` gives `book.name # => Some('Cixin Liu')`, and `None()` where there is no author. The prefix forms are Rails': `prefix: true` names the reader after the target (`author_name`), and `prefix: :writer` names it `writer_name`. `private: true` works as it does there. The reader forwards whatever it was called with, arguments and block alike. `allow_nil: true` is accepted and says nothing new, so a `delegate` declaration swaps over unchanged unless it delegates a writer. `delegate_optional :name=, to: :author` raises instead: an assignment through an absent target has nowhere to put the value, and dropping it silently is what the type is there to prevent. `allow_nil: false` asks for a reader that raises on absence, which this does not have, so it raises `ArgumentError` where it is written, as a declaration with no `to:` does.
|
|
406
|
+
`delegate_optional` is Rails' `delegate` with `allow_nil`, where the absent case is a `None` rather than a `nil`: `delegate_optional :name, to: :author` gives `book.name # => Some('Cixin Liu')`, and `None()` where there is no author. The prefix forms are Rails': `prefix: true` names the reader after the target (`author_name`), and `prefix: :writer` names it `writer_name`. `private: true` works as it does there. The reader forwards whatever it was called with, arguments and block alike. `allow_nil: true` is accepted and says nothing new, so a `delegate` declaration swaps over unchanged unless it delegates a writer or a predicate. A name ending in `?` asks for a verdict, and no Option can serve as one, because every Option is truthy. So a predicate answers a bare `true` or `false` through `some_and?`: `delegate_optional :prolific?, to: :author` gives `book.prolific? # => false` for an unprolific author, and `false` again where there is no author, which branches an `if` the way Rails' `allow_nil: true` does. The one thing that costs is a `?` method that answers a value by design, which now answers `true`; declare it without the `?`, or write `some_and?` by hand. `delegate_optional :name=, to: :author` raises instead: an assignment through an absent target has nowhere to put the value, and dropping it silently is what the type is there to prevent. `allow_nil: false` asks for a reader that raises on absence, which this does not have, so it raises `ArgumentError` where it is written, as a declaration with no `to:` does.
|
|
407
407
|
|
|
408
408
|
It is available on every model, converted or not, because it lifts both ends one layer. The target is lifted, so a plain record reads as `Some` and a `nil` as `None`. What the delegated call returns is lifted too, so a delegated reader that is itself an Option comes back as one Option rather than two.
|
|
409
409
|
|
data/doctest_helper.rb
CHANGED
|
@@ -76,6 +76,10 @@ class Writer < ActiveRecord::Base
|
|
|
76
76
|
def styled_name
|
|
77
77
|
yield(name)
|
|
78
78
|
end
|
|
79
|
+
|
|
80
|
+
def credited?
|
|
81
|
+
!bio.nil?
|
|
82
|
+
end
|
|
79
83
|
end
|
|
80
84
|
|
|
81
85
|
# A converted model reads its association as an Option.
|
|
@@ -86,6 +90,7 @@ class Article < ActiveRecord::Base
|
|
|
86
90
|
delegate_optional :name, to: :writer, prefix: :author
|
|
87
91
|
delegate_optional :bio, to: :writer
|
|
88
92
|
delegate_optional :greeting, :styled_name, to: :writer, prefix: true
|
|
93
|
+
delegate_optional :credited?, to: :writer
|
|
89
94
|
delegate_optional :table_name, to: :class
|
|
90
95
|
end
|
|
91
96
|
|
|
@@ -27,6 +27,11 @@ module Errgonomic
|
|
|
27
27
|
# @!method delegate_optional(*methods, to: nil, prefix: nil, private: nil, allow_nil: nil)
|
|
28
28
|
# @!scope class
|
|
29
29
|
# Delegates to an optional target, answering an Option: None where the target is absent.
|
|
30
|
+
# A name ending in `?` is the exception: it answers a bare boolean, false for an absent target.
|
|
31
|
+
# @example a predicate answers a verdict rather than an Option, since every Option is truthy
|
|
32
|
+
# Article.create!(title: 'Omelas', writer: Writer.create!(name: 'Ursula', bio: 'writes')).credited? # => true
|
|
33
|
+
# Article.create!(title: 'Omelas', writer: Writer.create!(name: 'Ursula')).credited? # => false
|
|
34
|
+
# Article.create!(title: 'Untitled').credited? # => false
|
|
30
35
|
# @example prefix forms name the reader, as they do for Rails' delegate
|
|
31
36
|
# article = Article.create!(title: 'Omelas', writer: Writer.create!(name: 'Ursula', bio: 'writes'))
|
|
32
37
|
# article.writer_name # => Some('Ursula')
|
|
@@ -158,11 +163,25 @@ module Errgonomic
|
|
|
158
163
|
def define_optional_delegation(receiver, method_name, reader, declared_at)
|
|
159
164
|
class_eval <<-RUBY, declared_at.path, declared_at.lineno # rubocop:disable Style/EvalWithLocation
|
|
160
165
|
def #{reader}(...)
|
|
161
|
-
#{receiver
|
|
166
|
+
#{delegation_body(receiver, method_name)}
|
|
162
167
|
end
|
|
163
168
|
RUBY
|
|
164
169
|
end
|
|
165
170
|
|
|
171
|
+
# A ? name asks for a verdict, and an Option is not usable as one:
|
|
172
|
+
# every Option is truthy, so Some(false) and None both take the true
|
|
173
|
+
# branch. A predicate therefore answers a bare boolean, reading an
|
|
174
|
+
# absent target as false, which branches the way nil does.
|
|
175
|
+
def delegation_body(receiver, method_name)
|
|
176
|
+
return "#{receiver}.to_option.some_and? { |target| target.#{method_name}(...) }" if predicate?(method_name)
|
|
177
|
+
|
|
178
|
+
"#{receiver}.to_option.and_then { |target| target.#{method_name}(...).to_option }"
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def predicate?(method_name)
|
|
182
|
+
method_name.to_s.end_with?('?')
|
|
183
|
+
end
|
|
184
|
+
|
|
166
185
|
# A target named for a Ruby keyword reads as the keyword in the body
|
|
167
186
|
# it is written into, so it needs an explicit receiver. Rails answers
|
|
168
187
|
# the same question for delegate, and answers it for the same names.
|
|
@@ -38,10 +38,12 @@ module Errgonomic
|
|
|
38
38
|
# Validation unwraps at read_attribute_for_validation, the seam every
|
|
39
39
|
# EachValidator fetches an attribute through; serialization at
|
|
40
40
|
# read_attribute_for_serialization, the seam every attribute in a
|
|
41
|
-
# payload is fetched through;
|
|
42
|
-
# value, the seam every field reads its record through
|
|
43
|
-
#
|
|
44
|
-
#
|
|
41
|
+
# payload is fetched through; a form helper at ActionView's tag
|
|
42
|
+
# value, the seam every field reads its record through; and a query
|
|
43
|
+
# method at query_attribute, the seam every generated attr? is cast
|
|
44
|
+
# through. So a standard validator weighs the value, a payload carries
|
|
45
|
+
# it, a form renders it and a query method answers for it, rather than
|
|
46
|
+
# for the wrapper. A singular association with nested
|
|
45
47
|
# attributes goes further and keeps its plain reader: nested attributes
|
|
46
48
|
# are assigned through the reader, and ActiveRecord asks whatever it
|
|
47
49
|
# finds there whether it is a new record. So does a reader a framework
|
|
@@ -115,6 +117,29 @@ module Errgonomic
|
|
|
115
117
|
errgonomic_omit_absent_keys(hash)
|
|
116
118
|
end
|
|
117
119
|
|
|
120
|
+
# A query method reads the record through the public reader and matches
|
|
121
|
+
# what it finds against true, then false and nil, before falling through
|
|
122
|
+
# to blankness. An Option is none of those, and its blankness is its
|
|
123
|
+
# discriminant rather than its value, so an explicit false and a stored
|
|
124
|
+
# zero would answer true. Unwrapping the read is what keeps the answer
|
|
125
|
+
# the one an unconverted model gives.
|
|
126
|
+
#
|
|
127
|
+
# @example
|
|
128
|
+
# Note.new(pinned: false).pinned? # => false
|
|
129
|
+
# Note.new(rank: 0).rank? # => false
|
|
130
|
+
# Note.new(title: '').title? # => false
|
|
131
|
+
# Note.new(pinned: true).pinned? # => true
|
|
132
|
+
# Note.new(rank: 3).rank? # => true
|
|
133
|
+
def query_attribute(attr_name)
|
|
134
|
+
query_cast_attribute(attr_name, Errgonomic::Rails.unwrap_option(public_send(attr_name)))
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# Rails copies query_attribute into attribute? at the alias, and the
|
|
138
|
+
# attr? methods it generates call that copy, so an override of the one
|
|
139
|
+
# never reaches them.
|
|
140
|
+
alias attribute? query_attribute
|
|
141
|
+
private :attribute?
|
|
142
|
+
|
|
118
143
|
# YARD does not see through a concern's class_methods block, so the
|
|
119
144
|
# method it documents is declared rather than read.
|
|
120
145
|
#
|
data/lib/errgonomic/version.rb
CHANGED