philiprehberger-result

Tests Gem Version Last updated

Result type with Ok/Err, map, flat_map, and pattern matching

Requirements

  • Ruby >= 3.1

Installation

Add to your Gemfile:

gem "philiprehberger-result"

Or install directly:

gem install philiprehberger-result

Usage

require "philiprehberger/result"

# Create results
ok = Philiprehberger::Result.ok(42)
err = Philiprehberger::Result.err("not found")

ok.ok?   # => true
err.err? # => true

Transformations

result = Philiprehberger::Result.ok(5)

result.map { |v| v * 2 }          # => Ok(10)
result.flat_map { |v| Result.ok(v + 1) } # => Ok(6)

err = Philiprehberger::Result.err("bad input")
err.map { |v| v * 2 }             # => Err("bad input") (no-op)
err.map_err { |e| e.upcase }      # => Err("BAD INPUT")

Unwrapping

ok = Philiprehberger::Result.ok(42)
ok.unwrap!          # => 42
ok.unwrap_or(0)     # => 42

err = Philiprehberger::Result.err("fail")
err.unwrap_or(0)    # => 0
err.unwrap!         # raises UnwrapError

Exception-safe wrapping

result = Philiprehberger::Result.try { Integer("123") }
# => Ok(123)

result = Philiprehberger::Result.try { Integer("abc") }
# => Err(#<ArgumentError: invalid value for Integer(): "abc">)

# Catch specific exceptions
result = Philiprehberger::Result.try(IOError) { File.read("missing") }

Side Effects

# Execute a side-effect on Ok without changing the result
Philiprehberger::Result.ok(42)
  .tap_ok { |v| puts "Got value: #{v}" }
  .map { |v| v * 2 }
# prints "Got value: 42", returns Ok(84)

# Execute a side-effect on Err without changing the result
Philiprehberger::Result.err("fail")
  .tap_err { |e| logger.error(e) }
  .or_else { |e| Philiprehberger::Result.ok("default") }
# logs "fail", returns Ok("default")

Filtering

result = Philiprehberger::Result.ok(5)

# Convert Ok to Err when predicate fails
result.filter(-> { "must be negative" }) { |v| v < 0 }
# => Err("must be negative")

result.filter(-> { "must be positive" }) { |v| v > 0 }
# => Ok(5)

# Err passes through unchanged
Philiprehberger::Result.err("already failed")
  .filter(-> { "unused" }) { |v| v > 0 }
# => Err("already failed")

Combining Results

ok1 = Philiprehberger::Result.ok(1)
ok2 = Philiprehberger::Result.ok(2)
err = Philiprehberger::Result.err("fail")

Philiprehberger::Result.all([ok1, ok2])
# => Ok([1, 2])

Philiprehberger::Result.all([ok1, err, ok2])
# => Err("fail")

Error Recovery

result = Philiprehberger::Result.err("not found")

# Recover with or_else
recovered = result.or_else { |e| Philiprehberger::Result.ok("default") }
# => Ok("default")

# Chain with and_then (alias for flat_map)
Philiprehberger::Result.ok(5)
  .and_then { |v| Philiprehberger::Result.ok(v * 2) }
  .and_then { |v| Philiprehberger::Result.ok(v + 1) }
# => Ok(11)

Serialization

Philiprehberger::Result.ok(42).to_h    # => { ok: 42 }
Philiprehberger::Result.err("fail").to_h # => { err: "fail" }

First Success with any

results = [Result.err("timeout"), Result.ok(42), Result.ok(99)]
Result.any(results).unwrap!  # => 42

Combining with zip

a = Result.ok(1)
b = Result.ok(2)
a.zip(b).unwrap!  # => [1, 2]

Flattening

nested = Philiprehberger::Result.ok(Philiprehberger::Result.ok(42))
Philiprehberger::Result.flatten(nested).unwrap!  # => 42

nested = Philiprehberger::Result.ok(Philiprehberger::Result.err("fail"))
Philiprehberger::Result.flatten(nested).err?     # => true

Typed Recovery

Result.err(ArgumentError.new("bad"))
  .recover(ArgumentError) { |e| "default" }
  .unwrap!  # => "default"

Maybe Conversion

# Extract the value, collapsing Err to nil
Philiprehberger::Result.ok(42).to_maybe   # => 42
Philiprehberger::Result.err("fail").to_maybe # => nil

# Note: Ok(nil) and Err(anything) both return nil
Philiprehberger::Result.ok(nil).to_maybe  # => nil

Containment Checks

# Test whether an Ok holds a specific value
Philiprehberger::Result.ok(42).contains?(42)      # => true
Philiprehberger::Result.ok(42).contains?(99)      # => false
Philiprehberger::Result.err("fail").contains?(42) # => false

# Test whether an Err holds a specific error
Philiprehberger::Result.err("fail").contains_err?("fail")  # => true
Philiprehberger::Result.err("fail").contains_err?("other") # => false
Philiprehberger::Result.ok(42).contains_err?("fail")       # => false

Partitioning Results

results = [
  Philiprehberger::Result.ok(1),
  Philiprehberger::Result.err("a"),
  Philiprehberger::Result.ok(2),
  Philiprehberger::Result.err("b")
]

values, errors = Philiprehberger::Result.partition(results)
# values => [1, 2]
# errors => ["a", "b"]

Pattern Matching

case Philiprehberger::Result.ok(42)
in Philiprehberger::Result::Ok[value]
  puts "Success: #{value}"
in Philiprehberger::Result::Err[error]
  puts "Error: #{error}"
end

API

Method / Class Description
Result.ok(value) Create a success result
Result.err(error) Create a failure result
Result.try(*exceptions, &block) Wrap a block, capturing exceptions as Err
Result.all(results) Combine results: Ok with values array, or first Err
`Ok#map { \ v\
`Ok#flat_map { \ v\
Ok#unwrap! Return the value
Ok#unwrap_or(default) Return the value (ignores default)
`#tap_ok { \ v\
`#tap_err { \ e\
`#filter(error_fn) { \ v\
`Err#map_err { \ e\
Err#unwrap! Raise UnwrapError
Err#unwrap_or(default) Return the default
`Ok#or_else { \ e\
`Err#or_else { \ e\
`#and_then { \ v\
Result.any(results) First Ok, or Err with all errors
#zip(other) Combine two Ok results into Ok([a, b])
#recover(error_class) { block } Recover from specific error types
Ok#unwrap_err! Raise UnwrapError (no error to extract)
Err#unwrap_err! Return the error value
Result.flatten(result) Flatten nested Result (Ok(Ok(v)) to Ok(v))
`#map_or(default) { \ v\
#to_h Serialize to { ok: value } or { err: error }
#to_maybe Return the value on Ok, nil on Err
#contains?(value) Return true if Ok equals value
#contains_err?(error) Return true if Err equals error
Result.partition(results) Split into [ok_values, err_values] arrays

Development

bundle install
bundle exec rspec
bundle exec rubocop

Support

If you find this project useful:

Star the repo

🐛 Report issues

💡 Suggest features

❤️ Sponsor development

🌐 All Open Source Projects

💻 GitHub Profile

🔗 LinkedIn Profile

License

MIT