-
-
Notifications
You must be signed in to change notification settings - Fork 135
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
This commit introduces an RSpec extension for Dry::Monads, providing: - Matchers for success, failure, some, and none monad types - Constructors for creating monad instances in specs - Support for catching missing constants in spec files
- Loading branch information
1 parent
342cc3c
commit 826e737
Showing
6 changed files
with
291 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -17,5 +17,6 @@ group :docs do | |
end | ||
|
||
group :test do | ||
gem "debug_inspector" | ||
gem "dry-types" | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
# frozen_string_literal: true | ||
|
||
Dry::Monads.extend(Dry::Core::Extensions) | ||
|
||
Dry::Monads.register_extension(:rspec) do | ||
require "dry/monads/extensions/rspec" | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,191 @@ | ||
# frozen_string_literal: true | ||
|
||
debug_inspector_available = | ||
begin | ||
require "debug_inspector" | ||
defined?(DebugInspector) | ||
rescue LoadError | ||
false | ||
end | ||
|
||
module Dry | ||
module Monads | ||
module RSpec | ||
module Matchers | ||
class BeSuccessMatcher | ||
def initialize(value = Undefined) | ||
@value = value | ||
end | ||
|
||
def matches?(actual) | ||
if Undefined.equal?(@value) | ||
actual.success? | ||
else | ||
actual.eql?(Result::Success.new(@value)) | ||
end | ||
end | ||
|
||
def failure_message | ||
if Undefined.equal?(@value) | ||
"expected #{actual.inspect} to be a success" | ||
else | ||
"expected #{actual.inspect} to be a success with value #{@value.inspect}" | ||
end | ||
end | ||
|
||
def failure_message_when_negated | ||
if Undefined.equal?(@value) | ||
"expected #{actual.inspect} to not be a success" | ||
else | ||
"expected #{actual.inspect} to not be a success with value #{@value.inspect}" | ||
end | ||
end | ||
end | ||
|
||
class BeFailureMatcher | ||
def initialize(value = Undefined) | ||
@value = value | ||
end | ||
|
||
def matches?(actual) | ||
if Undefined.equal?(@value) | ||
actual.failure? | ||
else | ||
actual.eql?(Result::Failure.new(@value)) | ||
end | ||
end | ||
|
||
def be_failure | ||
BeFailureMatcher.new | ||
end | ||
|
||
def failure_message | ||
if Undefined.equal?(@value) | ||
"expected #{actual.inspect} to be a failure" | ||
else | ||
"expected #{actual.inspect} to be a failure with value #{@value.inspect}" | ||
end | ||
end | ||
|
||
def failure_message_when_negated | ||
if Undefined.equal?(@value) | ||
"expected #{actual.inspect} to not be a failure" | ||
else | ||
"expected #{actual.inspect} to not be a failure with value #{@value.inspect}" | ||
end | ||
end | ||
end | ||
|
||
class BeSomeMatcher | ||
def initialize(value = Undefined) | ||
@value = value | ||
end | ||
|
||
def matches?(actual) | ||
if Undefined.equal?(@value) | ||
actual.some? | ||
else | ||
actual.eql?(Maybe::Some.new(@value)) | ||
end | ||
end | ||
|
||
def failure_message | ||
if Undefined.equal?(@value) | ||
"expected #{actual.inspect} to be a some" | ||
else | ||
"expected #{actual.inspect} to be a some with value #{@value.inspect}" | ||
end | ||
end | ||
|
||
def failure_message_when_negated | ||
if Undefined.equal?(@value) | ||
"expected #{actual.inspect} to not be a some" | ||
else | ||
"expected #{actual.inspect} to not be a some with value #{@value.inspect}" | ||
end | ||
end | ||
end | ||
|
||
class BeNoneMatcher | ||
def matches?(actual) | ||
actual.none? | ||
end | ||
|
||
def failure_message | ||
"expected #{actual.inspect} to be a none" | ||
end | ||
|
||
def failure_message_when_negated | ||
"expected #{actual.inspect} to not be a none" | ||
end | ||
end | ||
|
||
def be_success(value = Undefined) | ||
BeSuccessMatcher.new(value) | ||
end | ||
|
||
def be_failure(value = Undefined) | ||
BeFailureMatcher.new(value) | ||
end | ||
|
||
def be_some(value = Undefined) | ||
BeSomeMatcher.new(value) | ||
end | ||
|
||
def be_none | ||
BeNoneMatcher.new | ||
end | ||
end | ||
|
||
Constructors = Monads[:result, :maybe] | ||
end | ||
end | ||
end | ||
|
||
RSpec.configure do |config| | ||
config.include Dry::Monads::RSpec::Matchers | ||
config.include Dry::Monads::RSpec::Constructors | ||
end | ||
|
||
catch_missing_const = Module.new do | ||
constants = %i[Success Failure Some None].to_set | ||
|
||
name_to_const = proc do |name| | ||
case name | ||
in :Success | ||
Dry::Monads::Result::Success | ||
in :Failure | ||
Dry::Monads::Result::Failure | ||
in :Some | ||
Dry::Monads::Maybe::Some | ||
in :None | ||
Dry::Monads::Maybe::None | ||
end | ||
end | ||
|
||
if debug_inspector_available | ||
define_method(:const_missing) do |name| | ||
if constants.include?(name) && caller_locations(1, 1).first.path.end_with?("_spec.rb") | ||
DebugInspector.open do |dc| | ||
if dc.frame_binding(2).receiver.is_a?(RSpec::Core::ExampleGroup) | ||
name_to_const.(name) | ||
else | ||
super(name) | ||
end | ||
end | ||
else | ||
super(name) | ||
end | ||
end | ||
else | ||
define_method(:const_missing) do |name| | ||
if constants.include?(name) && caller_locations(1, 1).first.path.end_with?("_spec.rb") | ||
name_to_const.(name) | ||
else | ||
super(name) | ||
end | ||
end | ||
end | ||
end | ||
|
||
Object.extend(catch_missing_const) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,84 @@ | ||
# frozen_string_literal: true | ||
|
||
RSpec.describe "RSpec extension" do | ||
before(:all) do | ||
Dry::Monads.load_extensions(:rspec) | ||
end | ||
|
||
it "adds constructors and matchers" do | ||
expect(Success(1)).to be_success | ||
expect(Success(1)).to be_success(1) | ||
expect(Success(1)).not_to be_success(2) | ||
expect(Failure(1)).to be_failure | ||
expect(Failure(1)).to be_failure(1) | ||
expect(Failure(1)).not_to be_failure(2) | ||
expect(Some(1)).to be_some | ||
expect(Some(1)).to be_some(1) | ||
expect(Some(1)).not_to be_some(2) | ||
expect(None()).to be_none | ||
expect(None()).not_to be_some | ||
expect(None()).not_to be_some(1) | ||
end | ||
|
||
it "catches missing constants" do | ||
expect(Success(1)).to be_a(Success) | ||
expect(Failure(1)).to be_a(Failure) | ||
expect(Some(1)).to be_a(Some) | ||
expect(None()).to be_a(None) | ||
end | ||
|
||
context "patten matching" do | ||
example "with result" do | ||
success = Success(1) | ||
success => Success(value) | ||
expect(value).to eql(1) | ||
|
||
case Success(nested: Failure(:value)) | ||
in Success(nested: Success(value)) | ||
raise "unexpected" | ||
in Success(nested: Failure(value)) | ||
expect(value).to eql(:value) | ||
end | ||
end | ||
|
||
example "with maybe" do | ||
maybe = Some(1) | ||
maybe => Some(value) | ||
expect(value).to eql(1) | ||
|
||
case Some(nested: None()) | ||
in Some(nested: Some(value)) | ||
raise "unexpected" | ||
in Some(nested: None()) | ||
nil | ||
end | ||
end | ||
end | ||
|
||
context "with missing constant" do | ||
let(:operation) do | ||
require_relative "../fixtures/missing_constant" | ||
|
||
MissingConstant.new | ||
end | ||
|
||
it "raises an error when it comes from a non-spec file" do | ||
expect { operation.(1) }.to raise_error(NameError) | ||
end | ||
|
||
context "inline class" do | ||
let(:operation) do | ||
Class.new { | ||
def call(value) | ||
Success[value] | ||
end | ||
}.new | ||
end | ||
|
||
# we use debug_inspector to check if the error comes from a non-spec context | ||
it "raises an error when it comes from a non-spec context" do | ||
expect { operation.(1) }.to raise_error(NameError) | ||
end | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
# frozen_string_literal: true | ||
|
||
class MissingConstant | ||
def call(value) | ||
Success[value] | ||
end | ||
end |