diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index e9fe282..f014865 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -23,5 +23,5 @@ jobs: with: ruby-version: ${{ matrix.ruby }} bundler-cache: true - - name: Run the default task - run: bundle exec rake spec + - name: Run yard doctest + run: bundle exec yard doctest diff --git a/.rubocop.yml b/.rubocop.yml new file mode 100644 index 0000000..2c01aab --- /dev/null +++ b/.rubocop.yml @@ -0,0 +1 @@ +require: rubocop-yard diff --git a/.yardopts b/.yardopts new file mode 100644 index 0000000..9c072fa --- /dev/null +++ b/.yardopts @@ -0,0 +1 @@ +--plugin yard-doctest diff --git a/Gemfile b/Gemfile index 432b36b..965df16 100644 --- a/Gemfile +++ b/Gemfile @@ -1,12 +1,14 @@ # frozen_string_literal: true -source "https://rubygems.org" +source 'https://rubygems.org' # Specify your gem's dependencies in errgonomic.gemspec gemspec -gem "rake", "~> 13.0" +gem 'rake', '~> 13.0', group: :development +gem 'rspec', '~> 3.0', group: :development +gem 'rubocop', group: :development +gem 'rubocop-yard', group: :development +gem 'solargraph', group: :development -gem "rspec", "~> 3.0" - -gem "standard", "~> 1.3" +# gem "standard", "~> 1.3", group: :development diff --git a/Gemfile.lock b/Gemfile.lock index a1c2d47..ca280a2 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,18 +1,32 @@ PATH remote: . specs: - errgonomic (0.1.0) + errgonomic (0.2.0) concurrent-ruby (~> 1.0) GEM remote: https://rubygems.org/ specs: ast (2.4.2) + backport (1.2.0) + benchmark (0.4.0) concurrent-ruby (1.3.5) diff-lcs (1.6.0) + jaro_winkler (1.6.0) json (2.10.1) + kramdown (2.5.1) + rexml (>= 3.3.9) + kramdown-parser-gfm (1.1.0) + kramdown (~> 2.0) language_server-protocol (3.17.0.4) - lint_roller (1.1.0) + logger (1.6.6) + mini_portile2 (2.8.8) + minitest (5.25.4) + nokogiri (1.18.3) + mini_portile2 (~> 2.8.2) + racc (~> 1.4) + observer (0.1.2) + ostruct (0.6.1) parallel (1.26.3) parser (3.3.7.1) ast (~> 2.4.1) @@ -20,7 +34,12 @@ GEM racc (1.8.1) rainbow (3.1.1) rake (13.2.1) + rbs (3.8.1) + logger regexp_parser (2.10.0) + reverse_markdown (3.0.0) + nokogiri + rexml (3.4.1) rspec (3.13.0) rspec-core (~> 3.13.0) rspec-expectations (~> 3.13.0) @@ -46,35 +65,53 @@ GEM unicode-display_width (>= 2.4.0, < 4.0) rubocop-ast (1.38.1) parser (>= 3.3.1.0) - rubocop-performance (1.23.1) - rubocop (>= 1.48.1, < 2.0) - rubocop-ast (>= 1.31.1, < 2.0) + rubocop-yard (0.10.0) + rubocop (~> 1.21) + yard ruby-progressbar (1.13.0) - standard (1.45.0) - language_server-protocol (~> 3.17.0.2) - lint_roller (~> 1.0) - rubocop (~> 1.71.0) - standard-custom (~> 1.0.0) - standard-performance (~> 1.6) - standard-custom (1.0.2) - lint_roller (~> 1.0) - rubocop (~> 1.50) - standard-performance (1.6.0) - lint_roller (~> 1.1) - rubocop-performance (~> 1.23.0) + solargraph (0.52.0) + backport (~> 1.2) + benchmark + bundler (~> 2.0) + diff-lcs (~> 1.4) + jaro_winkler (~> 1.6) + kramdown (~> 2.3) + kramdown-parser-gfm (~> 1.1) + logger (~> 1.6) + observer (~> 0.1) + ostruct (~> 0.6) + parser (~> 3.0) + rbs (~> 3.0) + reverse_markdown (>= 2.0, < 4) + rubocop (~> 1.38) + thor (~> 1.0) + tilt (~> 2.0) + yard (~> 0.9, >= 0.9.24) + yard-solargraph (~> 0.1) + thor (1.3.2) + tilt (2.6.0) unicode-display_width (3.1.4) unicode-emoji (~> 4.0, >= 4.0.4) unicode-emoji (4.0.4) + yard (0.9.37) + yard-doctest (0.1.17) + minitest + yard + yard-solargraph (0.1.0) + yard (~> 0.9) PLATFORMS - arm64-darwin-24 ruby DEPENDENCIES errgonomic! rake (~> 13.0) rspec (~> 3.0) - standard (~> 1.3) + rubocop + rubocop-yard + solargraph + yard (~> 0.9) + yard-doctest (~> 0.1) BUNDLED WITH 2.5.22 diff --git a/Rakefile b/Rakefile index df40677..741248c 100644 --- a/Rakefile +++ b/Rakefile @@ -1,10 +1,14 @@ # frozen_string_literal: true -require "bundler/gem_tasks" -require "rspec/core/rake_task" +require 'bundler/gem_tasks' +require 'rspec/core/rake_task' RSpec::Core::RakeTask.new(:spec) -require "standard/rake" +require 'yard/doctest/rake' +YARD::Doctest::RakeTask.new do |task| + task.doctest_opts = %w[-v] + task.pattern = 'lib/**/*.rb' +end -task default: %i[spec standard] +task default: %i[spec yard:doctest] diff --git a/bin/console b/bin/console index 395721b..062f92b 100755 --- a/bin/console +++ b/bin/console @@ -1,11 +1,11 @@ #!/usr/bin/env ruby # frozen_string_literal: true -require "bundler/setup" -require "errgonomic" +require 'bundler/setup' +require 'errgonomic' # You can add fixtures and/or initialization code here to make experimenting # with your gem easier. You can also use a different console, if you like. -require "irb" +require 'irb' IRB.start(__FILE__) diff --git a/doctest_helper.rb b/doctest_helper.rb new file mode 100644 index 0000000..5bc96b3 --- /dev/null +++ b/doctest_helper.rb @@ -0,0 +1,3 @@ +# frozen_string_literal: true + +require_relative 'lib/errgonomic' diff --git a/errgonomic.gemspec b/errgonomic.gemspec index 2b6b5fd..22e4b88 100644 --- a/errgonomic.gemspec +++ b/errgonomic.gemspec @@ -1,21 +1,26 @@ # frozen_string_literal: true -require_relative "lib/errgonomic/version" +# when we build in the nix store, version.rb is hashed and adjacent to the gemspec +if __FILE__.include?('/nix/store') && (version_file = Dir.glob('./*-version.rb').first) + require_relative version_file +else + require_relative 'lib/errgonomic/version' +end Gem::Specification.new do |spec| - spec.name = "errgonomic" + spec.name = 'errgonomic' spec.version = Errgonomic::VERSION - spec.authors = ["Nick Zadrozny"] - spec.email = ["nick@onemorecloud.com"] + spec.authors = ['Nick Zadrozny'] + spec.email = ['nick@onemorecloud.com'] - spec.summary = "Opinionated, ergonomic error handling for Ruby, inspired by Rails and Rust." + spec.summary = 'Opinionated, ergonomic error handling for Ruby, inspired by Rails and Rust.' spec.description = "Let's blend the Rails 'present' and 'blank' conventions with a few patterns from Rust Option types." - spec.homepage = "https://omc.io/" - spec.license = "MIT" - spec.required_ruby_version = ">= 3.0.0" + spec.homepage = 'https://omc.io/' + spec.license = 'MIT' + spec.required_ruby_version = '>= 3.0.0' - spec.metadata["homepage_uri"] = spec.homepage - spec.metadata["source_code_uri"] = "https://github.com/omc/errgonomic" + spec.metadata['homepage_uri'] = spec.homepage + spec.metadata['source_code_uri'] = 'https://github.com/omc/errgonomic' # Specify which files should be added to the gem when it is released. # The `git ls-files -z` loads the files in the RubyGem that have been added into git. @@ -26,14 +31,16 @@ Gem::Specification.new do |spec| f.start_with?(*%w[bin/ test/ spec/ features/ .git .github appveyor Gemfile]) end end - spec.bindir = "exe" + spec.bindir = 'exe' spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) } - spec.require_paths = ["lib"] + spec.require_paths = ['lib'] # Uncomment to register a new dependency of your gem # spec.add_dependency "example-gem", "~> 1.0" - spec.add_dependency "concurrent-ruby", "~> 1.0" + spec.add_dependency 'concurrent-ruby', '~> 1.0' + spec.add_development_dependency 'yard', '~> 0.9' + spec.add_development_dependency 'yard-doctest', '~> 0.1' # For more information and examples about making a new gem, check out our # guide at: https://bundler.io/guides/creating_gem.html diff --git a/flake.nix b/flake.nix index e01754b..7b270fa 100644 --- a/flake.nix +++ b/flake.nix @@ -15,13 +15,6 @@ "aarch64-darwin" ]; overlays = [ - (final: prev: { - gems = final.bundlerEnv { - name = "errgonomic"; - gemdir = ./.; - # src = final.lib.cleanSource ../.; - }; - }) ]; forAllSystems = f: @@ -36,12 +29,25 @@ { devShells = forAllSystems ( { pkgs, ... }: + let + inherit (pkgs) ruby bundix; + in { default = pkgs.mkShell { buildInputs = [ - pkgs.ruby - pkgs.bundix - pkgs.gems + ruby + bundix + (pkgs.bundlerEnv { + name = "errgonomic"; + gemdir = ./.; + extraConfigPaths = [ + ./errgonomic.gemspec + ./lib/errgonomic/version.rb + ]; + postInstall = '' + find . >&2 + ''; + }) ]; }; } diff --git a/gemset.nix b/gemset.nix index 146b2c0..22b520a 100644 --- a/gemset.nix +++ b/gemset.nix @@ -9,6 +9,26 @@ }; version = "2.4.2"; }; + backport = { + groups = ["default"]; + platforms = []; + source = { + remotes = ["https://rubygems.org"]; + sha256 = "0xbzzjrgah0f8ifgd449kak2vyf30micpz6x2g82aipfv7ypsb4i"; + type = "gem"; + }; + version = "1.2.0"; + }; + benchmark = { + groups = ["default"]; + platforms = []; + source = { + remotes = ["https://rubygems.org"]; + sha256 = "0jl71qcgamm96dzyqk695j24qszhcc7liw74qc83fpjljp2gh4hg"; + type = "gem"; + }; + version = "0.4.0"; + }; concurrent-ruby = { groups = ["default"]; platforms = []; @@ -37,7 +57,17 @@ path = ./.; type = "path"; }; - version = "0.1.0"; + version = "0.2.0"; + }; + jaro_winkler = { + groups = ["default"]; + platforms = []; + source = { + remotes = ["https://rubygems.org"]; + sha256 = "09645h5an19zc1i7wlmixszj8xxqb2zc8qlf8dmx39bxpas1l24b"; + type = "gem"; + }; + version = "1.6.0"; }; json = { groups = ["default"]; @@ -49,6 +79,28 @@ }; version = "2.10.1"; }; + kramdown = { + dependencies = ["rexml"]; + groups = ["default"]; + platforms = []; + source = { + remotes = ["https://rubygems.org"]; + sha256 = "131nwypz8b4pq1hxs6gsz3k00i9b75y3cgpkq57vxknkv6mvdfw7"; + type = "gem"; + }; + version = "2.5.1"; + }; + kramdown-parser-gfm = { + dependencies = ["kramdown"]; + groups = ["default"]; + platforms = []; + source = { + remotes = ["https://rubygems.org"]; + sha256 = "0a8pb3v951f4x7h968rqfsa19c8arz21zw1vaj42jza22rap8fgv"; + type = "gem"; + }; + version = "1.1.0"; + }; language_server-protocol = { groups = ["default"]; platforms = []; @@ -59,15 +111,66 @@ }; version = "3.17.0.4"; }; - lint_roller = { + logger = { groups = ["default"]; platforms = []; source = { remotes = ["https://rubygems.org"]; - sha256 = "11yc0d84hsnlvx8cpk4cbj6a4dz9pk0r1k29p0n1fz9acddq831c"; + sha256 = "05s008w9vy7is3njblmavrbdzyrwwc1fsziffdr58w9pwqj8sqfx"; type = "gem"; }; - version = "1.1.0"; + version = "1.6.6"; + }; + mini_portile2 = { + groups = ["default"]; + platforms = []; + source = { + remotes = ["https://rubygems.org"]; + sha256 = "0x8asxl83msn815lwmb2d7q5p29p7drhjv5va0byhk60v9n16iwf"; + type = "gem"; + }; + version = "2.8.8"; + }; + minitest = { + groups = ["default" "development"]; + platforms = []; + source = { + remotes = ["https://rubygems.org"]; + sha256 = "0izrg03wn2yj3gd76ck7ifbm9h2kgy8kpg4fk06ckpy4bbicmwlw"; + type = "gem"; + }; + version = "5.25.4"; + }; + nokogiri = { + dependencies = ["mini_portile2" "racc"]; + groups = ["default"]; + platforms = []; + source = { + remotes = ["https://rubygems.org"]; + sha256 = "0npx535cs8qc33n0lpbbwl0p9fi3a5bczn6ayqhxvknh9yqw77vb"; + type = "gem"; + }; + version = "1.18.3"; + }; + observer = { + groups = ["default"]; + platforms = []; + source = { + remotes = ["https://rubygems.org"]; + sha256 = "1b2h1642jy1xrgyakyzz6bkq43gwp8yvxrs8sww12rms65qi18yq"; + type = "gem"; + }; + version = "0.1.2"; + }; + ostruct = { + groups = ["default"]; + platforms = []; + source = { + remotes = ["https://rubygems.org"]; + sha256 = "05xqijcf80sza5pnlp1c8whdaay8x5dc13214ngh790zrizgp8q9"; + type = "gem"; + }; + version = "0.6.1"; }; parallel = { groups = ["default"]; @@ -120,6 +223,17 @@ }; version = "13.2.1"; }; + rbs = { + dependencies = ["logger"]; + groups = ["default"]; + platforms = []; + source = { + remotes = ["https://rubygems.org"]; + sha256 = "07cwjkx7b3ssy8ccqq1s34sc5snwvgxan2ikmp9y2rz2a9wy6v1b"; + type = "gem"; + }; + version = "3.8.1"; + }; regexp_parser = { groups = ["default"]; platforms = []; @@ -130,6 +244,27 @@ }; version = "2.10.0"; }; + reverse_markdown = { + dependencies = ["nokogiri"]; + groups = ["default"]; + platforms = []; + source = { + remotes = ["https://rubygems.org"]; + sha256 = "195c7yra7amggqj7rir0yr09r4v29c2hgkbkb21mj0jsfs3868mb"; + type = "gem"; + }; + version = "3.0.0"; + }; + rexml = { + groups = ["default"]; + platforms = []; + source = { + remotes = ["https://rubygems.org"]; + sha256 = "1jmbf6lf7pcyacpb939xjjpn1f84c3nw83dy3p1lwjx0l2ljfif7"; + type = "gem"; + }; + version = "3.4.1"; + }; rspec = { dependencies = ["rspec-core" "rspec-expectations" "rspec-mocks"]; groups = ["default"]; @@ -206,16 +341,16 @@ }; version = "1.38.1"; }; - rubocop-performance = { - dependencies = ["rubocop" "rubocop-ast"]; - groups = ["default"]; + rubocop-yard = { + dependencies = ["rubocop" "yard"]; + groups = ["development"]; platforms = []; source = { remotes = ["https://rubygems.org"]; - sha256 = "10hv0lz54q34dlwx6vld0qx1fjskfb0nyb5c18cadrpmjnkqcbzj"; + sha256 = "03s8lwah6apkr1g25whhd9y2zrqq9dy56g5kwn0bxp0slakrpisz"; type = "gem"; }; - version = "1.23.1"; + version = "0.10.0"; }; ruby-progressbar = { groups = ["default"]; @@ -227,38 +362,36 @@ }; version = "1.13.0"; }; - standard = { - dependencies = ["language_server-protocol" "lint_roller" "rubocop" "standard-custom" "standard-performance"]; + solargraph = { + dependencies = ["backport" "benchmark" "diff-lcs" "jaro_winkler" "kramdown" "kramdown-parser-gfm" "logger" "observer" "ostruct" "parser" "rbs" "reverse_markdown" "rubocop" "thor" "tilt" "yard" "yard-solargraph"]; groups = ["default"]; platforms = []; source = { remotes = ["https://rubygems.org"]; - sha256 = "13ijzq7r0v0rm1yyba1jfw2s9r3kfxljwypfhzpnkrsag64kk2b5"; + sha256 = "0fqa486hfn6kdbqp3ppy3jvl9xyj8jz41a2dzgkhc6ny2pj31w92"; type = "gem"; }; - version = "1.45.0"; + version = "0.52.0"; }; - standard-custom = { - dependencies = ["lint_roller" "rubocop"]; + thor = { groups = ["default"]; platforms = []; source = { remotes = ["https://rubygems.org"]; - sha256 = "0av55ai0nv23z5mhrwj1clmxpgyngk7vk6rh58d4y1ws2y2dqjj2"; + sha256 = "1nmymd86a0vb39pzj2cwv57avdrl6pl3lf5bsz58q594kqxjkw7f"; type = "gem"; }; - version = "1.0.2"; + version = "1.3.2"; }; - standard-performance = { - dependencies = ["lint_roller" "rubocop-performance"]; + tilt = { groups = ["default"]; platforms = []; source = { remotes = ["https://rubygems.org"]; - sha256 = "1x298w3wmq8cavbsg903wc3arxp3xh2x8263brvy128436m732rd"; + sha256 = "0szpapi229v3scrvw1pgy0vpjm7z3qlf58m1198kxn70cs278g96"; type = "gem"; }; - version = "1.6.0"; + version = "2.6.0"; }; unicode-display_width = { dependencies = ["unicode-emoji"]; @@ -281,4 +414,36 @@ }; version = "4.0.4"; }; + yard = { + groups = ["development"]; + platforms = []; + source = { + remotes = ["https://rubygems.org"]; + sha256 = "14k9lb9a60r9z2zcqg08by9iljrrgjxdkbd91gw17rkqkqwi1sd6"; + type = "gem"; + }; + version = "0.9.37"; + }; + yard-doctest = { + dependencies = ["minitest" "yard"]; + groups = ["development"]; + platforms = []; + source = { + remotes = ["https://rubygems.org"]; + sha256 = "0zw4fa5ri58w76yawh0sc9xj69z26qm59lvjxr1gqn4zv5smmcvw"; + type = "gem"; + }; + version = "0.1.17"; + }; + yard-solargraph = { + dependencies = ["yard"]; + groups = ["default"]; + platforms = []; + source = { + remotes = ["https://rubygems.org"]; + sha256 = "03lklm47k6k294ww97x6zpvlqlyjm5q8jidrixhil622r4cld6m1"; + type = "gem"; + }; + version = "0.1.0"; + }; } diff --git a/lib/errgonomic.rb b/lib/errgonomic.rb index 9b354bf..348e7df 100644 --- a/lib/errgonomic.rb +++ b/lib/errgonomic.rb @@ -1,94 +1,58 @@ # frozen_string_literal: true -require_relative "errgonomic/version" +require_relative 'errgonomic/version' unless defined?(Errgonomic::VERSION) -# The semantics here borrow heavily from ActiveSupport. Let's prefer that if -# loaded, otherwise just copypasta the bits we like. Or convince me to make that -# gem a dependency. -if !Object.methods.include?(:blank?) - require_relative "errgonomic/core_ext/blank" -end +# A more opinionated blend with Rails presence. +require_relative 'errgonomic/presence' + +# Bring in our Option and Result. +require_relative 'errgonomic/option' +require_relative 'errgonomic/result' +# Errgonomic adds opinionated abstractions to handle errors in a way that blends +# Rust and Ruby ergonomics. This library leans on Rails conventions for some +# presence-related methods; when in doubt, make those feel like Rails. It also +# has an implementation of Option and Result; when in doubt, make those feel +# more like Rust. module Errgonomic class Error < StandardError; end class NotPresentError < Error; end class TypeMismatchError < Error; end -end -class Object - # Returns the receiver if it is present, otherwise raises a NotPresentError. - # This method is useful to enforce strong expectations, where it is preferable - # to fail early rather than risk causing an ambiguous error somewhere else. - # - # @param message [String] The error message to raise if the receiver is not present. - # @return [Object] The receiver if it is present, otherwise raises a NotPresentError. - def present_or_raise(message) - raise Errgonomic::NotPresentError, message if blank? - self - end + class UnwrapError < Error; end - # Returns the receiver if it is present, otherwise returns the given value. If - # constructing the default value is expensive, consider using - # +present_or_else+. - # - # @param value [Object] The value to return if the receiver is not present. - # @return [Object] The receiver if it is present, otherwise the given value. - def present_or(value) - # TBD whether this is *too* strict - if value.class != self.class && self.class != NilClass - raise Errgonomic::TypeMismatchError, "Type mismatch: default value is a #{value.class} but original was a #{self.class}" - end + class ExpectError < Error; end - return self if present? + class ArgumentError < Error; end - value - end + class ResultRequiredError < Error; end - # Returns the receiver if it is present, otherwise returns the result of the - # block. Invoking a block may be preferable to returning a default value with - # +present_or+, if constructing the default value is expensive. - # - # @param block [Proc] The block to call if the receiver is not present. - # @return [Object] The receiver if it is present, otherwise the result of the block. - def present_or_else(&block) - return block.call if blank? - self - end + class NotComparableError < StandardError; end - # Returns the receiver if it is blank, otherwise raises a NotPresentError. - # This method is helpful to enforce expectations where blank objects are required. - # - # @param message [String] The error message to raise if the receiver is not blank. - # @return [Object] The receiver if it is blank, otherwise raises a NotPresentError. - def blank_or_raise(message) - raise Errgonomic::NotPresentError, message unless blank? - self + # A little bit of control over how pedantic we are in our runtime type checks. + def self.give_me_ambiguous_downstream_errors? + @give_me_ambiguous_downstream_errors || true end - # Returns the receiver if it is blank, otherwise returns the given value. - # - # @param value [Object] The value to return if the receiver is not blank. - # @return [Object] The receiver if it is blank, otherwise the given value. - def blank_or(value) - # TBD whether this is *too* strict - if value.class != self.class && self.class != NilClass - raise Errgonomic::TypeMismatchError, "Type mismatch: default value is a #{value.class} but original was a #{self.class}" - end - - return self if blank? + # You can opt out of the pedantic runtime checks for lazy block evaluation, + # but not quietly. + def self.with_ambiguous_downstream_errors + original_value = @give_me_ambiguous_downstream_errors + @give_me_ambiguous_downstream_errors = true + yield + ensure + @give_me_ambiguous_downstream_errors = original_value + end - value + # Lenient inner value comparison means the inner value of a Some or Ok can be + # compared to some other non-Result or non-Option value. + def self.lenient_inner_value_comparison? + @lenient_inner_value_comparison ||= true end - # Returns the receiver if it is blank, otherwise returns the result of the - # block. - # - # @param block [Proc] The block to call if the receiver is not blank. - # @return [Object] The receiver if it is blank, otherwise the result of the block. - def blank_or_else(&block) - return block.call unless blank? - self + def self.give_me_lenient_inner_value_comparison=(value) + @lenient_inner_value_comparison = value end end diff --git a/lib/errgonomic/core_ext/blank.rb b/lib/errgonomic/core_ext/blank.rb index 84c7e0a..b784d05 100644 --- a/lib/errgonomic/core_ext/blank.rb +++ b/lib/errgonomic/core_ext/blank.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require "concurrent/map" +require 'concurrent/map' class Object # An object is blank if it's false, empty, or a whitespace string. @@ -99,7 +99,7 @@ class Array # [1,2,3].blank? # => false # # @return [true, false] - alias_method :blank?, :empty? + alias blank? empty? def present? # :nodoc: !empty? @@ -113,7 +113,7 @@ class Hash # { key: 'value' }.blank? # => false # # @return [true, false] - alias_method :blank?, :empty? + alias blank? empty? def present? # :nodoc: !empty? @@ -125,7 +125,7 @@ class Symbol # # :''.blank? # => true # :symbol.blank? # => false - alias_method :blank?, :empty? + alias blank? empty? def present? # :nodoc: !empty? diff --git a/lib/errgonomic/option.rb b/lib/errgonomic/option.rb new file mode 100644 index 0000000..15f0d7f --- /dev/null +++ b/lib/errgonomic/option.rb @@ -0,0 +1,358 @@ +# frozen_string_literal: true + +module Errgonomic + module Option + # The base class for all options. Some and None are subclasses. + # + class Any + # An option of the same type with an equal inner value is equal. + # + # Because we're going to monkey patch this into other libraries Rails, we + # allow some "pass through" functionality into the inner value of a Some, + # such as comparability here. + # + # TODO: does None == null? + # + # strict: + # Some(1) == 1 # => raise Errgonomic::NotComparableError, "Cannot compare Errgonomic::Option::Some with Integer" + # + # @example + # Some(1) == Some(1) # => true + # Some(1) == Some(2) # => false + # Some(1) == None() # => false + # None() == None() # => true + # Some(1) == 1 # => false + # None() == nil # => false + def ==(other) + return false if self.class != other.class + return true if none? + + value == other.value + end + + # @example + # measurement = Errgonomic::Option::Some.new(1) + # case measurement + # in Errgonomic::Option::Some, value + # "Measurement is #{measurement.value}" + # in Errgonomic::Option::None + # "Measurement is not available" + # else + # "not matched" + # end # => "Measurement is 1" + def deconstruct + return [self, value] if some? + + [Errgonomic::Option::None] + end + + # return true if the contained value is Some and the block returns truthy + # + # @example + # Some(1).some_and { |x| x > 0 } # => true + # Some(0).some_and { |x| x > 0 } # => false + # None().some_and { |x| x > 0 } # => false + def some_and(&block) + return false if none? + + !!block.call(value) + end + + alias some_and? some_and + + # return true if the contained value is None or the block returns truthy + # + # @example + # None().none_or { false } # => true + # Some(1).none_or { |x| x > 0 } # => true + # Some(1).none_or { |x| x < 0 } # => false + def none_or(&block) + return true if none? + + !!block.call(value) + end + + alias none_or? none_or + + # return an Array with the contained value, if any + # @example + # Some(1).to_a # => [1] + # None().to_a # => [] + def to_a + return [] if none? + + [value] + end + + # returns the inner value if present, else raises an error + # @example + # Some(1).unwrap! # => 1 + # None().unwrap! # => raise Errgonomic::UnwrapError, "cannot unwrap None" + def unwrap! + raise Errgonomic::UnwrapError, 'cannot unwrap None' if none? + + value + end + + # returns the inner value if pressent, else raises an error with the given + # message + # @example + # Some(1).expect!("msg") # => 1 + # None().expect!("msg") # => raise Errgonomic::ExpectError, "msg" + def expect!(msg) + raise Errgonomic::ExpectError, msg if none? + + value + end + + # returns the inner value if present, else returns the default value + # @example + # Some(1).unwrap_or(2) # => 1 + # None().unwrap_or(2) # => 2 + def unwrap_or(default) + return default if none? + + value + end + + # returns the inner value if present, else returns the result of the + # provided block + # @example + # Some(1).unwrap_or_else { 2 } # => 1 + # None().unwrap_or_else { 2 } # => 2 + def unwrap_or_else(&block) + return block.call if none? + + value + end + + # Calls a function with the inner value, if Some, but returns the original + # option. In Rust, this is "inspect" but that clashes with Ruby + # conventions. We call this "tap_some" to avoid further clashing with + # "tap." + # + # @example + # tapped = false + # Some(1).tap_some { |x| tapped = x } # => Some(1) + # tapped # => 1 + # tapped = false + # None().tap_some { tapped = true } # => None() + # tapped # => false + def tap_some(&block) + block.call(value) if some? + self + end + + # Maps the Option to another Option by applying a function to the + # contained value (if Some) or returns None. Raises a pedantic exception + # if the return value of the block is not an Option. + # + # @example + # Some(1).map { |x| x + 1 } # => Some(2) + # None().map { |x| x + 1 } # => None() + def map(&block) + return self if none? + + Some(block.call(value)) + end + + # Returns the provided default (if none), or applies a function to the + # contained value (if some). If you want lazy evaluation for the provided + # value, use +map_or_else+. + # + # @example + # None().map_or(1) { 100 } # => Some(1) + # Some(1).map_or(100) { |x| x + 1 } # => Some(2) + # Some("foo").map_or(0) { |str| str.length } # => Some(3) + def map_or(default, &block) + return Some(default) if none? + + Some(block.call(value)) + end + + # Computes a default from the given Proc if None, or applies the block to + # the contained value (if Some). + # + # @example + # None().map_or_else(-> { :foo }) { :bar } # => Some(:foo) + # Some("str").map_or_else(-> { 100 }) { |str| str.length } # => Some(3) + # None().map_or_else( -> { nil }) { |str| str.length } # => None() + def map_or_else(proc, &block) + if none? + val = proc.call + return val ? Some(val) : None() + end + + Some(block.call(value)) + end + + # convert the option into a result where Some is Ok and None is Err + # @example + # None().ok # => Err() + # Some(1).ok # => Ok(1) + def ok + return Errgonomic::Result::Ok.new(value) if some? + + Errgonomic::Result::Err.new + end + + # Transforms the option into a result, mapping Some(v) to Ok(v) and None to Err(err) + # + # @example + # None().ok_or("wow") # => Err("wow") + # Some(1).ok_or("such err") # => Ok(1) + def ok_or(err) + return Errgonomic::Result::Ok.new(value) if some? + + Errgonomic::Result::Err.new(err) + end + + # Transforms the option into a result, mapping Some(v) to Ok(v) and None to Err(err). + # TODO: block or proc? + # + # @example + # None().ok_or_else { "wow" } # => Err("wow") + # Some("foo").ok_or_else { "such err" } # => Ok("foo") + def ok_or_else(&block) + return Errgonomic::Result::Ok.new(value) if some? + + Errgonomic::Result::Err.new(block.call) + end + + # Returns the option if it contains a value, otherwise returns the provided Option. Returns an Option. + # + # @example + # None().or(Some(1)) # => Some(1) + # Some(2).or(Some(3)) # => Some(2) + # None().or(2) # => raise Errgonomic::ArgumentError.new, "other must be an Option, was Integer" + def or(other) + raise ArgumentError, "other must be an Option, was #{other.class.name}" unless other.is_a?(Any) + + return self if some? + + other + end + + # Returns the option if it contains a value, otherwise calls the block and returns the result. Returns an Option. + # + # @example + # None().or_else { Some(1) } # => Some(1) + # Some(2).or_else { Some(3) } # => Some(2) + # None().or_else { 2 } # => raise Errgonomic::ArgumentError.new, "block must return an Option, was Integer" + def or_else(&block) + return self if some? + + val = block.call + 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 + + val + end + + # If self is Some, return the provided other Option. + # + # @example + # None().and(Some(1)) # => None() + # Some(2).and(Some(3)) # => Some(3) + def and(other) + return self if none? + + other + end + + # If self is Some, call the given block and return its value. Block most return an Option. + # + # @example + # None().and_then { Some(1) } # => None() + # Some(2).and_then { Some(3) } # => 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) + raise Errgonomic::ArgumentError.new, "block must return an Option, was #{val.class.name}" + end + + val + end + + # Zips self with another Option. + # + # If self is Some(s) and other is Some(o), this method returns + # Some([s, o]). Otherwise, None is returned. + # + # @example + # None().zip(Some(1)) # => None() + # Some(1).zip(None()) # => None() + # Some(2).zip(Some(3)) # => Some([2, 3]) + def zip(other) + return None() unless some? && other.some? + + Some([value, other.value]) + end + + # Zip two options using the block passed. If self is Some and Other is + # some, yield both of their values to the block and return its value as + # Some. Else return None. + # + # @example + # None().zip_with(Some(1)) { |a, b| a + b } # => None() + # Some(1).zip_with(None()) { |a, b| a + b } # => None() + # Some(2).zip_with(Some(3)) { |a, b| a + b } # => Some(5) + def zip_with(other, &block) + return None() unless some? && other.some? + other = block.call(value, other.value) + Some(other) + end + + # filter + # xor + # insert + # get_or_insert + # get_or_insert_with + # take + # take_if + # replace + # zip + # zip_with + end + + # Represent a value + class Some < Any + attr_accessor :value + + def initialize(value) + @value = value + end + + def some? + true + end + + def none? + false + end + end + + class None < Any + def some? + false + end + + def none? + true + end + end + end +end + +# Global convenience for constructing a Some value. +def Some(value) + Errgonomic::Option::Some.new(value) +end + +# Global convenience for constructing a None value. +def None + Errgonomic::Option::None.new +end diff --git a/lib/errgonomic/presence.rb b/lib/errgonomic/presence.rb new file mode 100644 index 0000000..d123261 --- /dev/null +++ b/lib/errgonomic/presence.rb @@ -0,0 +1,88 @@ +# frozen_string_literal: true + +# The semantics here borrow heavily from ActiveSupport. Let's prefer that if +# loaded, otherwise just copypasta the bits we like. Or convince me to make that +# gem a dependency. +require_relative './core_ext/blank' unless Object.methods.include?(:blank?) + +class Object + # Returns the receiver if it is present, otherwise raises a NotPresentError. + # This method is useful to enforce strong expectations, where it is preferable + # to fail early rather than risk causing an ambiguous error somewhere else. + # + # @param message [String] The error message to raise if the receiver is not present. + # @return [Object] The receiver if it is present, otherwise raises a NotPresentError. + def present_or_raise(message) + raise Errgonomic::NotPresentError, message if blank? + + self + end + + # Returns the receiver if it is present, otherwise returns the given value. If + # constructing the default value is expensive, consider using + # +present_or_else+. + # + # @param value [Object] The value to return if the receiver is not present. + # @return [Object] The receiver if it is present, otherwise the given value. + def present_or(value) + # TBD whether this is *too* strict + if value.class != self.class && self.class != NilClass + raise Errgonomic::TypeMismatchError, + "Type mismatch: default value is a #{value.class} but original was a #{self.class}" + end + + return self if present? + + value + end + + # Returns the receiver if it is present, otherwise returns the result of the + # block. Invoking a block may be preferable to returning a default value with + # +present_or+, if constructing the default value is expensive. + # + # @param block [Proc] The block to call if the receiver is not present. + # @return [Object] The receiver if it is present, otherwise the result of the block. + def present_or_else(&block) + return block.call if blank? + + self + end + + # Returns the receiver if it is blank, otherwise raises a NotPresentError. + # This method is helpful to enforce expectations where blank objects are required. + # + # @param message [String] The error message to raise if the receiver is not blank. + # @return [Object] The receiver if it is blank, otherwise raises a NotPresentError. + def blank_or_raise(message) + raise Errgonomic::NotPresentError, message unless blank? + + self + end + + # Returns the receiver if it is blank, otherwise returns the given value. + # + # @param value [Object] The value to return if the receiver is not blank. + # @return [Object] The receiver if it is blank, otherwise the given value. + def blank_or(value) + # TBD whether this is *too* strict + if value.class != self.class && self.class != NilClass + raise Errgonomic::TypeMismatchError, + "Type mismatch: default value is a #{value.class} but original was a #{self.class}" + end + + return self if blank? + + value + end + + # Returns the receiver if it is blank, otherwise returns the result of the + # block. + # + # @param block [Proc] The block to call if the receiver is not blank. + # @return [Object] The receiver if it is blank, otherwise the result of the block. + def blank_or_else(&block) + return block.call unless blank? + + self + end +end diff --git a/lib/errgonomic/result.rb b/lib/errgonomic/result.rb new file mode 100644 index 0000000..2aa758c --- /dev/null +++ b/lib/errgonomic/result.rb @@ -0,0 +1,316 @@ +# frozen_string_literal: true + +module Errgonomic + module Result + # The base class for Result's Ok and Err class variants. We implement as + # much logic as possible here, and let Ok and Err handle their + # initialization and self identification. + class Any + attr_reader :value + + def initialize(value) + @value = value + end + + # Equality comparison for Result objects is based on value not reference. + # + # @param other [Object] + # + # @example + # Ok(1) == Ok(1) # => true + # Ok(1) == Err(1) # => false + # Ok(1).object_id == Ok(1).object_id # => false + # Ok(1) == 1 # => false + # Err() == nil # => false + def ==(other) + return false if self.class != other.class + + value == other.value + end + + # Indicate that this is some kind of result object. Contrast to + # Object#result? which is false for all other types. + # + # @example + # Ok("a").result? # => true + # Err("a").result? # => true + # "a".result? # => false + def result? + true + end + + # Return true if the inner value is an Ok and the result of the block is + # truthy. + # + # @param [Proc] block The block to evaluate if the inner value is an Ok. + # + # @example + # Ok(1).ok_and?(&:odd?) # => true + # Ok(1).ok_and?(&:even?) # => false + # Err(:a).ok_and? { |_| true } # => false + # Err(:b).ok_and? { |_| false } # => false + def ok_and?(&block) + if ok? + !!block.call(value) + else + false + end + end + + # Return true if the inner value is an Err and the result of the block is + # truthy. + # + # @example + # Ok(1).err_and?(&:odd?) # => false + # Ok(1).err_and?(&:even?) # => false + # Err(:a).err_and? { |_| true } # => true + # Err(:b).err_and? { |_| false } # => false + def err_and?(&block) + if err? + !!block.call(value) + else + false + end + end + + # Return the inner value of an Ok, else raise an exception when Err. + # + # @example + # Ok(1).unwrap! # => 1 + # Err(:c).unwrap! # => raise Errgonomic::UnwrapError, "value is an Err" + def unwrap! + raise Errgonomic::UnwrapError, 'value is an Err' unless ok? + + @value + end + + # Return the inner value of an Ok, else raise an exception with the given + # message when Err. + # + # @param msg [String] + # + # @example + # Ok(1).expect!("should have worked") # => 1 + # Err(:d).expect!("should have worked") # => raise Errgonomic::ExpectError, "should have worked" + def expect!(msg) + raise Errgonomic::ExpectError, msg unless ok? + + @value + end + + # Return the inner value of an Err, else raise an exception when Ok. + # + # @example + # Ok(1).unwrap_err! # => raise Errgonomic::UnwrapError, 1 + # Err(:e).unwrap_err! # => :e + def unwrap_err! + raise Errgonomic::UnwrapError, value unless err? + + @value + end + + # Given another result, return it if the inner result is Ok, else return + # the inner Err. Raise an exception if the other value is not a Result. + # + # @param other [Errgonomic::Result::Any] + # + # @example + # Ok(1).and(Ok(2)) # => Ok(2) + # Ok(1).and(Err(:f)) # => Err(:f) + # Err(:g).and(Ok(1)) # => Err(:g) + # Err(:h).and(Err(:i)) # => Err(:h) + # Ok(1).and(2) # => raise Errgonomic::ArgumentError, "other must be a Result" + def and(other) + raise Errgonomic::ArgumentError, 'other must be a Result' unless other.is_a?(Errgonomic::Result::Any) + return self if err? + + other + end + + # Given a block, evaluate it and return its result if the inner result is + # Ok, else return the inner Err. This is lazy evaluated, and we + # pedantically check the type of the block's return value at runtime. This + # is annoying, sorry, but better than an "undefined method" error. + # Hopefully it gives your test suite a chance to detect incorrect usage. + # + # @param block [Proc] + # + # @example + # Ok(1).and_then { |x| Ok(x + 1) } # => Ok(2) + # Ok(1).and_then { |_| Err(:error) } # => Err(:error) + # Err(:error).and_then { |x| Ok(x + 1) } # => Err(:error) + # Err(:error).and_then { |x| Err(:error2) } # => Err(:error) + 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? + raise Errgonomic::ArgumentError, 'and_then block must return a Result' + end + + res + end + + # Return other if self is Err, else return the original Option. Raises a + # pedantic runtime exception if other is not a Result. + # + # @param other [Errgonomic::Result::Any] + # + # @example + # Err(:j).or(Ok(1)) # => Ok(1) + # Err(:k).or(Err(:l)) # => Err(:l) + # Err(:m).or("oops") # => raise Errgonomic::ArgumentError, "other must be a Result; you might want unwrap_or" + def or(other) + unless other.is_a?(Errgonomic::Result::Any) + raise Errgonomic::ArgumentError, + 'other must be a Result; you might want unwrap_or' + end + return other if err? + + self + end + + # Return self if it is Ok, else lazy evaluate the block and return its + # result. Raises a pedantic runtime check that the block returns a Result. + # Sorry about that, hopefully it helps your tests. Better than ambiguous + # downstream "undefined method" errors, probably. + # + # @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" + 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? + raise Errgonomic::ArgumentError, 'or_else block must return a Result' + end + + res + end + + # Return the inner value if self is Ok, else return the provided default. + # + # @param other [Object] + # + # @example + # Ok(1).unwrap_or(2) # => 1 + # Err(:t).unwrap_or(:u) # => :u + def unwrap_or(other) + return value if ok? + + other + end + + # Return the inner value if self is Ok, else lazy evaluate the block and + # return its result. + # + # @param block [Proc] + # + # @example + # Ok(1).unwrap_or_else { 2 } # => 1 + # Err(:v).unwrap_or_else { :w } # => :w + def unwrap_or_else(&block) + return value if ok? + + block.call(self) + end + end + + # The Ok variant. + class Ok < Any + attr_accessor :value + + # Ok is always ok + # + # @example + # Ok(1).ok? # => true + def ok? + true + end + + # Ok is never err + # + # @example + # Ok(1).err? # => false + def err? + false + end + end + + class Err < Any + class Arbitrary; end + + attr_accessor :value + + # Err may be constructed without a value, if you want. + # + # @example + # Err(:y).value # => :y + # Err().value # => Arbitrary + def initialize(value = Arbitrary) + super(value) + end + + # Err is always err + # + # @example + # Err(:z).err? # => true + def err? + true + end + + # Err is never ok + # + # @example + # Err(:A).ok? # => false + def ok? + false + end + end + end +end + +# Introduce certain helper methods into the Object class. +# +# @example +# "foo".result? # => false +# "foo".assert_result! # => raise Errgonomic::ResultRequiredError +class Object + # Convenience method to indicate whether we are working with a result. + # TBD whether we implement some stubs for the rest of the Result API; I want + # to think about how effectively these map to truthiness or presence. + # + # @example + # "foo".result? # => false + # Ok("foo").result? # => true + def result? + false + end + + # Lacking static typing, we are going to want to make it easy to enforce at + # runtime that a given object is a Result. + # + # @example + # "foo".assert_result! # => raise Errgonomic::ResultRequiredError + # Ok("foo").assert_result! # => true + def assert_result! + return true if result? + + raise Errgonomic::ResultRequiredError + end +end + +# Global convenience method for constructing an Ok result. +def Ok(value) + Errgonomic::Result::Ok.new(value) +end + +# Global convenience method for constructing an Err result. +def Err(value = Errgonomic::Result::Err::Arbitrary) + Errgonomic::Result::Err.new(value) +end diff --git a/lib/errgonomic/version.rb b/lib/errgonomic/version.rb index a2389e7..2c8e883 100644 --- a/lib/errgonomic/version.rb +++ b/lib/errgonomic/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module Errgonomic - VERSION = "0.1.0" + VERSION = '0.2.0' end diff --git a/spec/errgonomic_spec.rb b/spec/errgonomic_spec.rb deleted file mode 100644 index 6654ddd..0000000 --- a/spec/errgonomic_spec.rb +++ /dev/null @@ -1,93 +0,0 @@ -# frozen_string_literal: true - -RSpec.describe Errgonomic do - it "has a version number" do - expect(Errgonomic::VERSION).not_to be nil - end - - describe "present_or_raise" do - it "raises an error for various blank objects" do - expect { nil.present_or_raise("foo") }.to raise_error(Errgonomic::NotPresentError) - expect { [].present_or_raise("foo") }.to raise_error(Errgonomic::NotPresentError) - expect { {}.present_or_raise("foo") }.to raise_error(Errgonomic::NotPresentError) - end - - it "returns the value itself for present types" do - expect("bar".present_or_raise("foo")).to eq("bar") - expect(["baz"].present_or_raise("foo")).to eq(["baz"]) - expect({foo: "bar"}.present_or_raise("foo")).to eq({foo: "bar"}) - end - end - - describe "present_or" do - it "returns the default value for various blank objects" do - expect(nil.present_or("foo")).to eq("foo") - expect([].present_or(["foo"])).to eq(["foo"]) - expect({}.present_or({foo: "bar"})).to eq({foo: "bar"}) - end - - it "rather strictly requires the value to match the starting type, except for nil" do - expect(nil.present_or("foo")).to eq("foo") - expect { [].present_or("bar") }.to raise_error(Errgonomic::TypeMismatchError) - expect { {}.present_or("bar") }.to raise_error(Errgonomic::TypeMismatchError) - end - - it "even more strictly will fail when default value is not the same type as the original non-blank value" do - expect { ["foo"].present_or("bad") }.to raise_error(Errgonomic::TypeMismatchError) - expect { {foo: "bar"}.present_or("bad") }.to raise_error(Errgonomic::TypeMismatchError) - end - - it "returns the value itself for present types" do - expect("bar".present_or("foo")).to eq("bar") - expect(["baz"].present_or(["foo"])).to eq(["baz"]) - expect({foo: "bar"}.present_or({foo: "baz"})).to eq({foo: "bar"}) - end - end - - describe "blank_or_raise" do - it "raises an error for present objects" do - expect { "bar".blank_or_raise("foo") }.to raise_error(Errgonomic::NotPresentError) - expect { ["baz"].blank_or_raise("foo") }.to raise_error(Errgonomic::NotPresentError) - expect { { foo: "bar" }.blank_or_raise("foo") }.to raise_error(Errgonomic::NotPresentError) - end - - it "returns the value itself for blank types" do - expect(nil.blank_or_raise("foo")).to eq(nil) - expect([].blank_or_raise("foo")).to eq([]) - expect({}.blank_or_raise("foo")).to eq({}) - end - end - - describe "blank_or" do - it "returns the receiver for blank objects" do - expect(nil.blank_or("foo")).to eq(nil) - expect([].blank_or(["foo"])).to eq([]) - expect({}.blank_or({ foo: "bar" })).to eq({}) - end - - it "returns the default value for present objects" do - expect("bar".blank_or("foo")).to eq("foo") - expect(["baz"].blank_or(["foo"])).to eq(["foo"]) - expect({ foo: "bar" }.blank_or({ foo: "baz" })).to eq({ foo: "baz" }) - end - - it "enforces type checks similar to present_or" do - expect { "bar".blank_or(["foo"]) }.to raise_error(Errgonomic::TypeMismatchError) - expect { [].blank_or("foo") }.to raise_error(Errgonomic::TypeMismatchError) - end - end - - describe "blank_or_else" do - it "returns the receiver for blank objects" do - expect(nil.blank_or_else { "foo" }).to eq(nil) - expect([].blank_or_else { ["foo"] }).to eq([]) - expect({}.blank_or_else { { foo: "bar" } }).to eq({}) - end - - it "returns the result of the block for present objects" do - expect("bar".blank_or_else { "foo" }).to eq("foo") - expect(["baz"].blank_or_else { ["foo"] }).to eq(["foo"]) - expect({ foo: "bar" }.blank_or_else { { foo: "baz" } }).to eq({ foo: "baz" }) - end - end -end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb deleted file mode 100644 index 1eaae8f..0000000 --- a/spec/spec_helper.rb +++ /dev/null @@ -1,15 +0,0 @@ -# frozen_string_literal: true - -require "errgonomic" - -RSpec.configure do |config| - # Enable flags like --only-failures and --next-failure - config.example_status_persistence_file_path = ".rspec_status" - - # Disable RSpec exposing methods globally on `Module` and `main` - config.disable_monkey_patching! - - config.expect_with :rspec do |c| - c.syntax = :expect - end -end