errgonomic 0.9.3 → 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 +14 -0
- data/README.md +10 -6
- data/lib/errgonomic/option.rb +59 -8
- data/lib/errgonomic/rails/active_record_optional.rb +15 -6
- data/lib/errgonomic/result.rb +70 -11
- 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,19 @@
|
|
|
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
|
+
|
|
3
17
|
## [0.9.3] - 2026-09-10
|
|
4
18
|
|
|
5
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.
|
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`.
|
|
@@ -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
|
```
|
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
|
|
192
|
+
# in Some(value)
|
|
181
193
|
# "Measurement is #{value}"
|
|
182
|
-
# in
|
|
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
|
|
@@ -961,3 +1007,8 @@ end
|
|
|
961
1007
|
def None
|
|
962
1008
|
Errgonomic::Option::None.new
|
|
963
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
|
|
@@ -471,27 +473,79 @@ module Errgonomic
|
|
|
471
473
|
pp.text(inspect)
|
|
472
474
|
end
|
|
473
475
|
|
|
474
|
-
#
|
|
475
|
-
#
|
|
476
|
-
#
|
|
477
|
-
#
|
|
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)
|
|
478
510
|
# "Measurement is #{value}"
|
|
479
|
-
# in
|
|
511
|
+
# in Err(err)
|
|
480
512
|
# "Measurement is not available"
|
|
481
513
|
# end # => "Measurement is 1"
|
|
482
514
|
#
|
|
483
|
-
# @example
|
|
484
|
-
#
|
|
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"))
|
|
485
537
|
# case result
|
|
486
|
-
# in
|
|
538
|
+
# in Ok(value)
|
|
487
539
|
# "Measurement is #{value}"
|
|
488
|
-
# in
|
|
540
|
+
# in Err(String => msg)
|
|
489
541
|
# "Measurement failed with a message: #{msg}"
|
|
490
|
-
# in
|
|
542
|
+
# in Err(Exception => e)
|
|
491
543
|
# "Measurement produced an exception -- #{e.class}: #{e}"
|
|
492
544
|
# end # => "Measurement produced an exception -- StandardError: nope"
|
|
493
545
|
def deconstruct
|
|
494
|
-
[
|
|
546
|
+
return [] if value.equal?(Err::Arbitrary)
|
|
547
|
+
|
|
548
|
+
[value]
|
|
495
549
|
end
|
|
496
550
|
|
|
497
551
|
protected
|
|
@@ -643,3 +697,8 @@ end
|
|
|
643
697
|
def Err(value = Errgonomic::Result::Err::Arbitrary)
|
|
644
698
|
Errgonomic::Result::Err.new(value)
|
|
645
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/
|