Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,5 @@ jobs:
bundler-cache: true
- name: Run yard doctest
run: bundle exec yard doctest
- name: Run rake test
run: bundle exec rake test
16 changes: 15 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,21 @@ TODO: Write usage instructions here

## Development

After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
After checking out the repo, run `bin/setup` to install dependencies. You can also run `bin/console` for an interactive prompt that will allow you to experiment.

This project encourages **red, green, refactor** when making changes. First, add or change a test that captures the desired behavior; next, run the tests to observe the failure message, confirming the test is useful; next, make the smallest code change(s) to make the test pass. Once tests pass, review your diff and look for opportunities to simplify or improve abstractions; make changes and iterate, running tests on each change to guard against regressions.

Run the doctest suite with:

```bash
nix develop -c yard doctest
```

Run all tests with:

```
nix develop -c rake
```

To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).

Expand Down
9 changes: 6 additions & 3 deletions Rakefile
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,16 @@

require 'bundler/gem_tasks'

require 'rspec/core/rake_task'
RSpec::Core::RakeTask.new(:spec)
require 'rake/testtask'
Rake::TestTask.new(:test) do |t|
t.libs << 'test'
t.test_files = FileList['test/**/*_test.rb']
end

require 'yard/doctest/rake'
YARD::Doctest::RakeTask.new do |task|
task.doctest_opts = %w[-v]
task.pattern = 'lib/**/*.rb'
end

task default: %i[spec yard:doctest]
task default: %i[test yard:doctest]
7 changes: 5 additions & 2 deletions lib/errgonomic.rb
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,9 @@ class NotPresentError < Error; end
class TypeMismatchError < Error; end

class UnwrapError < Error
def initialize(msg, value)
attr_reader :value

def initialize(msg, value = nil)
super(msg)
@value = value
end
Expand All @@ -47,8 +49,9 @@ class NotComparableError < StandardError; end
class SerializeError < TypeError; end

# A little bit of control over how pedantic we are in our runtime type checks.
# Default is false: we are pedantic and raise errors on type mismatches.
def self.give_me_ambiguous_downstream_errors?
@give_me_ambiguous_downstream_errors || true
!!@give_me_ambiguous_downstream_errors
end

# You can opt out of the pedantic runtime checks for lazy block evaluation,
Expand Down
13 changes: 7 additions & 6 deletions lib/errgonomic/option.rb
Original file line number Diff line number Diff line change
Expand Up @@ -257,7 +257,7 @@ def or_else(&block)
return self if some?

val = block.call
if !val.is_a?(Errgonomic::Option::Any) && Errgonomic.give_me_ambiguous_downstream_errors?
if !val.is_a?(Errgonomic::Option::Any) && !Errgonomic.give_me_ambiguous_downstream_errors?
raise Errgonomic::ArgumentError.new, "block must return an Option, was #{val.class.name}"
end

Expand All @@ -275,16 +275,17 @@ def and(other)
other
end

# If self is Some, call the given block and return its value. Block most return an Option.
# If self is Some, call the given block with the inner value and return
# its result. Block must return an Option.
#
# @example
# None().and_then { Some(1) } # => None()
# Some(2).and_then { Some(3) } # => Some(3)
# None().and_then { |x| Some(x + 1) } # => None()
# Some(2).and_then { |x| Some(x + 1) } # => Some(3)
def and_then(&block)
return self if none?

val = block.call
if Errgonomic.give_me_ambiguous_downstream_errors? && !val.is_a?(Errgonomic::Option::Any)
val = block.call(value)
if !Errgonomic.give_me_ambiguous_downstream_errors? && !val.is_a?(Errgonomic::Option::Any)
raise Errgonomic::ArgumentError.new, "block must return an Option, was #{val.class.name}"
end

Expand Down
9 changes: 3 additions & 6 deletions lib/errgonomic/rails.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,9 @@ def self.setup_before
ActiveRecord::Base.include(Errgonomic::Rails::ActiveRecordDelegateOptional)
end

# Wrapping optional associations requires that we include the module after
# the class is first evaluated, so that it can define its associations for
# later reflection.
def self.setup_after
# todo
end
# Models opt in to ActiveRecordOptional individually via
# `include Errgonomic::Rails::ActiveRecordOptional`.
def self.setup_after; end
end

# Hook into Rails with a Railtie
Expand Down
19 changes: 8 additions & 11 deletions lib/errgonomic/result.rb
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ def and_then(&block)
return self if err?

res = block.call(value)
if !res.is_a?(Errgonomic::Result::Any) && Errgonomic.give_me_ambiguous_downstream_errors?
if !res.is_a?(Errgonomic::Result::Any) && !Errgonomic.give_me_ambiguous_downstream_errors?
raise Errgonomic::ArgumentError, 'and_then block must return a Result'
end

Expand Down Expand Up @@ -173,20 +173,18 @@ def or(other)
# Sorry about that, hopefully it helps your tests. Better than ambiguous
# downstream "undefined method" errors, probably.
#
# TODO yield the Err
#
# @param block [Proc]
#
# @example
# Ok(1).or_else { Ok(2) } # => Ok(1)
# Err(:o).or_else { Ok(1) } # => Ok(1)
# Err(:q).or_else { Err(:r) } # => Err(:r)
# Err(:s).or_else { "oops" } # => raise Errgonomic::ArgumentError, "or_else block must return a Result"
# Ok(1).or_else { |e| Ok(2) } # => Ok(1)
# Err(:o).or_else { |e| Ok(1) } # => Ok(1)
# Err(:q).or_else { |e| Err(:r) } # => Err(:r)
# Err(:s).or_else { |e| "oops" } # => raise Errgonomic::ArgumentError, "or_else block must return a Result"
def or_else(&block)
return self if ok?

res = block.call(self)
if !res.is_a?(Errgonomic::Result::Any) && Errgonomic.give_me_ambiguous_downstream_errors?
res = block.call(value)
if !res.is_a?(Errgonomic::Result::Any) && !Errgonomic.give_me_ambiguous_downstream_errors?
raise Errgonomic::ArgumentError, 'or_else block must return a Result'
end

Expand Down Expand Up @@ -249,8 +247,7 @@ def tap_ok(&block)
def map(&block)
return self if err?

@value = block.call(value)
self
Ok(block.call(value))
end

# Refuse to serialize an unwrapped Result as a String. Results must be
Expand Down
7 changes: 3 additions & 4 deletions test/rails_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -36,25 +36,24 @@

class Author < ActiveRecord::Base
has_many :books
include Errgonomic::Rails::ActiveRecordOptional
end

class Book < ActiveRecord::Base
has_many :reviews
has_many :reviewers, through: :reviews, source: :user
belongs_to :author, optional: true

include Errgonomic::Rails::ActiveRecordOptional
delegate_optional :name, to: :author, prefix: true
end

class Genre < ActiveRecord::Base
has_many :books
belongs_to :parent, class_name: 'Genre', optional: true
include Errgonomic::Rails::ActiveRecordOptional
end

# Optional associations have to be defined after the model is evaluated so we
# can reflect on those associations.
Errgonomic::Rails.setup_after

class BugTest < Minitest::Test
def test_optional_attributes
author = Author.create!(name: 'Cixin Liu')
Expand Down
Loading