README.md
# Dry::Mutations
[![Build Status](https://travis-ci.org/am-kantox/dry-mutations.svg?branch=master)](https://travis-ci.org/am-kantox/dry-mutations)
[![Code Climate](https://codeclimate.com/github/am-kantox/dry-mutations/badges/gpa.svg)](https://codeclimate.com/github/am-kantox/dry-mutations)
---
A link between [`dry-validation`](http://dry-rb.org/gems/dry-validation) and
[`mutations`](http://github.com/cypriss/mutations) gems. This gem enables
support for `dry-validation` schemas to be used within legacy `mutations`-based
syntax.
## Installation
Add this line to your application's Gemfile:
```ruby
gem 'dry-mutations'
```
And then execute:
$ bundle
Or install it yourself as:
$ gem install dry-mutations
## Was ⇒ Is
### Was
```ruby
class ComposedMutation < Mutations::Command
...
def validate
additional_validate(input1, input2)
@nested = NestedMutation.new(inputs, input1: input1, input2: input2)
unless @nested.validation_outcome.success?
@nested.validation_outcome.errors.each do |key, error|
add_error(key.to_sym, error.symbolic, error.message)
end
end
end
def execute
@nested.run!
end
end
```
### Is
```ruby
class ComposedValidation < Mutations::Command
prepend ::Dry::Mutations::Extensions::Command
prepend ::Dry::Mutations::Extensions::Sieve
...
def validate
additional_validate(input1, input2)
end
end
class ComposedTransform < Mutations::Command
prepend ::Dry::Mutations::Extensions::Command
...
def execute
inputs.merge(input1: input1, input2: input2)
end
end
class ComposedMutation
extend ::Dry::Mutations::Transactions::DSL
chain do
validate ComposedValidation
transform ComposedTransform
mutate NestedMutation
end
end
```
### Call syntax
Basically, any call syntax is supported:
```ruby
# preferred
ComposedMutation.(input) # returns (Either ∨ Outcome) object
# legacy
ComposedMutation.run(input) # returns (Either ∨ Outcome) object
ComposedMutation.new(input).run # returns (Either ∨ Outcome) object
ComposedMutation.run!(input) # throws Mutation::ValidationException
ComposedMutation.new(input).run! # throws Mutation::ValidationException
```
## Usage
### Enable extensions for the specific mutation’s command
Prepend a `::Dry::Mutations::Extensions::Command` module to your `Mutation::Command` instance:
```ruby
class MyMutation < Mutations::Command
prepend ::Dry::Mutations::Extensions::Command
required do
model :company, class: 'Profile'
model :user
hash :maturity_set do
string :maturity_choice, in: %w(spot forward_days fixed_date)
optional do
hash :maturity_days_set do
integer :days, default: 3 # For spot or forward_days options
end
hash :maturity_date_set do
date :date # When passing a fixed date
end
end
end
...
```
### `dry-validation` syntax
It is possible to mix standard mutations’ syntax with `dry-rb` schemas:
```ruby
class MyMutation < Mutations::Command
prepend ::Dry::Mutations::Extensions::Command
required do
model :company, class: 'Profile'
end
schema do
required(:maturity_choice).filled(:str?, included_in?: %w(spot forward_days fixed_date))
end
```
### Reusing schema
Basically, everything [written here](http://dry-rb.org/gems/dry-validation/reusing-schemas/)
is applicable. Syntax to include the nested schema is as simple as:
```ruby
UserSchema = Dry::Validation.Schema do
required(:email).filled(:str?)
required(:name).filled(:str?)
required(:address).schema(AddressSchema)
end
```
or, in legacy `mutations` syntax (**NB! Starting with `0.99.9`!**):
```ruby
required do
string :name
schema :address, AddressSchema
string :email
end
```
## `ActiveRecord::Relation` support
```ruby
schema(Dry::Mutations.Schema do
required(:slaves).filled(relation?: Slave)
end)
```
## Combining dry schemas with mutation-like syntax
Since version `0.99.9`, one might pass the `Dry::Validation::Schema` directly
to legacy mutations syntax:
```ruby
required do
model :user
schema :address, AddressSchema # AddressSchema = ::Dry::Validation.Schema {}
date: Date.today
end
```
---
Since version `0.11.1`, one might pass the instance of `Dry::Validation::Schema`
and/or `Dry::Validation::Form` instance to `schema` mutation DSL.
Such a block might be _only one_, and it _must be_ the first DSL in the mutation.
**NB** this is not a preferred way to do things, but it might be useful to _share_
schemas (unlikely the above, this will _embed_ the schema, rather than _nest_ it.)
### Correct
```ruby
Class.new(::Mutations::Command) do
prepend ::Dry::Mutations::Extensions::Command
prepend ::Dry::Mutations::Extensions::Sieve
schema(::Dry::Validation.Form do
required(:integer_value).filled(:int?, gt?: 0)
required(:date_value).filled(:date?)
required(:bool_value).filled(:bool?)
end)
required do
integer :forty_two
string :hello
end
end
```
### Incorrect
```ruby
Class.new(::Mutations::Command) do
prepend ::Dry::Mutations::Extensions::Command
prepend ::Dry::Mutations::Extensions::Sieve
required do
integer :forty_two
string :hello
end
schema(::Dry::Validation.Form do
required(:integer_value).filled(:int?, gt?: 0)
required(:date_value).filled(:date?)
required(:bool_value).filled(:bool?)
end)
end
```
## Declare the resulting type of a schema
Using the approach above, one might start with a schema type declaration:
```ruby
# this line must be a first declaration
schema(::Dry::Validation.Form {})
# now continue with generic `schema {}` blocks to append features:
schema do
required(:integer_value).filled(:int?, gt?: 0)
required(:date_value).filled(:date?)
required(:bool_value).filled(:bool?)
end
```
see [`schema_spec.rb`](https://github.com/am-kantox/dry-mutations/blob/master/spec/dry/mutations/schema_spec.rb)
for an inspiration.
### Subschema’s type
Startign with `0.99.100` we accept `type:` parameter in call to `schema`:
```ruby
schema type: :form do
...
end
```
## Dealing with `outcome`
### Command
```ruby
let!(:command) do
Class.new(::Mutations::Command) do
prepend ::Dry::Mutations::Extensions::Command
required { string :name, max_length: 5 }
schema { required(:amount).filled(:int?, gt?: 0) }
def execute
@inputs
end
end
end
```
### Using `Either` monad
```ruby
outcome = command.new(name: 'John', amount: 42).run
outcome.right?
#⇒ true
outcome.either.value
#⇒ { 'name' => 'John', 'amount' => 42 }
outcome = command.new(name: 'John Donne', amount: -500).run
outcome.right?
#⇒ false
outcome.left?
#⇒ true
outcome.either
#⇒ Left({
# "name"=>#<Dry::Mutations::Errors::ErrorAtom:0x00000003b4e7b0
# @key="name",
# @symbol=:max_length,
# @message="size cannot be greater than 5",
# @index=0,
# @dry_message=#<Dry::Validation::Message
# predicate=:max_size?
# path=[:name]
# text="size cannot be greater than 5"
# options={:args=>[5], :rule=>:name, :each=>false}>>,
# "amount"=>#<Dry::Mutations::Errors::ErrorAtom:0x00000003b4e508
# @key="amount",
# @symbol=:gt?,
# @message="must be greater than 0",
# @index=1,
# @dry_message=#<Dry::Validation::Message
# predicate=:gt?
# path=[:amount]
# text="must be greater than 0"
# options={:args=>[0], :rule=>:amount, :each=>false}>>
# })
outcome.either.value
#⇒ the hash ⇑ above
```
### Using `Matcher`
```ruby
expect(outcome.match { |m| m.success(&:keys) }).to match_array(%w(amount name))
expect(outcome.match { |m| m.failure(&:keys) }).to be_nil
```
## Turn On Globally (use with caution!)
ENV['GLOBAL_DRY_MUTATIONS'] = 'true' && rake
That way _all_ mutations all over the system will be patched/injected with
new functionality. This is untested in all possible environments.
Bug reports are very welcome!
## Changelog
#### 0.99.1
Support for direct input parameters invocation. 100%-compatibility with `mutations`:
```ruby
def validate # input ≡ { date: nil }
date < Date.now
end
```
#### 1.1.0
More handy `chain`s, better `dry-rb` integration, improvements.
#### 0.99.0
Support for `default:` guard. 99%-compatibility with `mutations`
## Development
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
## Contributing
Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/dry-mutations. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](contributor-covenant.org) code of conduct.
## License
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).