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. ::
-style calling
Immediately I noticed the use of top-level Capitalized Methods, such as Dry::Schema.Params do ...
and Dry::Types()
.
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 alias
es? 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()
While 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()
passes Types.container
to 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 Dry::Schema::Params(TheSchemaClass)
or TheSchemaClass.for_params
.
Proc vs. Class
It struck me as kind of odd that dry-types and dry-schema prefer creating Proc
s, 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
of Dry::Types
, Dry::Schema
, and 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.
Incomplete Documentation
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 success?
or failure?
, and or render errors
or call to_h
(or 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 @example
tags.
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.success?
/result.failure?
instead of result.error?
, or result.errors?
, or result.has_errors?
. Maybe if more alias
es were added this might make the API less difficult for beginners to get used to.
Dry::Validation::Rule
methods
I was slightly confused at first trying to understand how to write additional rule(...)
s for Dry::Validation::Contract
s. Normally I would expect key
and value
to be passed in as block arguments, but apparently the rule(...)
s block is instance_eval
or instance_exec
ed in a Dry::Validation::Rule
instance and key
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 key
; and 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 thekey
, value
, or values
objects.