-
-
Notifications
You must be signed in to change notification settings - Fork 137
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Include Dry::Monads[...] doesn't give access to class constants #182
Comments
Actually, it has nothing to do with dry-monads, it's a peculiarity of how rspec works. Specs are actually written in a global lexical scope, there's no encompassing name around. When you ref Success in specs, it's been searched in Object. I don't know if there's a way or trick to make it work other than define Success as global constant. |
💡 A refinement can be a nice solution. So for Dry Monads, you could do something like this: # Implemntation
module Dry
module Monads
module Cast
refine Kernel do
def Success(object) = Result::Success object
def Failure(object) = Result::Failure object
end
end
end
end
# Usage
using Dry::Monads::Cast
Success "Pass"
Failure "Danger!" I explain the above in a bit more detail here but is one of the super powers of using a refinement like this. |
Thanks @flash-gordon, I forgot about that gotcha with RSpec using blocks. And thanks @bkuhlmann, I was wondering if refinements could be used for this so good to know they can! I think this can work too, to add access to the constants only within RSpec examples: # frozen_string_literal: true
require "dry/monads"
RSpec.configure do |config|
config.include Dry::Monads[:result]
config.before(:all) do
Success = Dry::Monads::Success
Failure = Dry::Monads::Failure
end
end |
Defining constants in specs can be problematic if you forgot to add a mixin somewhere in your code. Specs pass but in prod you'll have a missing constant error. Better define it on startup and call it a day. |
@bkuhlmann refinements help with methods but they don't solve the constant problem. FWIW you can add methods globally in tests with Object.extend(Module.new {
def const_missing(name)
if name.equal?(:Success) && caller_locations(1, 1).first.path.end_with?('_spec.rb')
Dry::Monads::Result::Success
else
super
end
end
}) I actually like this approach. I think we can have an I'll test it in my projects first. |
Nikita: True. Refinements don't solve the constant problem but do ensure the global namespace isn't polluted. Use of Use of |
BTW requiring |
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
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
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
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
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
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
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
You can test the working version in main: I'll give it some time, a week or so, before making a release. I may want to add some features to it |
Nice! Works for me in my app :) I think the As a possible different way to approach it, that could avoid the need for |
This is not correct because it'll be called on every reference in specs, I don't define constants in I was in a hurry and didn't explain clearly why 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 It should raise an error despite being referenced from a
I wouldn't bother. If there's a user of both dry-monads and such a peculiar config they'll let us know :) |
Thanks for testing! |
Describe the bug
When using
include Dry::Monads[:result]
, you can access the constructorsSuccess(...)
andFailure(...)
. But, you don't get theSuccess
andFailure
class constants themselves.In a spec if you do
expect(result).to be_a(Success)
, you get anuninitialized constant Success
error. This is confusing because you do have access to the constructor which creates aSuccess
, but not the class constant itself. Checking the type of a class can be considered a violation of duck typing but it's also a practical way to make sure you get the correct result type, especially in specs.This happens with both
include
andextend
.There are a couple of workarounds:
Dry::Monads::Result::Success
.expect(result).to be_success
For context, this came up here: https://discourse.hanamirb.org/t/trouble-testing-the-result-of-an-operation/1121
A benefit of including the entire class constants, is that it provides more flexibility in terms of creating result type, e.g. you can use
Success[...]
andSuccess.new(...)
, instead of justSuccess(...)
. This would have less encapsulation, but I believe the trade-off is worth it for improved developer experience.Conversion vs constructor convention
As an aside, the conventions for
String(...)
,Int(...)
, andArray(...)
are as conversion methods which return the args if they're already the right type, rather than constructing a new object as a wrapper or copy. By usingSucess(...)
andFailure(...)
we're changing that convention to create a new object to wrap the arg regardless of the contents. Using those other syntax options for creating a new Result object are more conventional. We can't change that behavior at this point IMO, but we could change to recommending usingSuccess[...]
in our docs if we wanted to, or evenSuccess.new(...)
which is the most conventional. Happy to make this into a separate issue for discussion if you'd like.To Reproduce
Expected behavior
I would expect to get
Success
andFailure
classes included when I doinclude Dry::Monads[:result]
, not just their constructor methods.My environment
The text was updated successfully, but these errors were encountered: