It manages object creating and its life time and also injects dependencies to the class. IoC container creates an object of the specified class and also injects all the dependency objects through constructor, property or method at run time and disposes it at the appropriate time. This is done so that we don't have to create and manage objects manually.
sample/apps/web/controllers/users/index.rb module Web module Controllers module Users class Index include Web::Action include Import[ user_repository: 'repositories.user' ] def call(_params) # ... users = user_repository.all # ... end end end end end # sample/system/import.rb Import = Container.injector
sample/system/boot/config.rb Container.boot(:config) do |container| container.namespace(:config) do namespace(:urls) do register(:user_email_confirmation) { ENV.fetch('USER_EMAIL_CONFIRMATION_URL') } end end end # sample/lib/sample/util/external_urls.rb module Sample module Util class ExternalUrls include Import[ user_email_confirmation: 'config.urls.user_email_confirmation', ] def user_email_confirmation_url(token) "#{user_email_confirmation}?token=#{token}" end end end end
sample/system/container.rb class Container < Dry::System::Container # ... class Registry < Dry::Container::Registry def call(container, key, item, options) { memoize: true }.merge(options) if item.is_a?(Proc) super end end class Resolver < Dry::Container::Resolver def call(container, key) # whatever super end end configure do |config| config.registry = Registry.new config.resolver = Resolver.new end # ... end
application business logic and evaluates it by steps, e.g. user creation, users listing, user removal and etc. Operations flow is based on the railway oriented programming.
:create private def validate(input) # returns Success(valid_data) or Failure(validation) end def create(input) # returns Success(user) end end https://dry-rb.org/gems/dry-transaction/basic-usage/
not convenient to pass state from one step to another • database transactions looks awful https://medium.com/pepegramming/do-notation-1e0840a6dbe0 https://www.morozov.is/2018/05/27/do-notation-ruby.html
‘dry-monads' gem ‘bcrypt' # ... # sample/lib/sample/util/password_digitizer.rb module Sample module Util class PasswordDigitizer def call(password) BCrypt::Password.create(password).to_s end end end end
class Create include Dry::Monads::Result::Mixin include Dry::Monads::Do.for(:call) include Import[ user: 'repositories.user', digitizer: 'util.password_digitizer' ] def call(input) input = yield validate(input) input = yield build_password(input) user = yield create(input) Success(user) end private def validate(input) # ... end def create(input) Success( user_repo.create(input) ) end def build_password(input) Success(input.merge( encrypted_password: digitizer.call(input[:password]) )) end end end end end end
module Users module Operations class List include Dry::Monads::Result::Mixin include Dry::Monads::Do.for(:call) include Import[ user: 'repositories.user' ] def call(input) input = yield validate(input) relation = user.find_all_by(input) Success(relation) end private def validate(input) # ... end end end end end end
module Users class List include Dry::Monads::Result::Mixin include Dry::Monads::Do.for(:call) include Import[ list: 'domain.users.operations.list', pagination_query: 'queries.pagination' ] def call(input) relation = yield list.call(input) collection = pagination_query.call(relation) Success(collection) end end end end end
Web class Container < Dry::System::Container setting :path_prefix configure do |config| config.path_prefix = Pathname('apps/web') config.auto_register = %w[operations].map do |dir| config.path_prefix.join(dir) end end load_paths! Hanami.root.join('apps') end end # sample/apps/web/system/import.rb require_relative './container' module Web AppImport = Web::Container.injector end
Index include Web::Action include AppImport[ list: 'web.operations.users.list' ] params do # ... end def call(params) # ... users = list.call(params) # ... end end end end end
class Create include Web::Action include Import[ create_user: ‘domain.users.operations.create’ ] params do required(:name).filled(:str?) required(:email).filled(:str?) required(:password).filled(:str?) end def call(params) if params.valid? # ... create_user.call(params) else # ... end end end end end end
predicate(:uuid?) do |value| /\A(urn:uuid:)?[\da-f]{8}-([\da-f]{4}-)[\da-f]{12}\z/i.match?(value) end end end # sample/apps/web/params.rb require_relative '../../lib/sample/schemas/predicates' module Web class Params < Hanami::Action::Params predicates Schemas::Predicates end end
Users class Create include Web::Action include Import[ create_user: ‘domain.users.operations.create’ ] class Params < Web::Params params do # ... required(:inviter_id).filled(:uuid?) end end params Params def call(params) if params.valid? # ... create_user.call(params) else # ... end end end end end end
attr_reader :result predicates Schemas::Predicates end end # sample/apps/web/helpers/params_validation.rb module Web module Helpers module ParamsValidation def validate_params!(params) result = params.result return result.output if result.success? halt( 422, { code: :invalid, error: result.messages }.to_json ) end end end end
... configure do # ... load_paths << [ 'helpers', 'controllers', ] controller.prepare do # ... include Web::Helpers::ParamsValidation # ... end end # ... end end
Create include Web::Action include Import[ create_user: ‘domain.users.operations.create’ ] class Params < Web::Params params do required(:name).filled(:str?) required(:email).filled(:str?) required(:password).filled(:str?) required(:inviter_id).filled(:uuid?) end end params Params def call(params) input = validate_params!(params) # ... end end end end end
Create = Dry::Validation.Schema(::Schemas::Base) do configure do option :repo def uniq?(value) repo.email_uniq?(value) end end required(:name).filled(:str?, format?: /\A[a-zA-Z]\z/, max_size?: 255) required(:email).filled(:str?, :email?, :unique?, max_size?: 255) required(:password).filled(:str?, :strong?) optional(:inviter_id).filled(:uuid?) end end end end end
class Create include Dry::Monads::Result::Mixin include Dry::Monads::Do.for(:call) include Import[ user: 'repositories.user', digitizer: 'util.password_digitizer' ] def call(input) input = yield validate(input) input = yield build_password(input) user = yield create(input) Success(user) end private def validate(input) result = Schemas::Create.with(repo: user).(input) if result.success? Success(result.output) else Failure(error) end end # ... end end end end end
'representable' # ... # sample/apps/web/serializers/user.rb module Web module Serializers class User < Representable::Decorator include Representable::JSON property :id property :email property :name property :inviter_id end end end
= 200 def respond_with(result, serializer:, status: DEFAULT_STATUS) if result.success? respond_with_success( result.value!, serializer: serializer, status: status ) else respond_with_failure(result.failure) end end def respond_with_success(result, serializer:, status: DEFAULT_STATUS) self.body = serializer.new(result).to_json self.status = status end def respond_with_failure(failure) # ... end end end end
Create include Web::Action include Import[ create_user: 'domain.users.operations.create' ] class Params < Web::Params params do required(:name).filled(:str?) required(:email).filled(:str?) required(:password).filled(:str?) required(:inviter_id).filled(:uuid?) end end params Params def call(params) input = validate_params!(params) result = create_user.call(input) respond_with(result, serializer: Serializers::User) end end end end end