Hello, I recently used some of the dry-rb gems in a small Sinatra app for form input validation and JSON Hash coercion for Sidekiq workers. Here is some feedback based on my experience setting up the gems and navigating the guides. I need to stress to maintainers and fans of dry-rb, I am not attacking dry-rb, just pointing out things that stuck out to me, a beginner who’s unfamiliar with the dry-rb way of doing things.
Dot-style calling vs.
Immediately I noticed the use of top-level Capitalized Methods, such as
Dry::Schema.Params do ... and
However, most Ruby code and Rubyists do not call Capitalized class methods such as
Foo.Bar(...), but instead do
Foo::Bar(...). However, Ruby’s syntax doesn’t allow
Foo::Bar do, so you must do
Foo::Bar() do. I found the mixing of dot-style method calling with Capitalized Methods kind of surprising/confusing at first, and wondered why those methods couldn’t have been given lower-case names or possibly
aliases? Perhaps changing the examples to use
::-style method calls for the top-level Capitalized Methods would make the examples less surprising for beginners.
include Dry::Types() is starting to grow on me, it was still unusual to see this style of code. After looking at the API documentation for
Dry::Types() it accepts additional arguments and calls
Dry::Types::Module.new(...) with those arguments. Including an initialized
Module class is something I am more familiar with and have see in other code bases and generally prefer.
Penalty for bypassing the top-level Capitalized Methods
Initially I thought (and was told) I could bypass
include Dry::Types() or
Dry::Schema::Params() and just access the underlying Class/Module directly. Turns out that these top-level Capitalized Methods do additional things or pass additional default values into the underlying methods/classes, making it difficult to bypass them and access the underlying classes or methods directly. For example,
Dry::Types::Module.new. This means if I were to bypass
include Dry::Types() I would need to do
include Dry::Types::Module.new(Dry::Types.container). So while yes one can technically bypass the top-level Capitalized Methods, there is a slight penalty to doing so, which makes it a less viable option. I feel like this could be improved by adding default arguments to the underlying Class constructors, so they match the method signatures of the top-level Capitalized Methods.
Another example, is when I needed to define a common
Dry::Schema that would then be used to create both
Dry::Schema::Params() (to be used in a
Dry::Validation::Contract class) and
Dry::Schema::JSON() for validating/coercing both HTTP params and JSON params, I learned that one does this via
Dry::Schema::Params(parent: TheSchemaClass) and
Dry::Schema::JSON(parent: TheSchemaClass). While it is amazing that this is possible with dry-schema, I found the additional
parent: keyword argument kind of unnecessary. Ideally, I should be able to call
Proc vs. Class
It struck me as kind of odd that dry-types and dry-schema prefer creating
Procs, but dry-validation defines validations as Classes. Furthermore, I couldn’t find an example of initializing a
Dry::Validation::Contract with some additional state, that would warrant it being a class. Every example showed
validator = ValidationClass.new and
result = validator.call(params). I ended up having to add a
def self.call(params) method to my
Dry::Validation::Contract classes to initialize the contract and call it with the given params.
Monad vs. Declarative
While dry-types, dry-schema, and dry-validation use monadic style (aka
method-chaining) for defining data in a method-chaining style,
Dry::Struct has declarative attributes. For my usage
Dry::Validation, I didn’t really see the
benefits of monadic style for defining types or schema params; maybe I could be
wrong and there are exotic edge-cases where monadic style is necessary or the order of the chained method-calls is important. While monads
are certainly a valid way to construct logic, declarative attributes with Type
classes that can be nested or unioned is also a valid style, one which
beginners might be more familiar with.
I think dry-types could support defining types as class (which would accept additional type information via
initialize or be defined as class attributes), and dry-schema could support declarative attributes for defining schema params (with type and other validation configuration passed in as keyword arguments).
This is a pattern I have used in
command_mapper and Ronin::Core::Params.
I constantly found myself searching the guides trying to find examples of what I wanted to do. While there are many examples of how to use dry-types, dry-schema, or dry-validation, I couldn’t find complete end-to-end examples showing how to fully use the libraries to validate params and handle errors. The dry-validation guides forgot to include an example of inspecting the
Dry::Validation::Result object. I eventually had to check the API documentation for
Dry::Validation::Result and infer that you check
failure?, and or render
errors or call
output in the case of
Dry::Schema::Result) to get the result.
Additionally, only a few methods or classes in the API documentation actually have
Unusual Naming Conventions
I struggled a bit with some of the method name choices. I eventually figured out
filled(:string) meant non-empty
String. Or that I had to check
result.failure? instead of
result.has_errors?. Maybe if more
aliases were added this might make the API less difficult for beginners to get used to.
I was slightly confused at first trying to understand how to write additional
Dry::Validation::Contracts. Normally I would expect
value to be passed in as block arguments, but apparently the
rule(...)s block is
instance_execed in a
Dry::Validation::Rule instance and
value are methods. The other thing that caught me off guard was that
key is actually a
Dry::Validation::Failures object which is used for adding error messages, not the key
Symbol name, but
value is actually the value for the
values allows access to all other values. Also, you can specify multiple keys in
rule(...), but it doesn’t seem to have any effect on the