diff --git a/CHANGELOG.md b/CHANGELOG.md index 397341d..7a6df41 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ All notable changes to this project will be documented in this file. This projec ### Added * [#27](https://github.com/michaelherold/interactor-contracts/pull/27): Upgrade dry-validation to 1.0 - [@vaihtovirta](https://github.com/vaihtovirta). +* [#30](https://github.com/michaelherold/interactor-contracts/pull/30): Allow setting a custom I18n backend for contract messages - [@michaelherold](https://github.com/michaelherold). ### Changed diff --git a/Gemfile b/Gemfile index bc35b55..dbf0486 100644 --- a/Gemfile +++ b/Gemfile @@ -19,6 +19,7 @@ group :development do end group :development, :test do + gem 'i18n' gem 'pry' gem 'rake', '< 11' end diff --git a/README.md b/README.md index ebbb970..5dcd73c 100644 --- a/README.md +++ b/README.md @@ -103,6 +103,36 @@ result.failure? #=> true [dry-validation]: https://github.com/dryrb/dry-validation +### I18n support + +You can [configure the underlying `dry-validation` contract][config] by passing +a block to the `config` method in your contract. This block will be evaluated on +the underlying configuration for the contract. For example, if you want to set +up the contract to use I18n in your Rails app, you might do something like this: + +```ruby +class MyInteractor + include Interactor + include Interactor::Contracts + + config do + messages.backend = :i18n + messages.load_paths << Rails.root / 'config' / 'locales' / 'errors.yml' + messages.top_namespace = :interactor_contracts + end +end +``` + +This sets up the I18n system (assuming the delicate load-order has been done in +the right way - you have to require `i18n` prior to requiring +`interactor-contracts` since we load `dry-validation` immediately) to use your +custom file. All lookups for error messages happen starting at the +`interactor_contracts` key in this example. + +See [the documentation for `dry-validation`][config] for more information. + +[config]: https://dry-rb.org/gems/dry-validation/1.0/configuration/ + ## Development After checking out the repo, run `bin/setup` to install dependencies. Then, run diff --git a/lib/interactor/contracts/contract.rb b/lib/interactor/contracts/contract.rb index f17f04a..240fda7 100644 --- a/lib/interactor/contracts/contract.rb +++ b/lib/interactor/contracts/contract.rb @@ -89,6 +89,16 @@ def add_expectation(&term) expectations.add(&term) end + # Configures the underlying contracts for the validation schemata + # + # @api private + # @private + # @return [void] + def config(&block) + promises.config(&block) + expectations.config(&block) + end + # The consequences for the Contract # # @example diff --git a/lib/interactor/contracts/dsl.rb b/lib/interactor/contracts/dsl.rb index 5d3e19b..6accc0d 100644 --- a/lib/interactor/contracts/dsl.rb +++ b/lib/interactor/contracts/dsl.rb @@ -31,6 +31,27 @@ def promises(&block) end alias assures promises + # Sends configuration set up to the underlying contracts in the terms + # + # @example + # class CreatePerson + # include Interactor + # include Interactor::Contracts + # + # config do + # messages.backend = :i18n + # messages.top_namespace = :my_app + # messages.load_paths << File.join(__dir__, '..', 'errors.yml') + # end + # end + # + # @api public + # @params [Block] block the block to execute for the underlying contracts + # @return [void] + def config(&block) + contract.config(&block) + end + # The Contract to enforce on calls to the Interactor # # @example diff --git a/lib/interactor/contracts/terms.rb b/lib/interactor/contracts/terms.rb index 4317951..5962e5b 100644 --- a/lib/interactor/contracts/terms.rb +++ b/lib/interactor/contracts/terms.rb @@ -30,6 +30,10 @@ def initialize(terms = Class.new(Dry::Validation::Contract)) # @return [void] def add(&term) @terms = Class.new(Dry::Validation::Contract).tap do |new_terms| + new_terms.instance_variable_set( + :@config, + @terms.instance_variable_get(:@config).dup + ) new_terms.params(@terms.schema, &term) end end @@ -52,6 +56,15 @@ def call(context) Outcome.new(@terms.new.call(context.to_h)) end + # Configures the underlying contracts within the terms + # + # @api private + # @private + # @return [void] + def config(&block) + @terms.config.instance_exec(&block) + end + private # Defines no-op rules block if no schema has been added diff --git a/spec/interactor/contracts/dsl_spec.rb b/spec/interactor/contracts/dsl_spec.rb index 28459d3..36306d3 100644 --- a/spec/interactor/contracts/dsl_spec.rb +++ b/spec/interactor/contracts/dsl_spec.rb @@ -3,6 +3,40 @@ require 'spec_helper' RSpec.describe Interactor::Contracts::DSL do + describe '.config' do + it 'allows you to configure the messaging for the contracts' do + require 'i18n' + require 'dry/schema/messages/i18n' + + klass = Class.new do + include Interactor + include Interactor::Contracts + + config do + messages.backend = :i18n + messages.top_namespace = :my_app + messages.load_paths << File.expand_path( + File.join('..', '..', 'support', 'errors.yml'), + __dir__ + ) + end + + expects do + required(:bar).filled + end + + on_breach do |breaches| + context.fail!(message: breaches.to_h) + end + end + + result = klass.call + + expect(result).to be_a_failure + expect(result.message).to eq(bar: ['bar is foobared']) + end + end + describe '.expects' do subject(:interactor_call) { klass.call(context) } diff --git a/spec/support/errors.yml b/spec/support/errors.yml new file mode 100644 index 0000000..d0a259a --- /dev/null +++ b/spec/support/errors.yml @@ -0,0 +1,4 @@ +en: + my_app: + errors: + key?: 'is foobared'