diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index f014865..2cd3716 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -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 diff --git a/README.md b/README.md index a6161d5..59add33 100644 --- a/README.md +++ b/README.md @@ -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). diff --git a/Rakefile b/Rakefile index 741248c..a6f85b8 100644 --- a/Rakefile +++ b/Rakefile @@ -2,8 +2,11 @@ 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| @@ -11,4 +14,4 @@ YARD::Doctest::RakeTask.new do |task| task.pattern = 'lib/**/*.rb' end -task default: %i[spec yard:doctest] +task default: %i[test yard:doctest] diff --git a/lib/errgonomic.rb b/lib/errgonomic.rb index 4251eb3..27f253f 100644 --- a/lib/errgonomic.rb +++ b/lib/errgonomic.rb @@ -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 @@ -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, diff --git a/lib/errgonomic/option.rb b/lib/errgonomic/option.rb index eb76830..4b81bf6 100644 --- a/lib/errgonomic/option.rb +++ b/lib/errgonomic/option.rb @@ -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 @@ -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 diff --git a/lib/errgonomic/rails.rb b/lib/errgonomic/rails.rb index 8b473d5..4226904 100644 --- a/lib/errgonomic/rails.rb +++ b/lib/errgonomic/rails.rb @@ -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 diff --git a/lib/errgonomic/result.rb b/lib/errgonomic/result.rb index 0478c2d..97df1e7 100644 --- a/lib/errgonomic/result.rb +++ b/lib/errgonomic/result.rb @@ -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 @@ -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 @@ -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 diff --git a/test/rails_test.rb b/test/rails_test.rb index 041c671..cd84044 100644 --- a/test/rails_test.rb +++ b/test/rails_test.rb @@ -36,6 +36,7 @@ class Author < ActiveRecord::Base has_many :books + include Errgonomic::Rails::ActiveRecordOptional end class Book < ActiveRecord::Base @@ -43,18 +44,16 @@ class Book < ActiveRecord::Base 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')