errgonomic 0.9.2 → 0.10.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 +26 -0
- data/README.md +11 -7
- data/lib/errgonomic/core_ext/enumerable.rb +2 -2
- data/lib/errgonomic/option.rb +116 -11
- data/lib/errgonomic/rails/active_record_optional.rb +15 -6
- data/lib/errgonomic/result.rb +97 -19
- data/lib/errgonomic/variant_name.rb +63 -0
- data/lib/errgonomic/version.rb +1 -1
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d39f466feedb411b53780fa853b509dc6a40db2f12aef63235a6817881ce2d95
|
|
4
|
+
data.tar.gz: bc81bba0f57ddaac60178b0319533feeb054be296614332b8a9744d72b0ae9d1
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 26b24e1089898ad573927824616829559a6c9fa5c169abf71addcad1284a345597ae12928f4a22153ce1d06f64db3572e7e4d0d6c596df2dffef4a1eb260e72e
|
|
7
|
+
data.tar.gz: 5f20b741f3feb1c4f5b84daee2f3934edc09f89f962a459c485dfed8356d63e84297f778b3b74f761ada2c1be2ea5c371c73b318a2ef5aa54c8ba635612ba450
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,31 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [0.10.0] - 2026-09-10
|
|
4
|
+
|
|
5
|
+
This release gives `deconstruct` the Rust shape and names the four variants at top level, so a pattern reads as `in Some(v)`.
|
|
6
|
+
|
|
7
|
+
### Upgrading from 0.9.3
|
|
8
|
+
|
|
9
|
+
`deconstruct` answers `[value]` for a `Some`, an `Ok` and an `Err` and `[]` for a `None`, so a pattern written against 0.9.x's `[self, value]` has to change shape: `in Errgonomic::Option::Some, v` becomes `in Some(v)`, `in Errgonomic::Result::Err, String => msg` becomes `in Err(String => msg)`, and `in Errgonomic::Option::None` stays as it is or becomes `in None`. `Some`, `None`, `Ok` and `Err` are now top-level names for the four variants, for patterns, as well as constructors. An application that defines its own constant under one of those names has to rename it: errgonomic refuses to load over one with a `NameError`, and a `class` or `module` of that name written after it loads raises `TypeError`.
|
|
10
|
+
|
|
11
|
+
### Changes
|
|
12
|
+
|
|
13
|
+
- [Behavior change] `deconstruct` answers `[value]` for a `Some`, an `Ok` and an `Err` and `[]` for a `None`, the one-payload shape `Data.define(:value)` and Rust's tuple variants share, where 0.9.x answered `[self, value]` and `[None]`. `in Some(v)` binds the value, `in Ok(Some(v))` nests, a two-branch `case/in` with no `else` is exhaustive, and a wrong type reaches `NoMatchingPatternError`. There is no `deconstruct_keys`: a one-payload sum type has no named field, and a `Some` around a Hash nests as `in Some({ id: })` through the Hash's own protocol
|
|
14
|
+
- A value-less `Err()` deconstructs to `[]`, so `in Err` and `in Err()` match it and `in Err(e)` matches only an `Err` that carries a value. The sentinel `Err()` holds in place of a value is internal, and a pattern variable must never bind it
|
|
15
|
+
- `Some`, `None`, `Ok` and `Err` are defined at top level beside the constructors of the same name, so a pattern reads as it does in Rust. Each is an `Errgonomic::VariantName` rather than the class it names: it matches as that class in a pattern and a `case/when`, and a bare one, written as Rust writes `return None`, refuses to stand in for a value. `to_s`, `to_json`, `as_json` and every ActiveRecord boundary that unwraps an Option raise `Errgonomic::SerializeError` on it, where the class would write `Errgonomic::Option::None` into a string, a column or a query, and a boolean column would store `true`. A constant of the same name that the application already defines stops the gem's load with a `NameError`, and a `class None` written after the gem loads raises `TypeError`, rather than one replacing or reopening the other. Rails defines none of the four
|
|
16
|
+
|
|
17
|
+
## [0.9.3] - 2026-09-10
|
|
18
|
+
|
|
19
|
+
This release removes the public `value` slot from `Some`, `Ok` and `Err`, and freezes every instance as it is constructed, so an Option or a Result is the value the README already said it was.
|
|
20
|
+
|
|
21
|
+
### Upgrading from 0.9.2
|
|
22
|
+
|
|
23
|
+
`value` and `value=` are gone from `Some`, `Ok` and `Err`, and every instance is frozen as it is constructed. A read of `.value` becomes `unwrap_or(fallback)`, `expect!(message)`, `map`, `and_then` or a pattern, each of which names the other branch; a write of `.value=` becomes a new `Some(v)` assigned where the old one lived. A call to either now raises `Errgonomic::UnwrappedAccessError`, which is a `NoMethodError`, naming the combinators.
|
|
24
|
+
|
|
25
|
+
### Changes
|
|
26
|
+
|
|
27
|
+
- [Behavior change] `Some`, `Ok` and `Err` no longer expose `value` or `value=`, and every Option and Result is frozen as it is constructed, whether by `Some`, `None`, `Ok`, `Err`, `new` or a combinator; `clone` keeps it frozen. A copy that skips construction, from `dup`, `Marshal.load`, a YAML load or ActiveSupport's `deep_dup`, is not frozen; with no writer, it changes only through `instance_variable_set`. The reader reached the inner value with no `None` branch, the writer mutated a wrapper through an alias and moved a Hash key out from under its own bucket, and the README already said an Option is a value rather than a slot. The reader is protected, for the sibling reads equality, ordering and `zip` need; a call from outside gets the combinator teaching `Errgonomic::UnwrappedAccessError` gives any other miss
|
|
28
|
+
|
|
3
29
|
## [0.9.2] - 2026-09-10
|
|
4
30
|
|
|
5
31
|
This release makes `Option#and`, `#xor`, `#zip` and `#zip_with` check their operand the way `or` already did.
|
data/README.md
CHANGED
|
@@ -111,17 +111,21 @@ Some(1).each.to_a # => [1]
|
|
|
111
111
|
|
|
112
112
|
`map` wraps whatever the block returns, as Rust's does, so a block that itself returns an Option gives `Some(Some(x))`. `and_then` is the spelling for that block.
|
|
113
113
|
|
|
114
|
-
Options
|
|
114
|
+
Options pattern match in the Rust shape. `Some`, `None`, `Ok` and `Err` name the variants in a pattern as well as building them, so a pattern reads as it does in Rust:
|
|
115
115
|
|
|
116
116
|
```ruby
|
|
117
117
|
case measurement
|
|
118
|
-
in
|
|
118
|
+
in Some(value)
|
|
119
119
|
"Measurement is #{value}"
|
|
120
|
-
in
|
|
120
|
+
in None
|
|
121
121
|
"Measurement is not available"
|
|
122
122
|
end
|
|
123
123
|
```
|
|
124
124
|
|
|
125
|
+
Leave the `else` off: the two branches cover an Option, and a value that is not one raises `NoMatchingPatternError` where an `else` would take it quietly. When that value is a Result, the error's message refuses to print; see the `case/in` note below. `deconstruct` answers `[value]` for a `Some` and `[]` for a `None`, the one-payload shape `Data.define(:value)` and Rust's tuple variants share, and there is no `deconstruct_keys`, because a one-payload sum type has no named field. Patterns nest through the inner value's own protocol instead: `in Ok(Some(value))` reaches through a Result, and `in Some({ id: })` reaches into a Hash a `Some` wraps.
|
|
126
|
+
|
|
127
|
+
A bare `Some`, `None`, `Ok` or `Err`, without parentheses, is that name and not a value, where Rust's `return None` is one. It matches as the class it names in a pattern or a `case/when`, and refuses to stand in for a value: interpolation, `to_s`, `join`, `to_json` and `as_json` raise `Errgonomic::SerializeError` (`bare None names a variant for a pattern, not a value; build one with parentheses`), and so does handing one to an ActiveRecord attribute, a bulk write or a `where`. It is not a class, so `is_a?`, `kind_of?` and `instance_of?` raise `TypeError` when given the bare name (`class or module required`), `Some.new`, `Some <= Errgonomic::Option::Any` and `Some.name` raise `NoMethodError`, and Minitest's `assert_kind_of Some, x` raises the same `TypeError` before it can assert anything. Each has a working spelling: check the variant with `x.some?`, `x.none?`, `x.ok?` or `x.err?`, or give the fully qualified class anywhere a class or module is required, as in `x.is_a?(Errgonomic::Option::Some)`, `assert_kind_of Errgonomic::Option::Some, x`, `Errgonomic::Option::Some.new(1)`, `Errgonomic::Option::Some <= Errgonomic::Option::Any` or `Errgonomic::Option::Some.name`. Build with the constructor, `Some(1)`, rather than `.new`. An application constant of the same name collides loudly: defined before errgonomic loads, it stops the load with a `NameError`, and a `class None` or `module Ok` written after raises `TypeError` where it is written. Two forms get past that. `None = …` assigned after the load replaces the name with only Ruby's `already initialized constant` warning, and in a Rails application Zeitwerk skips an autoloaded `app/models/ok.rb` because `Ok` is already defined, so the first call on `Ok` raises `NoMethodError`.
|
|
128
|
+
|
|
125
129
|
An unhandled Option refuses to leak into your output: `to_s`, `to_json` and `as_json` raise `Errgonomic::SerializeError`, so you handle the inner value deliberately rather than shipping `Some("...")` to a user. The refusal names what it was carrying (`cannot serialize an unwrapped Some("cell-a1b2")`), so a payload built out of many values says which one went unhandled; the value's `inspect` is bounded to 60 characters, with an ellipsis past that. The refusal covers `as_json` because Hash and Array serialization recurses through that method, and an Option nested in a payload would otherwise serialize as `{"value": ...}`. A converted ActiveRecord model is the one exception, at the model boundary: it unwraps each attribute as it serializes, so a record's own `as_json` says what an unconverted record's says. See [Rails integration](#rails-integration).
|
|
126
130
|
|
|
127
131
|
The `to_s` refusal is the loudest guard of the three, because a string is where a wrapper turns into data: string interpolation, `Array#join`, `format`, `String()`, a bare ERB `<%= %>` and the key of a Hash on its way to JSON all reach the value through `to_s`, and every one of them raises rather than writing `Some("...")` into a hostname, a column or a page. Rust gives `Option` a `Debug` and no `Display`, and `inspect` is the `Debug` here: it renders `Some(1)`, and it is what a log line or a `rescue` should call (`"got #{opt.inspect}"`). The refusal says so: `Some(1) refuses to_s; use inspect for a log line, or unwrap_or / expect! for the value`.
|
|
@@ -144,7 +148,7 @@ Writers unwrap under that integration, which changes what a truthiness slip cost
|
|
|
144
148
|
|
|
145
149
|
The remaining present-side helpers are soft-deprecated on Options in favor of the combinators. They unwrap, where on any other object they return the receiver: `Some(v).present_or_raise!(msg)`, `present_or(default)` and `present_or_else { }` all yield `v`, and `None` raises, substitutes, or computes. Each prints a one-line stderr nudge naming the combinator to use instead (`expect!`, `unwrap_or`, `unwrap_or_else`), once per process per method rather than once per call, so a hot path does not flood the log. The blank side (`blank_or*`) raises `UnwrappedAccessError` outright: an Option's blankness is its discriminant, so test it with `none?`.
|
|
146
150
|
|
|
147
|
-
Four of Rust's methods are deliberately absent: `take`, `replace`, `insert` and `get_or_insert`. Every one of them writes through an `&mut Option`, and an Option here is a value rather than a slot: `Some(1)` is something you pass around and compare, not a cell whose contents you swap out from under another reference. Build the Option you want and assign it where the old one lived.
|
|
151
|
+
Four of Rust's methods are deliberately absent: `take`, `replace`, `insert` and `get_or_insert`. Every one of them writes through an `&mut Option`, and an Option here is a value rather than a slot: `Some(1)` is something you pass around and compare, not a cell whose contents you swap out from under another reference. Build the Option you want and assign it where the old one lived. The same rule is why a `Some`, an `Ok` and an `Err` have no `value` reader or writer, and why every instance is frozen as it is constructed. A copy that skips construction, from `dup`, `Marshal.load`, a YAML load or ActiveSupport's `deep_dup`, is not frozen; with no writer, it changes only through `instance_variable_set`. A reader would reach the inner value with no `None` branch, and a writer would move a Hash key out from under its own bucket. Reach in with `unwrap_or`, `expect!`, `map`, `and_then` or a pattern, each of which names what happens on the other branch.
|
|
148
152
|
|
|
149
153
|
Equality is between Options only: `Some(5) == Some(5)`, but `Some(5) == 5` and `None() == nil` are `false`. That is quiet, never an error, matching how every Ruby object compares across types. Rust rejects `Some(5) == 5` at compile time; Ruby cannot, so guard the idiom in review and tests: compare against a wrapped value (`opt == Some(5)`) or test the inner value (`opt.some_and? { |v| v == 5 }`). `Errgonomic.strict_equality = true` turns that guard into an error, which is what a test suite wants; see [Pedantic runtime checks](#pedantic-runtime-checks).
|
|
150
154
|
|
|
@@ -172,11 +176,11 @@ Results also pattern match, including against the kind of inner value:
|
|
|
172
176
|
|
|
173
177
|
```ruby
|
|
174
178
|
case result
|
|
175
|
-
in
|
|
179
|
+
in Ok(value)
|
|
176
180
|
"Measurement is #{value}"
|
|
177
|
-
in
|
|
181
|
+
in Err(String => msg)
|
|
178
182
|
"Measurement failed with a message: #{msg}"
|
|
179
|
-
in
|
|
183
|
+
in Err(Exception => e)
|
|
180
184
|
"Measurement produced an exception -- #{e.class}: #{e}"
|
|
181
185
|
end
|
|
182
186
|
```
|
|
@@ -46,7 +46,7 @@ module Enumerable
|
|
|
46
46
|
end
|
|
47
47
|
return None() if member.none?
|
|
48
48
|
|
|
49
|
-
values <<
|
|
49
|
+
member.tap_some { |value| values << value }
|
|
50
50
|
end
|
|
51
51
|
Some(values)
|
|
52
52
|
end
|
|
@@ -87,7 +87,7 @@ module Enumerable
|
|
|
87
87
|
end
|
|
88
88
|
return member if member.err?
|
|
89
89
|
|
|
90
|
-
values <<
|
|
90
|
+
member.tap_ok { |value| values << value }
|
|
91
91
|
end
|
|
92
92
|
Ok(values)
|
|
93
93
|
end
|
data/lib/errgonomic/option.rb
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require 'set'
|
|
4
4
|
require 'stringio'
|
|
5
|
+
require_relative 'variant_name'
|
|
5
6
|
|
|
6
7
|
module Errgonomic
|
|
7
8
|
module Option
|
|
@@ -174,20 +175,65 @@ module Errgonomic
|
|
|
174
175
|
[self.class, value].hash
|
|
175
176
|
end
|
|
176
177
|
|
|
178
|
+
# The Rust shape: a Some deconstructs to its one payload and a None to
|
|
179
|
+
# nothing, so `in Some(v)` binds the value and `in None` matches. There
|
|
180
|
+
# is no deconstruct_keys, because a one-payload sum type has no named
|
|
181
|
+
# field; a Some wrapping a Hash nests as `in Some({id:})` through the
|
|
182
|
+
# Hash's own protocol.
|
|
183
|
+
#
|
|
177
184
|
# @example
|
|
178
|
-
#
|
|
185
|
+
# Some(1).deconstruct # => [1]
|
|
186
|
+
# None().deconstruct # => []
|
|
187
|
+
# Some(1).respond_to?(:deconstruct_keys) # => false
|
|
188
|
+
#
|
|
189
|
+
# @example a two-branch case/in with no else is exhaustive
|
|
190
|
+
# measurement = Some(1)
|
|
179
191
|
# case measurement
|
|
180
|
-
# in
|
|
181
|
-
# "Measurement is #{
|
|
182
|
-
# in
|
|
192
|
+
# in Some(value)
|
|
193
|
+
# "Measurement is #{value}"
|
|
194
|
+
# in None
|
|
183
195
|
# "Measurement is not available"
|
|
184
|
-
# else
|
|
185
|
-
# "not matched"
|
|
186
196
|
# end # => "Measurement is 1"
|
|
197
|
+
# case None()
|
|
198
|
+
# in Some(value)
|
|
199
|
+
# "Measurement is #{value}"
|
|
200
|
+
# in None
|
|
201
|
+
# "Measurement is not available"
|
|
202
|
+
# end # => "Measurement is not available"
|
|
203
|
+
#
|
|
204
|
+
# @example the wrong type falls through to Ruby's own exhaustiveness check
|
|
205
|
+
# begin
|
|
206
|
+
# case 1
|
|
207
|
+
# in Some(value) then value
|
|
208
|
+
# in None then nil
|
|
209
|
+
# end
|
|
210
|
+
# rescue NoMatchingPatternError => e
|
|
211
|
+
# [e.class, e.message]
|
|
212
|
+
# end # => [NoMatchingPatternError, "1"]
|
|
213
|
+
#
|
|
214
|
+
# @example a Result that falls through carries a message that refuses to print
|
|
215
|
+
# begin
|
|
216
|
+
# case Ok(1)
|
|
217
|
+
# in Some(value) then value
|
|
218
|
+
# in None then nil
|
|
219
|
+
# end
|
|
220
|
+
# rescue NoMatchingPatternError => e
|
|
221
|
+
# [e.class, (e.message rescue $!.class)]
|
|
222
|
+
# end # => [NoMatchingPatternError, Errgonomic::SerializeError]
|
|
223
|
+
#
|
|
224
|
+
# @example patterns nest through the inner value's own protocol
|
|
225
|
+
# case Ok(Some(1))
|
|
226
|
+
# in Ok(Some(value)) then value
|
|
227
|
+
# end # => 1
|
|
228
|
+
# case Some({ id: 7, name: 'x' })
|
|
229
|
+
# in Some({ id: }) then id
|
|
230
|
+
# end # => 7
|
|
231
|
+
# case Some(1)
|
|
232
|
+
# in Errgonomic::Option::Some(value) then "bound #{value}"
|
|
233
|
+
# else "not matched"
|
|
234
|
+
# end # => "bound 1"
|
|
187
235
|
def deconstruct
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
[Errgonomic::Option::None]
|
|
236
|
+
to_a
|
|
191
237
|
end
|
|
192
238
|
|
|
193
239
|
# Options order like Rust's: None sorts before any Some, and Somes
|
|
@@ -851,11 +897,47 @@ module Errgonomic
|
|
|
851
897
|
|
|
852
898
|
# Represent a value
|
|
853
899
|
class Some < Any
|
|
854
|
-
|
|
855
|
-
|
|
900
|
+
# A Some is a value, not a slot: nothing outside reads the inner value
|
|
901
|
+
# without handling the None branch, and nothing swaps it out from under
|
|
902
|
+
# another reference or a Hash key.
|
|
903
|
+
#
|
|
904
|
+
# @example the inner value is reached through a combinator, never a reader
|
|
905
|
+
# begin
|
|
906
|
+
# Some(1).value
|
|
907
|
+
# rescue NoMethodError => e
|
|
908
|
+
# e.class
|
|
909
|
+
# end # => Errgonomic::UnwrappedAccessError
|
|
910
|
+
# Some(1).respond_to?(:value) # => false
|
|
911
|
+
#
|
|
912
|
+
# @example a Some cannot be mutated through an alias
|
|
913
|
+
# a = Some(1)
|
|
914
|
+
# b = a
|
|
915
|
+
# begin
|
|
916
|
+
# b.value = 99
|
|
917
|
+
# rescue NoMethodError => e
|
|
918
|
+
# e.class
|
|
919
|
+
# end # => Errgonomic::UnwrappedAccessError
|
|
920
|
+
# a # => Some(1)
|
|
921
|
+
# Some(1).frozen? # => true
|
|
922
|
+
# begin
|
|
923
|
+
# Some(1).instance_variable_set(:@value, 2)
|
|
924
|
+
# rescue FrozenError => e
|
|
925
|
+
# e.class
|
|
926
|
+
# end # => FrozenError
|
|
927
|
+
#
|
|
928
|
+
# @example a Some keeps its place as a Hash key
|
|
929
|
+
# k = Some(1)
|
|
930
|
+
# h = { k => :v }
|
|
931
|
+
# begin
|
|
932
|
+
# k.value = 2
|
|
933
|
+
# rescue NoMethodError
|
|
934
|
+
# nil
|
|
935
|
+
# end
|
|
936
|
+
# h[k] # => :v
|
|
856
937
|
def initialize(value)
|
|
857
938
|
super()
|
|
858
939
|
@value = value
|
|
940
|
+
freeze
|
|
859
941
|
end
|
|
860
942
|
|
|
861
943
|
def some?
|
|
@@ -877,10 +959,28 @@ module Errgonomic
|
|
|
877
959
|
def inspect
|
|
878
960
|
"Some(#{value.inspect})"
|
|
879
961
|
end
|
|
962
|
+
|
|
963
|
+
protected
|
|
964
|
+
|
|
965
|
+
# Sibling instances read each other's value for equality, ordering and
|
|
966
|
+
# zip; nothing else does.
|
|
967
|
+
attr_reader :value
|
|
880
968
|
end
|
|
881
969
|
|
|
882
970
|
# Represent the absence of a value.
|
|
883
971
|
class None < Any
|
|
972
|
+
# @example a None has no value to read, and says so the same way a Some does
|
|
973
|
+
# begin
|
|
974
|
+
# None().value
|
|
975
|
+
# rescue NoMethodError => e
|
|
976
|
+
# e.class
|
|
977
|
+
# end # => Errgonomic::UnwrappedAccessError
|
|
978
|
+
# None().frozen? # => true
|
|
979
|
+
def initialize
|
|
980
|
+
super
|
|
981
|
+
freeze
|
|
982
|
+
end
|
|
983
|
+
|
|
884
984
|
def some?
|
|
885
985
|
false
|
|
886
986
|
end
|
|
@@ -907,3 +1007,8 @@ end
|
|
|
907
1007
|
def None
|
|
908
1008
|
Errgonomic::Option::None.new
|
|
909
1009
|
end
|
|
1010
|
+
|
|
1011
|
+
# The variants under their short names, so a pattern reads as it does in
|
|
1012
|
+
# Rust: `in Some(v)`, `in None`.
|
|
1013
|
+
Errgonomic::VariantName.define(:Some, Errgonomic::Option::Some)
|
|
1014
|
+
Errgonomic::VariantName.define(:None, Errgonomic::Option::None)
|
|
@@ -516,10 +516,10 @@ module Errgonomic
|
|
|
516
516
|
# Errgonomic::Rails.unwrap_options(1) # => 1
|
|
517
517
|
def self.unwrap_options(value)
|
|
518
518
|
case value
|
|
519
|
-
when Errgonomic::Option::Any
|
|
520
|
-
value
|
|
519
|
+
when Errgonomic::Option::Any, Errgonomic::VariantName
|
|
520
|
+
unwrap_option(value)
|
|
521
521
|
when Array
|
|
522
|
-
value.any? { |v|
|
|
522
|
+
value.any? { |v| unwrapped_at_boundary?(v) } ? value.map { |v| unwrap_options(v) } : value
|
|
523
523
|
else
|
|
524
524
|
value
|
|
525
525
|
end
|
|
@@ -527,13 +527,16 @@ module Errgonomic
|
|
|
527
527
|
|
|
528
528
|
# Take the value inside an Option, and a None as nil, where the boundary
|
|
529
529
|
# takes one value: an attribute is a single typed field, so a collection
|
|
530
|
-
# that happens to hold an Option is that collection.
|
|
530
|
+
# that happens to hold an Option is that collection. A bare variant name
|
|
531
|
+
# is refused here, since a boolean column would cast it to true.
|
|
531
532
|
#
|
|
532
533
|
# @example
|
|
533
534
|
# Errgonomic::Rails.unwrap_option(Some(1)) # => 1
|
|
534
535
|
# Errgonomic::Rails.unwrap_option(None()) # => nil
|
|
535
536
|
# Errgonomic::Rails.unwrap_option([Some(1)]) # => [Some(1)]
|
|
537
|
+
# Errgonomic::Rails.unwrap_option(None) # => raise Errgonomic::SerializeError, "bare None names a variant for a pattern, not a value; build one with parentheses"
|
|
536
538
|
def self.unwrap_option(value)
|
|
539
|
+
value.refuse! if value.is_a?(Errgonomic::VariantName)
|
|
537
540
|
value.is_a?(Errgonomic::Option::Any) ? value.unwrap_or(nil) : value
|
|
538
541
|
end
|
|
539
542
|
|
|
@@ -548,7 +551,7 @@ module Errgonomic
|
|
|
548
551
|
# plain = { title: 'x' }
|
|
549
552
|
# Errgonomic::Rails.unwrap_option_values(plain).equal?(plain) # => true
|
|
550
553
|
def self.unwrap_option_values(hash)
|
|
551
|
-
return hash unless hash.each_value.any?(
|
|
554
|
+
return hash unless hash.each_value.any? { |value| unwrapped_at_boundary?(value) }
|
|
552
555
|
|
|
553
556
|
hash.transform_values { |value| unwrap_option(value) }
|
|
554
557
|
end
|
|
@@ -561,11 +564,17 @@ module Errgonomic
|
|
|
561
564
|
# plain = [{ title: 'x' }]
|
|
562
565
|
# Errgonomic::Rails.unwrap_option_rows(plain).equal?(plain) # => true
|
|
563
566
|
def self.unwrap_option_rows(rows)
|
|
564
|
-
return rows unless rows.any? { |row| row.is_a?(Hash) && row.each_value.any?(
|
|
567
|
+
return rows unless rows.any? { |row| row.is_a?(Hash) && row.each_value.any? { |v| unwrapped_at_boundary?(v) } }
|
|
565
568
|
|
|
566
569
|
rows.map { |row| row.is_a?(Hash) ? unwrap_option_values(row) : row }
|
|
567
570
|
end
|
|
568
571
|
|
|
572
|
+
# An Option, or a bare variant name, which a boundary refuses rather than
|
|
573
|
+
# let a column type cast it.
|
|
574
|
+
def self.unwrapped_at_boundary?(value)
|
|
575
|
+
value.is_a?(Errgonomic::Option::Any) || value.is_a?(Errgonomic::VariantName)
|
|
576
|
+
end
|
|
577
|
+
|
|
569
578
|
# A declared default that is a Proc is not a value yet: ActiveModel calls
|
|
570
579
|
# it with no arguments each time a record is built. Wrap it rather than
|
|
571
580
|
# unwrap it, so what it returns meets the type where a literal default
|
data/lib/errgonomic/result.rb
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative 'variant_name'
|
|
4
|
+
|
|
3
5
|
module Errgonomic
|
|
4
6
|
module Result
|
|
5
7
|
# The base class for Result's Ok and Err class variants. We implement as
|
|
@@ -8,10 +10,27 @@ module Errgonomic
|
|
|
8
10
|
class Any
|
|
9
11
|
include Comparable
|
|
10
12
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
+
# A Result is a value, not a slot: the inner value is reached through a
|
|
14
|
+
# combinator that handles the other variant, and nothing swaps it out
|
|
15
|
+
# from under another reference.
|
|
16
|
+
#
|
|
17
|
+
# @example
|
|
18
|
+
# begin
|
|
19
|
+
# Ok(1).value
|
|
20
|
+
# rescue NoMethodError => e
|
|
21
|
+
# e.class
|
|
22
|
+
# end # => Errgonomic::UnwrappedAccessError
|
|
23
|
+
# begin
|
|
24
|
+
# Err(:x).value = :y
|
|
25
|
+
# rescue NoMethodError => e
|
|
26
|
+
# e.class
|
|
27
|
+
# end # => Errgonomic::UnwrappedAccessError
|
|
28
|
+
# Ok(1).respond_to?(:value) # => false
|
|
29
|
+
# Ok(1).frozen? # => true
|
|
30
|
+
# Err().frozen? # => true
|
|
13
31
|
def initialize(value)
|
|
14
32
|
@value = value
|
|
33
|
+
freeze
|
|
15
34
|
end
|
|
16
35
|
|
|
17
36
|
# Results order like Rust's: Ok sorts before any Err, and same variants
|
|
@@ -454,29 +473,87 @@ module Errgonomic
|
|
|
454
473
|
pp.text(inspect)
|
|
455
474
|
end
|
|
456
475
|
|
|
457
|
-
#
|
|
458
|
-
#
|
|
459
|
-
#
|
|
460
|
-
#
|
|
476
|
+
# The Rust shape: each variant deconstructs to its one payload, so
|
|
477
|
+
# `in Ok(v)` binds the value and `in Err(e)` binds the error. A
|
|
478
|
+
# value-less Err deconstructs to nothing: the sentinel behind it is
|
|
479
|
+
# internal and must never bind to a pattern variable.
|
|
480
|
+
#
|
|
481
|
+
# @example
|
|
482
|
+
# Ok(1).deconstruct # => [1]
|
|
483
|
+
# Err(:e).deconstruct # => [:e]
|
|
484
|
+
# Err().deconstruct # => []
|
|
485
|
+
# Ok(1).respond_to?(:deconstruct_keys) # => false
|
|
486
|
+
#
|
|
487
|
+
# @example a value-less Err matches `in Err` and `in Err()`, never `in Err(e)`
|
|
488
|
+
# case Err()
|
|
489
|
+
# in Err(e) then e
|
|
490
|
+
# in Err then :no_value
|
|
491
|
+
# end # => :no_value
|
|
492
|
+
# case Err()
|
|
493
|
+
# in Err() then :no_value
|
|
494
|
+
# end # => :no_value
|
|
495
|
+
# case Err(:x)
|
|
496
|
+
# in Err(e) then e
|
|
497
|
+
# in Err then :no_value
|
|
498
|
+
# end # => :x
|
|
499
|
+
# begin
|
|
500
|
+
# case Err()
|
|
501
|
+
# in Err(e) then e
|
|
502
|
+
# end
|
|
503
|
+
# rescue NoMatchingPatternError => e
|
|
504
|
+
# e.class
|
|
505
|
+
# end # => NoMatchingPatternError
|
|
506
|
+
#
|
|
507
|
+
# @example a two-branch case/in with no else is exhaustive
|
|
508
|
+
# case Ok(1)
|
|
509
|
+
# in Ok(value)
|
|
461
510
|
# "Measurement is #{value}"
|
|
462
|
-
# in
|
|
511
|
+
# in Err(err)
|
|
463
512
|
# "Measurement is not available"
|
|
464
513
|
# end # => "Measurement is 1"
|
|
465
514
|
#
|
|
466
|
-
# @example
|
|
467
|
-
#
|
|
515
|
+
# @example the wrong type falls through to Ruby's own exhaustiveness check
|
|
516
|
+
# begin
|
|
517
|
+
# case :done
|
|
518
|
+
# in Ok(value) then value
|
|
519
|
+
# in Err(err) then err
|
|
520
|
+
# end
|
|
521
|
+
# rescue NoMatchingPatternError => e
|
|
522
|
+
# [e.class, e.message]
|
|
523
|
+
# end # => [NoMatchingPatternError, "done"]
|
|
524
|
+
#
|
|
525
|
+
# @example an Option that falls through carries a message that refuses to print
|
|
526
|
+
# begin
|
|
527
|
+
# case Some(1)
|
|
528
|
+
# in Ok(value) then value
|
|
529
|
+
# in Err(err) then err
|
|
530
|
+
# end
|
|
531
|
+
# rescue NoMatchingPatternError => e
|
|
532
|
+
# [e.class, (e.message rescue $!.class)]
|
|
533
|
+
# end # => [NoMatchingPatternError, Errgonomic::SerializeError]
|
|
534
|
+
#
|
|
535
|
+
# @example a pattern reaches the kind of value inside the variant
|
|
536
|
+
# result = Err(StandardError.new("nope"))
|
|
468
537
|
# case result
|
|
469
|
-
# in
|
|
538
|
+
# in Ok(value)
|
|
470
539
|
# "Measurement is #{value}"
|
|
471
|
-
# in
|
|
540
|
+
# in Err(String => msg)
|
|
472
541
|
# "Measurement failed with a message: #{msg}"
|
|
473
|
-
# in
|
|
542
|
+
# in Err(Exception => e)
|
|
474
543
|
# "Measurement produced an exception -- #{e.class}: #{e}"
|
|
475
544
|
# end # => "Measurement produced an exception -- StandardError: nope"
|
|
476
545
|
def deconstruct
|
|
477
|
-
[
|
|
546
|
+
return [] if value.equal?(Err::Arbitrary)
|
|
547
|
+
|
|
548
|
+
[value]
|
|
478
549
|
end
|
|
479
550
|
|
|
551
|
+
protected
|
|
552
|
+
|
|
553
|
+
# Sibling instances read each other's value for equality and ordering;
|
|
554
|
+
# nothing else does.
|
|
555
|
+
attr_reader :value
|
|
556
|
+
|
|
480
557
|
private
|
|
481
558
|
|
|
482
559
|
def to_s_refusal
|
|
@@ -512,8 +589,6 @@ module Errgonomic
|
|
|
512
589
|
|
|
513
590
|
# The Ok variant.
|
|
514
591
|
class Ok < Any
|
|
515
|
-
attr_accessor :value
|
|
516
|
-
|
|
517
592
|
# Ok is always ok
|
|
518
593
|
#
|
|
519
594
|
# @example
|
|
@@ -544,13 +619,11 @@ module Errgonomic
|
|
|
544
619
|
class Err < Any
|
|
545
620
|
class Arbitrary; end
|
|
546
621
|
|
|
547
|
-
attr_accessor :value
|
|
548
|
-
|
|
549
622
|
# Err may be constructed without a value, if you want.
|
|
550
623
|
#
|
|
551
624
|
# @example
|
|
552
|
-
# Err(:y).
|
|
553
|
-
# Err().
|
|
625
|
+
# Err(:y).unwrap_err! # => :y
|
|
626
|
+
# Err().unwrap_err! # => Arbitrary
|
|
554
627
|
def initialize(value = Arbitrary)
|
|
555
628
|
super(value)
|
|
556
629
|
end
|
|
@@ -624,3 +697,8 @@ end
|
|
|
624
697
|
def Err(value = Errgonomic::Result::Err::Arbitrary)
|
|
625
698
|
Errgonomic::Result::Err.new(value)
|
|
626
699
|
end
|
|
700
|
+
|
|
701
|
+
# The variants under their short names, so a pattern reads as it does in
|
|
702
|
+
# Rust: `in Ok(v)`, `in Err(e)`.
|
|
703
|
+
Errgonomic::VariantName.define(:Ok, Errgonomic::Result::Ok)
|
|
704
|
+
Errgonomic::VariantName.define(:Err, Errgonomic::Result::Err)
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Errgonomic
|
|
4
|
+
# What a bare `Some`, `None`, `Ok` or `Err` evaluates to. Rust writes a
|
|
5
|
+
# value as `return None`, where here the value is `None()`; the bare name
|
|
6
|
+
# is for patterns. It matches as the class it names, and refuses to become
|
|
7
|
+
# a string, so a missing pair of parentheses raises rather than writing a
|
|
8
|
+
# class name into a column. It is not a class, so an application's own
|
|
9
|
+
# `class None` fails where it is written instead of reopening the gem's.
|
|
10
|
+
#
|
|
11
|
+
# @example
|
|
12
|
+
# case Some(1)
|
|
13
|
+
# in Some(value) then value
|
|
14
|
+
# end # => 1
|
|
15
|
+
# None === None() # => true
|
|
16
|
+
# None === Some(1) # => false
|
|
17
|
+
# None.inspect # => "Errgonomic::Option::None"
|
|
18
|
+
# "tier-#{None}" # => raise Errgonomic::SerializeError, "bare None names a variant for a pattern, not a value; build one with parentheses"
|
|
19
|
+
# [Ok].join # => raise Errgonomic::SerializeError, "bare Ok names a variant for a pattern, not a value; build one with parentheses"
|
|
20
|
+
# Some(1).is_a?(Some) # => raise TypeError, "class or module required"
|
|
21
|
+
# Some(1).is_a?(Errgonomic::Option::Some) # => true
|
|
22
|
+
class VariantName
|
|
23
|
+
# Name a variant at top level, refusing a constant the application
|
|
24
|
+
# already holds there rather than replacing it.
|
|
25
|
+
def self.define(name, variant)
|
|
26
|
+
if Object.const_defined?(name, false)
|
|
27
|
+
existing = Object.const_get(name)
|
|
28
|
+
return if existing.is_a?(VariantName) && existing.names?(variant)
|
|
29
|
+
|
|
30
|
+
raise NameError.new("#{name} is already defined as #{existing.inspect}; errgonomic defines #{name} " \
|
|
31
|
+
"at top level to name #{variant} in patterns, so rename the application's constant", name)
|
|
32
|
+
end
|
|
33
|
+
Object.const_set(name, new(name, variant))
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def initialize(name, variant)
|
|
37
|
+
@name = name
|
|
38
|
+
@variant = variant
|
|
39
|
+
freeze
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def names?(variant)
|
|
43
|
+
@variant.equal?(variant)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def ===(other)
|
|
47
|
+
@variant === other # rubocop:disable Style/CaseEquality
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def inspect
|
|
51
|
+
@variant.inspect
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Refuse to stand in for a value.
|
|
55
|
+
def refuse!(*_args)
|
|
56
|
+
raise Errgonomic::SerializeError,
|
|
57
|
+
"bare #{@name} names a variant for a pattern, not a value; build one with parentheses"
|
|
58
|
+
end
|
|
59
|
+
alias to_s refuse!
|
|
60
|
+
alias to_json refuse!
|
|
61
|
+
alias as_json refuse!
|
|
62
|
+
end
|
|
63
|
+
end
|
data/lib/errgonomic/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: errgonomic
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.10.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Nick Zadrozny
|
|
@@ -90,6 +90,7 @@ files:
|
|
|
90
90
|
- lib/errgonomic/rails/active_record_optional.rb
|
|
91
91
|
- lib/errgonomic/result.rb
|
|
92
92
|
- lib/errgonomic/type.rb
|
|
93
|
+
- lib/errgonomic/variant_name.rb
|
|
93
94
|
- lib/errgonomic/version.rb
|
|
94
95
|
- sig/errgonomic.rbs
|
|
95
96
|
homepage: https://omc.io/
|