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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2b6ae05f6601a1d5470b1cb9cd9266e3937b91fd2ea2bab8eeac469959b24e90
4
- data.tar.gz: 5b3095f01409a6275b500df7d08e0c8b23b50062924d61ada8a4a6c98e0b69fe
3
+ metadata.gz: d39f466feedb411b53780fa853b509dc6a40db2f12aef63235a6817881ce2d95
4
+ data.tar.gz: bc81bba0f57ddaac60178b0319533feeb054be296614332b8a9744d72b0ae9d1
5
5
  SHA512:
6
- metadata.gz: a74d4cdf58805801e853a59329b076ca43c553da3a670fc56762fea5d755da898c6a6951e2c697fcb91b28930382d2f0ff6e3e2a85038ad33a5a3a0d4567c6c3
7
- data.tar.gz: d636451a6addbfc3e2af62974e659fe7de490dd7b4176f3cf2f53c6f93dd6d620244f14e99a684817a160a4715c02ed4b5d0b003bded913f5962e64987d674cc
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 support pattern matching:
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 Errgonomic::Option::Some, value
118
+ in Some(value)
119
119
  "Measurement is #{value}"
120
- in Errgonomic::Option::None
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 Errgonomic::Result::Ok, value
179
+ in Ok(value)
176
180
  "Measurement is #{value}"
177
- in Errgonomic::Result::Err, String => msg
181
+ in Err(String => msg)
178
182
  "Measurement failed with a message: #{msg}"
179
- in Errgonomic::Result::Err, Exception => e
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 << member.value
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 << member.value
90
+ member.tap_ok { |value| values << value }
91
91
  end
92
92
  Ok(values)
93
93
  end
@@ -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
- # measurement = Errgonomic::Option::Some.new(1)
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 Errgonomic::Option::Some, value
181
- # "Measurement is #{measurement.value}"
182
- # in Errgonomic::Option::None
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
- return [self, value] if some?
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
- attr_accessor :value
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.unwrap_or(nil)
519
+ when Errgonomic::Option::Any, Errgonomic::VariantName
520
+ unwrap_option(value)
521
521
  when Array
522
- value.any? { |v| v.is_a?(Errgonomic::Option::Any) } ? value.map { |v| unwrap_options(v) } : value
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?(Errgonomic::Option::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?(Errgonomic::Option::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
@@ -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
- attr_reader :value
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
- # @example simple pattern match with variable capture of the value
458
- # result = Errgonomic::Result::Ok.new(1)
459
- # case result
460
- # in Errgonomic::Result::Ok, value
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 Errgonomic::Result::Err, err
511
+ # in Err(err)
463
512
  # "Measurement is not available"
464
513
  # end # => "Measurement is 1"
465
514
  #
466
- # @example more advanced pattern match against the kind of value
467
- # result = Errgonomic::Result::Err.new(StandardError.new("nope"))
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 Errgonomic::Result::Ok, value
538
+ # in Ok(value)
470
539
  # "Measurement is #{value}"
471
- # in Errgonomic::Result::Err, String => msg
540
+ # in Err(String => msg)
472
541
  # "Measurement failed with a message: #{msg}"
473
- # in Errgonomic::Result::Err, Exception => e
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
- [self, value]
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).value # => :y
553
- # Err().value # => Arbitrary
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Errgonomic
4
- VERSION = '0.9.2'
4
+ VERSION = '0.10.0'
5
5
  end
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.9.2
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/