--- url: 'https://servactory.com/featury/guide/actions.md' description: Description and examples of using actions on the Featury object --- # Actions of Featury To work with feature flags via Featury, need to create actions. Each action involves implementing logic over the names of the received feature flags and additional options. ## Example As an example, let's imagine that we have an ActiveRecord model that is responsible for all the project's feature flags. It's called `FeatureFlag`. Let's also imagine that working with feature flags in a project requires 4 actions: * `enabled?` * `disabled?` * `enable` * `disable` In this case, the Featury actions will look like this: ::: code-group ```ruby [app/features/application_feature/base.rb] module ApplicationFeature class Base < Featury::Base action :enabled? do |features:, **options| features.all? do |feature| FeatureFlag .find_or_create_by!(code: feature, actor: options[:user]) .enabled? end end action :disabled? do |features:, **options| features.any? do |feature| !FeatureFlag .find_or_create_by!(code: feature, actor: options[:user]) .enabled? end end action :enable do |features:, **options| features.all? do |feature| FeatureFlag .find_or_create_by!(code: feature, actor: options[:user]) .update!(enabled: true) end end action :disable do |features:, **options| features.all? do |feature| FeatureFlag .find_or_create_by!(code: feature, actor: options[:user]) .update!(enabled: false) end end end end ``` ::: --- --- url: 'https://servactory.com/guide/options/advanced.md' description: >- Description and examples of using advanced operating modes of options for all service attributes --- # Advanced mode Advanced mode enables detailed control over attribute options. ## Option `required` ::: code-group ```ruby [input] input :first_name, type: String, required: { is: true, message: "Input `first_name` is required" } ``` ::: ::: info Before version `2.6.0`, `service_class_name:` was used instead of `service:`. In the `2.6.0` release, this attribute was replaced by `service:`, which is an object with prepared data. ::: ::: code-group ```ruby [input] input :first_name, type: String, required: { message: lambda do |service:, input:, value:| "Input `first_name` is required" end } ``` ::: ## Option `inclusion` ::: info Since version `2.12.0` this option is [dynamic](../options/dynamic#option-inclusion). ::: ::: code-group ```ruby [input] input :event_name, type: String, inclusion: { in: %w[created rejected approved] } ``` ```ruby [internal] internal :event_name, type: String, inclusion: { in: %w[created rejected approved] } ``` ```ruby [output] output :event_name, type: String, inclusion: { in: %w[created rejected approved] } ``` ::: ::: info Before version `2.6.0`, `service_class_name:` was used instead of `service:`. In the `2.6.0` release, this attribute was replaced by `service:`, which is an object with prepared data. ::: ::: code-group ```ruby [input] input :event_name, type: String, inclusion: { in: %w[created rejected approved], message: lambda do |service:, input:, value:| value.present? ? "Incorrect `#{input.name}` specified: `#{value}`" : "Event name not specified" end } ``` ```ruby [internal] internal :event_name, type: String, inclusion: { in: %w[created rejected approved], message: lambda do |service:, internal:, value:| value.present? ? "Incorrect `#{internal.name}` specified: `#{value}`" : "Event name not specified" end } ``` ```ruby [output] output :event_name, type: String, inclusion: { in: %w[created rejected approved], message: lambda do |service:, output:, value:| value.present? ? "Incorrect `#{output.name}` specified: `#{value}`" : "Event name not specified" end } ``` ::: ## Option `consists_of` ::: info Since version `2.6.0` this option is [dynamic](../options/dynamic#option-consists-of). ::: ::: code-group ```ruby [input] input :ids, type: Array, consists_of: { type: String, message: "ID can only be of String type" } ``` ```ruby [internal] internal :ids, type: Array, consists_of: { type: String, message: "ID can only be of String type" } ``` ```ruby [output] output :ids, type: Array, consists_of: { type: String, message: "ID can only be of String type" } ``` ::: ::: code-group ```ruby [input] input :ids, type: Array, # The default array element type is String consists_of: { message: "ID can only be of String type" } ``` ```ruby [internal] internal :ids, type: Array, # The default array element type is String consists_of: { message: "ID can only be of String type" } ``` ```ruby [output] output :ids, type: Array, # The default array element type is String consists_of: { message: "ID can only be of String type" } ``` ::: ## Option `schema` ::: info Since version `2.12.0` this option is [dynamic](../options/dynamic#option-schema). ::: ::: code-group ```ruby [input] input :payload, type: Hash, schema: { is: { request_id: { type: String, required: true }, # ... }, message: "Problem with the value in the schema" } ``` ```ruby [internal] internal :payload, type: Hash, schema: { is: { request_id: { type: String, required: true }, # ... }, message: "Problem with the value in the schema" } ``` ```ruby [output] output :payload, type: Hash, schema: { is: { request_id: { type: String, required: true }, # ... }, message: "Problem with the value in the schema" } ``` ::: ::: code-group ```ruby [input] input :payload, type: Hash, schema: { is: { request_id: { type: String, required: true }, # ... }, message: lambda do |input_name:, key_name:, expected_type:, given_type:| "Problem with the value in the `#{input_name}` schema: " \ "`#{key_name}` has `#{given_type}` instead of `#{expected_type}`" end } ``` ```ruby [internal] internal :payload, type: Hash, schema: { is: { request_id: { type: String, required: true }, # ... }, message: lambda do |input_name:, key_name:, expected_type:, given_type:| "Problem with the value in the `#{input_name}` schema: " \ "`#{key_name}` has `#{given_type}` instead of `#{expected_type}`" end } ``` ```ruby [output] output :payload, type: Hash, schema: { is: { request_id: { type: String, required: true }, # ... }, message: lambda do |input_name:, key_name:, expected_type:, given_type:| "Problem with the value in the `#{input_name}` schema: " \ "`#{key_name}` has `#{given_type}` instead of `#{expected_type}`" end } ``` ::: ## Option `must` ::: info The `must` option works only in advanced mode. ::: ::: warning Since 3.0.0 The `is` lambda must return exactly `true`, not a truthy value. Values like `1`, `"string"`, or `[]` will fail validation. ::: ::: code-group ```ruby [input] input :invoice_numbers, type: Array, consists_of: String, must: { be_6_characters: { is: ->(value:, input:) { value.all? { |id| id.size == 6 } } } } ``` ```ruby [internal] internal :invoice_numbers, type: Array, consists_of: String, must: { be_6_characters: { is: ->(value:, internal:) { value.all? { |id| id.size == 6 } } } } ``` ```ruby [output] output :invoice_numbers, type: Array, consists_of: String, must: { be_6_characters: { is: ->(value:, output:) { value.all? { |id| id.size == 6 } } } } ``` ::: ::: info Before version `2.6.0`, `service_class_name:` was used instead of `service:`. In the `2.6.0` release, this attribute was replaced by `service:`, which is an object with prepared data. ::: ::: code-group ```ruby [input] input :invoice_numbers, type: Array, consists_of: String, must: { be_6_characters: { is: ->(value:, input:) { value.all? { |id| id.size == 6 } }, message: lambda do |service:, input:, value:, code:| "Wrong IDs in `#{input.name}`" end } } ``` ```ruby [internal] internal :invoice_numbers, type: Array, consists_of: String, must: { be_6_characters: { is: ->(value:, internal:) { value.all? { |id| id.size == 6 } }, message: lambda do |service:, internal:, value:, code:, reason:| "Wrong IDs in `#{internal.name}`" end } } ``` ```ruby [output] output :invoice_numbers, type: Array, consists_of: String, must: { be_6_characters: { is: ->(value:, output:) { value.all? { |id| id.size == 6 } }, message: lambda do |service:, output:, value:, code:| "Wrong IDs in `#{output.name}`" end } } ``` ::: --- --- url: 'https://servactory.com/datory/guide/data/attributes.md' description: Description and examples of use --- # Attributes ## Basic ### attribute ::: code-group ```ruby [Required] attribute :uuid, from: String, to: :id, as: String, format: :uuid ``` ```ruby [Optional] attribute :uuid, from: [String, NilClass], to: :id, as: [String, NilClass], format: :uuid, required: false ``` ::: ### string ::: code-group ```ruby [Required] string! :uuid, to: :id ``` ```ruby [Optional] string? :uuid, to: :id ``` ::: ### integer ::: code-group ```ruby [Required] integer! :rating, min: 1, max: 10 ``` ```ruby [Optional] integer? :rating, min: 1, max: 10 ``` ::: ### float ::: code-group ```ruby [Required] float! :rating ``` ```ruby [Optional] float? :rating ``` ::: ### boolean ::: code-group ```ruby [Required] boolean! :published ``` ```ruby [Optional] # not supported ``` ::: ## Options The following options are available for the `attribute` method: * `from`; * `to`; * `as`; * `format`; * `min`; * `max`. For helpers these options are also available, with the exception of the `from` option. You can find out about supported values for `format` [here](../../../guide/options/dynamic.md#option-format). ## Helpers ### uuid ::: code-group ```ruby [Example] uuid! :id ``` ```ruby [Equivalent] string! :id, format: :uuid ``` ::: ::: code-group ```ruby [Example] uuid? :id ``` ```ruby [Equivalent] string? :id, format: :uuid ``` ::: ### money ::: code-group ```ruby [Example] money! :box_office ``` ```ruby [Equivalent] integer! :box_office_cents string! :box_office_currency ``` ::: ::: code-group ```ruby [Example] money? :box_office ``` ```ruby [Equivalent] integer? :box_office_cents string? :box_office_currency ``` ::: ### duration ::: code-group ```ruby [Example] duration! :episode_duration ``` ```ruby [Equivalent] attribute :episode_duration, from: String, as: ActiveSupport::Duration, format: { from: :duration } ``` ::: ::: code-group ```ruby [Example] duration? :episode_duration ``` ```ruby [Equivalent] attribute :episode_duration, from: [String, NilClass], as: [ActiveSupport::Duration, NilClass], format: { from: :duration }, required: false ``` ::: ### date ::: code-group ```ruby [Example] date! :premiered_on ``` ```ruby [Equivalent] attribute :premiered_on, from: String, as: Date, format: { from: :date } ``` ::: ::: code-group ```ruby [Example] date? :premiered_on ``` ```ruby [Equivalent] attribute :premiered_on, from: [String, NilClass], as: [Date, NilClass], format: { from: :date }, required: false ``` ::: ### time ::: code-group ```ruby [Example] time! :premiered_at ``` ```ruby [Equivalent] attribute :premiered_at, from: String, as: Time, format: { from: :time } ``` ::: ::: code-group ```ruby [Example] time? :premiered_at ``` ```ruby [Equivalent] attribute :premiered_at, from: [String, NilClass], as: [Time, NilClass], format: { from: :time }, required: false ``` ::: ### datetime ::: code-group ```ruby [Example] datetime! :premiered_at ``` ```ruby [Equivalent] attribute :premiered_at, from: String, as: DateTime, format: { from: :datetime } ``` ::: ::: code-group ```ruby [Example] datetime? :premiered_at ``` ```ruby [Equivalent] attribute :premiered_at, from: [String, NilClass], as: [DateTime, NilClass], format: { from: :datetime }, required: false ``` ::: --- --- url: 'https://servactory.com/featury/guide/callbacks.md' description: Description and examples of using callbacks of the Featury object --- # Callbacks of Featury The work with actions can be tracked via `before` and `after` callbacks. For each callback, you can specify what actions it should trigger. By default, the callback will respond to all actions. Inside the callback, there is data from the called action, as well as feature flags from the called Featury object. ## Callback `before` In this example, the `before` callback will be triggered when any of the actions are called. ```ruby before do |action:, features:| Slack::API::Notify.call!(action:, features:) end ``` ## Callback `after` In this example, the `after` callback will only fire when the `enabled?` or `disabled?` action is called. ```ruby after :enabled?, :disabled? do |action:, features:| Slack::API::Notify.call!(action:, features:) end ``` --- --- url: 'https://servactory.com/guide/configuration.md' description: Description and examples of service configuration --- # Configuration Configure services via the `configuration` method, typically placed in the base class. ## Configuration examples ### For exceptions ::: code-group ```ruby {4-6,8} [app/services/application_service/base.rb] module ApplicationService class Base < Servactory::Base configuration do input_exception_class ApplicationService::Exceptions::Input internal_exception_class ApplicationService::Exceptions::Internal output_exception_class ApplicationService::Exceptions::Output failure_class ApplicationService::Exceptions::Failure end end end ``` ```ruby {3-5,7} [app/services/application_service/exceptions.rb] module ApplicationService module Exceptions class Input < Servactory::Exceptions::Input; end class Output < Servactory::Exceptions::Output; end class Internal < Servactory::Exceptions::Internal; end class Failure < Servactory::Exceptions::Failure; end end end ``` ::: ### For result ::: code-group ```ruby {6} [app/services/application_service/base.rb] module ApplicationService class Base < Servactory::Base configuration do # ... result_class ApplicationService::Result end end end ``` ```ruby {2} [app/services/application_service/result.rb] module ApplicationService class Result < Servactory::Result; end end ``` ::: ### Collection mode ::: code-group ```ruby {4} [app/services/application_service/base.rb] module ApplicationService class Base < Servactory::Base configuration do collection_mode_class_names([ActiveRecord::Relation]) end end end ``` ::: ### Hash mode ::: code-group ```ruby {4} [app/services/application_service/base.rb] module ApplicationService class Base < Servactory::Base configuration do hash_mode_class_names([CustomHash]) end end end ``` ::: ### Helpers for `input` Base custom helpers for `input` on the `must` and `prepare` options. #### Example with `must` ::: code-group ```ruby {4-20} [app/services/application_service/base.rb] module ApplicationService class Base < Servactory::Base configuration do input_option_helpers( [ Servactory::Maintenance::Attributes::OptionHelper.new( name: :must_be_6_characters, equivalent: { must: { be_6_characters: { is: ->(value:, input:) { value.all? { |id| id.size == 6 } }, message: lambda do |input:, **| "Wrong IDs in `#{input.name}`" end } } } ) ] ) end end end ``` ::: #### Example with `prepare` ::: code-group ```ruby {4-13} [app/services/application_service/base.rb] module ApplicationService class Base < Servactory::Base configuration do input_option_helpers( [ Servactory::Maintenance::Attributes::OptionHelper.new( name: :to_money, equivalent: { prepare: ->(value:) { Money.from_cents(value, :USD) } } ) ] ) end end end ``` ::: ### Helpers for `internal` Base custom helpers for `internal` on the `must` option. #### Example with `must` ::: code-group ```ruby {4-20} [app/services/application_service/base.rb] module ApplicationService class Base < Servactory::Base configuration do internal_option_helpers( [ Servactory::Maintenance::Attributes::OptionHelper.new( name: :must_be_6_characters, equivalent: { must: { be_6_characters: { is: ->(value:, internal:) { value.all? { |id| id.size == 6 } }, message: lambda do |internal:, **| "Wrong IDs in `#{internal.name}`" end } } } ) ] ) end end end ``` ::: ### Helpers for `output` Base custom helpers for `output` on the `must` option. #### Example with `must` ::: code-group ```ruby {4-20} [app/services/application_service/base.rb] module ApplicationService class Base < Servactory::Base configuration do output_option_helpers( [ Servactory::Maintenance::Attributes::OptionHelper.new( name: :must_be_6_characters, equivalent: { must: { be_6_characters: { is: ->(value:, output:) { value.all? { |id| id.size == 6 } }, message: lambda do |output:, **| "Wrong IDs in `#{output.name}`" end } } } ) ] ) end end end ``` ::: ### Aliases for `make` Add alternatives to `make` via `action_aliases` configuration. ::: code-group ```ruby {4} [app/services/application_service/base.rb] module ApplicationService class Base < Servactory::Base configuration do action_aliases %i[execute] end end end ``` ::: ### Customization for `make` Implement shortcuts for `make` via `action_shortcuts` configuration. Values replace `make` and serve as prefix to the instance method. #### Simple mode In simple mode, values are passed as an array of symbols. ::: code-group ```ruby {4-6} [app/services/application_service/base.rb] module ApplicationService class Base < Servactory::Base configuration do action_shortcuts( %i[assign perform] ) end end end ``` ::: ::: details Example of use ```ruby class CMS::API::Posts::Create < CMS::API::Base # ... assign :model perform :request private def assign_model # Build model for API request end def perform_request # Perform API request end # ... end ``` ::: #### Advanced mode In advanced mode, values are passed as a hash. ::: code-group ```ruby {6-11} [app/services/application_service/base.rb] module ApplicationService class Base < Servactory::Base configuration do action_shortcuts( %i[assign], { restrict: { # replacement for make prefix: :create, # method name prefix suffix: :restriction # method name suffix } } ) end end end ``` ::: ::: details Example of use ```ruby class Payments::Restrictions::Create < ApplicationService::Base input :payment, type: Payment # The exclamation mark will be moved to the end of the method name restrict :payment! private def create_payment_restriction! inputs.payment.restrictions.create!( reason: "Suspicion of fraud" ) end end ``` ::: ### Predicate methods Predicate methods for all attributes are enabled by default. Disable them if necessary. ::: code-group ```ruby {4} [app/services/application_service/base.rb] module ApplicationService class Base < Servactory::Base configuration do predicate_methods_enabled false end end end ``` ::: ### Root key for I18n Override the default root key for translations (`servactory`). ::: code-group ```ruby {4} [app/services/application_service/base.rb] module ApplicationService class Base < Servactory::Base configuration do i18n_root_key :my_app end end end ``` ::: This changes the translation lookup from `servactory.*` to `my_app.*`. See also [Internationalization (I18n)](/guide/i18n). --- --- url: 'https://servactory.com/datory/guide/usage/deserialization.md' description: Description and examples of use --- # Deserialization ## Usage ### Data preparation Data in Hash or JSON format can be used for deserialization. ### Call ```ruby SerialDto.deserialize(json) # => Datory::Result ``` --- --- url: 'https://servactory.com/guide/options/dynamic.md' description: Description and examples of using dynamic options for all service attributes --- # Dynamic options Dynamic options are `must`-based options that accept values as arguments. Unlike [custom helpers](../attributes/input#custom), dynamic options work with arguments. Servactory out of the box provides the following set of dynamic options: * `consists_of`; * `format`; * `inclusion`; * `max`; * `min`; * `multiple_of`; * `schema`; * `target`. By default, `consists_of`, `inclusion`, and `schema` are enabled. Enable the rest via ready-made sets in the option helpers configuration for each attribute. ## Ready-made options ### Option `consists_of` * Kit: `Servactory::ToolKit::DynamicOptions::ConsistsOf` * Based on: `must` * Enabled by default: Yes * [Source code](https://github.com/servactory/servactory/blob/main/lib/servactory/tool_kit/dynamic_options/consists_of.rb) ### Option `format` * Kit: `Servactory::ToolKit::DynamicOptions::Format` * Based on: `must` * Enabled by default: No * [Source code](https://github.com/servactory/servactory/blob/main/lib/servactory/tool_kit/dynamic_options/format.rb) #### Supported formats * `uuid`; * `email`; * `password`; * `duration`; * `date`; * `time`; * `datetime`; * `boolean`. #### Customization Overwrite existing formats or add custom ones via the `formats` attribute in the `use` method: ```ruby Servactory::ToolKit::DynamicOptions::Format.use( formats: { email: { pattern: /@/, validator: ->(value:) { value.present? } }, invoice: { pattern: /^([A]{2})-([0-9A-Z]{6})$/, validator: ->(value:) { value.present? } } } ) ``` #### Installation and usage ::: code-group ```ruby [Installation] input_option_helpers([ Servactory::ToolKit::DynamicOptions::Min.use ]) internal_option_helpers([ Servactory::ToolKit::DynamicOptions::Min.use(:minimum) ]) output_option_helpers([ Servactory::ToolKit::DynamicOptions::Min.use ]) ``` ```ruby [Usage] input :email, type: String, format: :email internal :email, type: String, format: { is: :email } output :data, type: String, format: { is: :email, message: lambda do |output:, value:, option_value:, **| "Incorrect `email` format in `#{output.name}`" end } ``` ::: ### Option `inclusion` * Kit: `Servactory::ToolKit::DynamicOptions::Inclusion` * Based on: `must` * Enabled by default: Yes * [Source code](https://github.com/servactory/servactory/blob/main/lib/servactory/tool_kit/dynamic_options/inclusion.rb) ### Option `max` * Kit: `Servactory::ToolKit::DynamicOptions::Max` * Based on: `must` * Enabled by default: No * [Source code](https://github.com/servactory/servactory/blob/main/lib/servactory/tool_kit/dynamic_options/max.rb) #### Installation and usage ::: code-group ```ruby [Installation] input_option_helpers([ Servactory::ToolKit::DynamicOptions::Max.use ]) internal_option_helpers([ Servactory::ToolKit::DynamicOptions::Max.use(:maximum) ]) output_option_helpers([ Servactory::ToolKit::DynamicOptions::Max.use ]) ``` ```ruby [Usage] input :data, type: Integer, max: 10 internal :data, type: String, maximum: { is: 10 } output :data, type: Array, max: { is: 10, message: lambda do |output:, value:, option_value:, **| "The size of the `#{output.name}` value must be less than or " \ "equal to `#{option_value}` (got: `#{value}`)" end } ``` ::: ### Option `min` * Kit: `Servactory::ToolKit::DynamicOptions::Min` * Based on: `must` * Enabled by default: No * [Source code](https://github.com/servactory/servactory/blob/main/lib/servactory/tool_kit/dynamic_options/min.rb) #### Installation and usage ::: code-group ```ruby [Installation] input_option_helpers([ Servactory::ToolKit::DynamicOptions::Min.use ]) internal_option_helpers([ Servactory::ToolKit::DynamicOptions::Min.use(:minimum) ]) output_option_helpers([ Servactory::ToolKit::DynamicOptions::Min.use ]) ``` ```ruby [Usage] input :data, type: Integer, min: 1 internal :data, type: String, minimum: { is: 1 } output :data, type: Array, min: { is: 1, message: lambda do |output:, value:, option_value:, **| "The size of the `#{output.name}` value must be greater than or " \ "equal to `#{option_value}` (got: `#{value}`)" end } ``` ::: ### Option `multiple_of` * Kit: `Servactory::ToolKit::DynamicOptions::MultipleOf` * Based on: `must` * Enabled by default: No * [Source code](https://github.com/servactory/servactory/blob/main/lib/servactory/tool_kit/dynamic_options/multiple_of.rb) #### Installation and usage ::: code-group ```ruby [Installation] input_option_helpers([ Servactory::ToolKit::DynamicOptions::MultipleOf.use ]) internal_option_helpers([ Servactory::ToolKit::DynamicOptions::MultipleOf.use(:divisible_by) ]) output_option_helpers([ Servactory::ToolKit::DynamicOptions::MultipleOf.use ]) ``` ```ruby [Usage] input :data, type: Integer, multiple_of: 2 internal :data, type: Integer, divisible_by: { is: 2 } output :data, type: Float, multiple_of: { is: 2, message: lambda do |output:, value:, option_value:, **| "Output `#{output.name}` has the value `#{value}`, " \ "which is not a multiple of `#{option_value}`" end } ``` ::: ### Option `schema` * Kit: `Servactory::ToolKit::DynamicOptions::Schema` * Based on: `must` * Enabled by default: Yes * [Source code](https://github.com/servactory/servactory/blob/main/lib/servactory/tool_kit/dynamic_options/schema.rb) ### Option `target` * Kit: `Servactory::ToolKit::DynamicOptions::Target` * Based on: `must` * Enabled by default: No * [Source code](https://github.com/servactory/servactory/blob/main/lib/servactory/tool_kit/dynamic_options/target.rb) #### Installation and usage ::: code-group ```ruby [Installation] input_option_helpers([ Servactory::ToolKit::DynamicOptions::Target.use ]) internal_option_helpers([ Servactory::ToolKit::DynamicOptions::Target.use(:expect) ]) output_option_helpers([ Servactory::ToolKit::DynamicOptions::Target.use ]) ``` ```ruby [Usage] input :service_class, type: Class, target: MyFirstService internal :service_class, type: Class, expect: { in: [MyFirstService, MySecondService] } output :service_class, type: Class, target: { in: [MyFirstService, MySecondService], message: lambda do |output:, value:, option_value:, **| "Output `#{output.name}`: #{value.inspect} is not allowed. " \ "Allowed: #{Array(option_value).map(&:name).join(', ')}" end } ``` ::: ## Custom options Create custom dynamic options using the template below. Place the class file in `app/services/application_service/dynamic_options`. ### Template ::: code-group ```ruby [app/services/application_service/dynamic_options/my_option.rb] module ApplicationService module DynamicOptions class MyOption < Servactory::ToolKit::DynamicOptions::Must def self.use(option_name = :my_option, **options) new(option_name).must(:be_the_best) end def condition_for_input_with(input:, value:, option:) # There should be conditions here that are intended for the input attribute end def condition_for_internal_with(internal:, value:, option:) # There should be conditions here that are intended for the internal attribute end def condition_for_output_with(output:, value:, option:) # There should be conditions here that are intended for the output attribute end def message_for_input_with(service:, input:, value:, option_value:, **) # There should be a message text here in case the condition is not met end def message_for_internal_with(service:, internal:, value:, option_value:, **) # There should be a message text here in case the condition is not met end def message_for_output_with(service:, output:, value:, option_value:, **) # There should be a message text here in case the condition is not met end end end end ``` ::: ### Application ```ruby input_option_helpers([ ApplicationService::DynamicOptions::MyOption.use ]) internal_option_helpers([ ApplicationService::DynamicOptions::MyOption.use(:my_best_option) ]) output_option_helpers([ ApplicationService::DynamicOptions::MyOption.use(some: :data) ]) ``` --- --- url: 'https://servactory.com/guide/exceptions/success.md' description: >- Description and examples of using early manual successful termination of the service --- # Early successful termination Terminate the service prematurely and successfully by calling the `success!` method. For Servactory this is also an exception, but a successful one. ## Usage Example: a notification service that operates depending on the environment. ```ruby class Notifications::Slack::Error::Send < ApplicationService::Base # ... make :check_environment! make :send_message! private def check_environment! return if Rails.env.production? success! end def send_message! # Here is the API request in Slack end end ``` This service immediately succeeds in non-production environments. Especially useful in complex implementations with multiple conditions. --- --- url: 'https://servactory.com/guide/extensions.md' description: >- Expand service functionality with custom extensions using the Stroma hook system --- # Extensions Extensions allow you to expand base service functionality through the [Stroma](https://github.com/servactory/stroma) hook system. Define custom behavior that runs before or after service execution stages. Create extensions in the `app/services/application_service/extensions` directory. ## Quick start ### Generate extension ```shell rails generate servactory:extension StatusActive ``` This creates `app/services/application_service/extensions/status_active/dsl.rb`. ### Connect extension ::: code-group ```ruby {4-6} [app/services/application_service/base.rb] module ApplicationService class Base < Servactory::Base extensions do before :actions, ApplicationService::Extensions::StatusActive::DSL end end end ``` ::: ### Use in service ```ruby {5} class Posts::Create < ApplicationService::Base input :user, type: User input :title, type: String status_active! :user make :create_post private def create_post # ... end end ``` ## Creating extensions ### Using generator ```shell rails generate servactory:extension MyExtension ``` Options: | Option | Default | Description | |--------|---------|-------------| | `--path` | `app/services/application_service/extensions` | Output directory | | `--namespace` | `ApplicationService` | Base namespace | Examples: ```shell # Basic rails generate servactory:extension Auditable # Nested namespace rails generate servactory:extension Admin::AuditTrail # Custom path rails generate servactory:extension MyExtension --path=lib/extensions ``` ### Extension structure ::: code-group ```ruby [app/services/application_service/extensions/my_extension/dsl.rb] module ApplicationService module Extensions module MyExtension module DSL def self.included(base) base.extend(ClassMethods) base.include(InstanceMethods) end module ClassMethods private def my_extension!(value) stroma.settings[:actions][:my_extension][:value] = value end end module InstanceMethods private def call!(**) value = self.class.stroma.settings[:actions][:my_extension][:value] if value.present? # Before logic end super # After logic end end end end end end ``` ::: ### Module structure explanation | Module | Purpose | |--------|---------| | `DSL` | Entry point module, connected via hooks | | `ClassMethods` | DSL methods called at class definition time | | `InstanceMethods` | Runtime methods called during service execution | * `base.extend(ClassMethods)` — adds class-level configuration methods * `base.include(InstanceMethods)` — adds instance-level runtime behavior ### File organization For complex extensions, split into separate files: ``` extensions/my_extension/ ├── dsl.rb # Main DSL module with self.included ├── class_methods.rb # ClassMethods module └── instance_methods.rb # InstanceMethods module ``` ## Connecting extensions ### Hooks: before and after ```ruby class ApplicationService::Base < Servactory::Base extensions do before :actions, ApplicationService::Extensions::Authorization::DSL after :actions, ApplicationService::Extensions::Publishable::DSL end end ``` ### Available hook keys Hooks can be attached to these stages (in execution order): | Key | Description | |-----|-------------| | `:configuration` | Service configuration | | `:info` | Service info | | `:context` | Context setup | | `:inputs` | Input processing | | `:internals` | Internal attributes | | `:outputs` | Output processing | | `:actions` | Action execution | Most extensions use `:actions` — the main execution point. ### Multiple extensions ```ruby {4-9} class ApplicationService::Base < Servactory::Base extensions do # Before hooks (execute in order) before :actions, ApplicationService::Extensions::Authorization::DSL before :actions, ApplicationService::Extensions::StatusActive::DSL # After hooks (execute in order) after :actions, ApplicationService::Extensions::Publishable::DSL after :actions, ApplicationService::Extensions::PostCondition::DSL end end ``` ### Execution order 1. `before` hooks execute in declaration order 2. Service actions (`make` methods) 3. `after` hooks execute in declaration order ### Understanding `super` Extensions form a call chain. `super` passes execution to the next module: ```ruby def call!(**) # Before logic (runs first) settings = self.class.stroma.settings[:actions][:my_extension] fail!(message: "Not configured") if settings[:required] && settings[:value].blank? super # Calls next extension or service actions # After logic (runs after service completes) Rails.logger.info("Service completed: #{self.class.name}") end ``` | Pattern | `super` placement | Use case | |---------|-------------------|----------| | Before | Logic before `super` | Validation, authorization | | After | Logic after `super` | Logging, publishing | | Around | Wrap `super` | Transactions, timing | | Short-circuit | Skip `super` | Caching, early return | ### Organizing complex extensions For extensions with complex logic, isolate it into dedicated `Tools` classes instead of adding methods to the extension module. This pattern is used internally by Servactory. **File structure:** ``` extensions/authorization/ ├── dsl.rb └── tools/ └── permission_checker.rb ``` ::: code-group ```ruby [dsl.rb] module ApplicationService module Extensions module Authorization module DSL def self.included(base) base.extend(ClassMethods) base.include(InstanceMethods) end module ClassMethods private def authorize_with(method_name) stroma.settings[:actions][:authorization][:method_name] = method_name end end module InstanceMethods private def call!(incoming_arguments: {}, **) method_name = self.class.stroma.settings[:actions][:authorization][:method_name] if method_name.present? # PORO class for extension logic, not a Servactory service Tools::PermissionChecker.check!(self, incoming_arguments, method_name) end super end end end end end end ``` ```ruby [tools/permission_checker.rb] module ApplicationService module Extensions module Authorization module Tools class PermissionChecker def self.check!(...) new(...).check! end def initialize(context, arguments, method_name) @context = context @arguments = arguments @method_name = method_name end def check! authorized = @context.send(@method_name, @arguments) return if authorized @context.fail!( :unauthorized, message: "Not authorized to perform this action" ) end end end end end end ``` ::: **Benefits:** * Logic is isolated in dedicated classes * No method pollution in extension modules * Easy to test each Tool in isolation * Scales well for complex extensions ## Stroma settings Extensions store configuration in Stroma settings. ### Key structure ``` stroma.settings[:registry_key][:extension_name][:setting_key] ``` | Level | Description | Example | |-------|-------------|---------| | `registry_key` | Hook target | `:actions` | | `extension_name` | Extension identifier | `:authorization` | | `setting_key` | Specific setting | `:method_name` | ### Writing settings In `ClassMethods`: ```ruby def authorize_with(method_name) stroma.settings[:actions][:authorization][:method_name] = method_name end ``` ### Reading settings In `InstanceMethods`: ```ruby def call!(**) method_name = self.class.stroma.settings[:actions][:authorization][:method_name] # ... super end ``` ### Auto-vivification Nested objects are created automatically on first access: ```ruby # This works without explicit initialization stroma.settings[:actions][:my_extension][:enabled] = true stroma.settings[:actions][:my_extension][:options] = { timeout: 30 } ``` ## Extension patterns ### Before pattern Validate or check conditions before service execution. ::: code-group ```ruby [extensions/authorization/dsl.rb] module ApplicationService module Extensions module Authorization module DSL def self.included(base) base.extend(ClassMethods) base.include(InstanceMethods) end module ClassMethods private def authorize_with(method_name) stroma.settings[:actions][:authorization][:method_name] = method_name end end module InstanceMethods private def call!(incoming_arguments: {}, **) method_name = self.class.stroma.settings[:actions][:authorization][:method_name] if method_name.present? authorized = send(method_name, incoming_arguments) unless authorized fail!( :unauthorized, message: "Not authorized to perform this action" ) end end super end end end end end end ``` ```ruby [Usage] class Posts::Delete < ApplicationService::Base input :post, type: Post input :user, type: User authorize_with :user_can_delete? make :delete_post private def user_can_delete?(args) args[:user].admin? || args[:post].author_id == args[:user].id end def delete_post inputs.post.destroy! end end ``` ::: ### Around pattern Wrap service execution in a context. ::: code-group ```ruby [extensions/transactional/dsl.rb] module ApplicationService module Extensions module Transactional module DSL def self.included(base) base.extend(ClassMethods) base.include(InstanceMethods) end module ClassMethods private def transactional!(transaction_class: nil) stroma.settings[:actions][:transactional][:enabled] = true stroma.settings[:actions][:transactional][:class] = transaction_class end end module InstanceMethods private def call!(**) settings = self.class.stroma.settings[:actions][:transactional] enabled = settings[:enabled] unless enabled super return end transaction_class = settings[:class] fail!(message: "Transaction class not configured") if transaction_class.nil? transaction_class.transaction { super } end end end end end end ``` ```ruby [Usage] class Orders::Create < ApplicationService::Base transactional! transaction_class: ActiveRecord::Base input :user, type: User input :items, type: Array output :order, type: Order make :create_order make :create_line_items make :charge_payment private def create_order outputs.order = Order.create!(user: inputs.user) end def create_line_items inputs.items.each do |item| outputs.order.line_items.create!(item) end end def charge_payment Payments::Charge.call!(amount: outputs.order.total_amount) end end ``` ::: ### After pattern Process results after service execution. ::: code-group ```ruby [extensions/publishable/dsl.rb] module ApplicationService module Extensions module Publishable module DSL def self.included(base) base.extend(ClassMethods) base.include(InstanceMethods) end module ClassMethods private def publishes(event_name, with: nil, event_bus: nil) stroma.settings[:actions][:publishable][:configurations] ||= [] stroma.settings[:actions][:publishable][:configurations] << { event_name:, payload_method: with, event_bus: } end end module InstanceMethods private def call!(**) super configurations = self.class.stroma.settings[:actions][:publishable][:configurations] || [] configurations.each do |config| event_name = config[:event_name] payload_method = config[:payload_method] event_bus = config[:event_bus] payload = payload_method.present? ? send(payload_method) : {} event_bus.publish(event_name, payload) end end end end end end end ``` ```ruby [Usage] class Users::Create < ApplicationService::Base publishes :user_created, with: :user_payload, event_bus: EventPublisher input :email, type: String input :name, type: String output :user, type: User make :create_user private def create_user outputs.user = User.create!(email: inputs.email, name: inputs.name) end def user_payload { user_id: outputs.user.id, email: outputs.user.email } end end ``` ::: ### Rescue pattern Handle errors and perform cleanup. ::: code-group ```ruby [extensions/rollbackable/dsl.rb] module ApplicationService module Extensions module Rollbackable module DSL def self.included(base) base.extend(ClassMethods) base.include(InstanceMethods) end module ClassMethods private def on_rollback(method_name) stroma.settings[:actions][:rollbackable][:method_name] = method_name end end module InstanceMethods private def call!(**) super rescue StandardError => e raise e if e.is_a?(Servactory::Exceptions::Success) method_name = self.class.stroma.settings[:actions][:rollbackable][:method_name] send(method_name) if method_name.present? raise end end end end end end ``` ```ruby [Usage] class Payments::Process < ApplicationService::Base on_rollback :cleanup_resources input :order, type: Order input :payment_method, type: PaymentMethod output :payment, type: Payment make :reserve_inventory make :charge_payment make :confirm_order private def reserve_inventory Inventory::Reserve.call!(items: inputs.order.items) end def charge_payment result = Payments::Charge.call!( payment_method: inputs.payment_method, amount: inputs.order.total_amount ) outputs.payment = result.payment end def confirm_order inputs.order.confirm! end def cleanup_resources Inventory::Release.call!(items: inputs.order.items) Payments::Refund.call!(payment: outputs.payment) if outputs.payment.present? end end ``` ::: ## Migration from 2.x ::: warning `Servactory::DSL.with_extensions(...)` is deprecated and will be removed in future releases. Please migrate to the new `extensions do` block syntax. ::: ### Syntax changes ::: code-group ```ruby [3.x (Current)] module ApplicationService class Base < Servactory::Base extensions do before :actions, ApplicationService::Extensions::StatusActive::DSL end end end ``` ```ruby [2.x (Legacy)] module ApplicationService class Base include Servactory::DSL.with_extensions( ApplicationService::Extensions::StatusActive::DSL ) end end ``` ::: ### Settings storage changes | Aspect | 3.x | 2.x | |--------|-----|-----| | Storage | `stroma.settings[:key][:ext][:setting]` | `attr_accessor` (class instance variable) | | Access | `self.class.stroma.settings[:key][:ext][:setting]` | `self.class.send(:var)` | | Inheritance | Automatic deep copy | Manual handling | ### Extension code changes ::: code-group ```ruby [3.x (Current)] module ClassMethods private def status_active!(model_name) stroma.settings[:actions][:status_active][:model_name] = model_name end end module InstanceMethods private def call!(**) model_name = self.class.stroma.settings[:actions][:status_active][:model_name] # ... super end end ``` ```ruby [2.x (Legacy)] module ClassMethods private attr_accessor :status_active_model_name def status_active!(model_name) self.status_active_model_name = model_name end end module InstanceMethods private def call!(**) super model_name = self.class.send(:status_active_model_name) # ... end end ``` ::: --- --- url: 'https://servactory.com/featury/guide/features.md' description: Description and examples of using the Featury feature object --- # Feature object Featury A feature object can contain work with either one specific feature flag or several feature flags. Several feature flags can also be represented as a group — a nested feature object. ## Prefix A feature object always has a prefix. By default, it is built based on the class name of the feature object. For example, for `User::OnboardingFeature` the default prefix will be `user_onboarding`. You can change the prefix using the `prefix` method: ```ruby class User::OnboardingFeature < ApplicationFeature prefix :onboarding # [!code focus] # ... end ``` ## Resources A feature object may expect a resource to be passed to it as input. These resources can be used in actions as an addition to working with feature flags. ### Options #### Option `option` Makes the resource optional for calling the feature object. Defaults to `false`. ```ruby class User::OnboardingFeature < ApplicationFeature prefix :onboarding resource :user, type: User, option: true # [!code focus] # ... end ``` #### Option `nested` Passes a resource to nested feature objects via `groups`. Defaults to `false`. ```ruby class User::OnboardingFeature < ApplicationFeature prefix :onboarding resource :user, type: User, nested: true # [!code focus] # ... end ``` ## Condition A feature object can contain a basic condition for operation. For example, this can be useful if you want to allow work with a resource only in a certain state. ```ruby class User::OnboardingFeature < ApplicationFeature prefix :onboarding resource :user, type: User condition ->(resources:) { resources.user.onboarding_awaiting? } # [!code focus] # ... end ``` ## Set of features Within a single feature object, you can specify one feature flag or several feature flags. ::: code-group ```ruby [One] class User::OnboardingFeature < ApplicationFeature prefix :onboarding resource :user, type: User condition ->(resources:) { resources.user.onboarding_awaiting? } features :passage # [!code focus] end ``` ```ruby [Several] class User::OnboardingFeature < ApplicationFeature prefix :onboarding resource :user, type: User condition ->(resources:) { resources.user.onboarding_awaiting? } features :passage, :integration # [!code focus] end ``` ::: Together with the `onboarding` prefix, an example of which is presented above, these feature flags will be collected: ::: code-group ```ruby [One] # => onboarding_passage ``` ```ruby [Several] # => onboarding_passage # => onboarding_integration ``` ::: ## Groups of feature sets ```ruby class User::OnboardingFeature < ApplicationFeature prefix :onboarding resource :user, type: User condition ->(resources:) { resources.user.onboarding_awaiting? } features :passage groups BillingFeature, # [!code focus] PaymentSystemFeature # [!code focus] end ``` --- --- url: 'https://servactory.com/getting-started.md' description: 'Requirements, conventions, installation and example of basic preparation' --- # Getting started with Servactory ## Conventions * Services inherit from `Servactory::Base` and reside in `app/services`. Common practice: create `ApplicationService::Base` as the base class for your services. * Group services by domain using namespaces, and name each class as a verb describing its action. Example: `Users::Create`, `Orders::Process`. Avoid adding `Service` to the namespace — the `app/services` directory already provides that context. ## Version support | Ruby/Rails | 8.1 | 8.0 | 7.2 | 7.1 | 7.0 | 6.1 | 6.0 | 5.2 | 5.1 | 5.0 | |------------|---|---|---|---|---|---|---|---|---|---| | 4.0 | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | | 3.4 | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | | 3.3 | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | | 3.2 | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | | 3.1 | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ## Installation Add this to `Gemfile`: ```ruby gem "servactory" ``` And execute: ```shell bundle install ``` ## Preparation First, prepare the base class for inheritance. ### Automatically Run the generator: ```shell bundle exec rails g servactory:install ``` This creates all necessary files. See [Rails Generators](/guide/rails/generators) for all available options and generators. ### Manually #### ApplicationService::Exceptions ::: code-group ```ruby [app/services/application_service/exceptions.rb] module ApplicationService module Exceptions class Input < Servactory::Exceptions::Input; end class Output < Servactory::Exceptions::Output; end class Internal < Servactory::Exceptions::Internal; end class Failure < Servactory::Exceptions::Failure; end end end ``` ::: #### ApplicationService::Result ::: code-group ```ruby [app/services/application_service/result.rb] module ApplicationService class Result < Servactory::Result; end end ``` ::: #### ApplicationService::Base ::: code-group ```ruby [app/services/application_service/base.rb] module ApplicationService class Base < Servactory::Base configuration do input_exception_class ApplicationService::Exceptions::Input internal_exception_class ApplicationService::Exceptions::Internal output_exception_class ApplicationService::Exceptions::Output failure_class ApplicationService::Exceptions::Failure result_class ApplicationService::Result end end end ``` ::: ## First service Create your first service: ```shell bundle exec rails g servactory:service users/create first_name middle_name last_name ``` Generate a spec file: ```shell bundle exec rails g servactory:rspec users/create first_name middle_name last_name ``` --- --- url: 'https://servactory.com/datory/getting-started.md' description: >- Datory is a data mapping tool for serialization and deserialization in Ruby applications --- # Getting started with Datory ## Version support | Ruby/Rails | 8.1 | 8.0 | 7.2 | 7.1 | 7.0 | 6.1 | 6.0 | 5.2 | 5.1 | 5.0 | |------------|---|---|---|---|---|---|---|---|---|---| | 4.0 | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | | 3.4 | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | | 3.3 | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | | 3.2 | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | | 3.1 | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ## Installation Add this to `Gemfile`: ```ruby gem "datory" ``` And execute: ```shell bundle install ``` ## Preparation As a first step, it is recommended to prepare the base class for further inheritance. ### For DTOs ::: code-group ```ruby [app/dtos/application_dto/base.rb] module ApplicationDTO class Base < Datory::Base end end ``` ::: ### For forms ::: code-group ```ruby [app/forms/application_form/base.rb] module ApplicationForm class Base < Datory::Base end end ``` ::: --- --- url: 'https://servactory.com/featury/getting-started.md' description: Featury is a feature flag and feature object framework for Ruby applications --- # Getting started with Featury ## Conventions * All feature classes are subclasses of `Featury::Base` and are located in the `app/features` directory. Common practice: create `ApplicationFeature::Base` as the base class for your features. * Use namespaces that match model names, and name the class as a noun describing the process. Use noun forms: `RegistrationFeature`, not `RegisterFeature`. Example: `User::RegistrationFeature`, `Order::FulfillmentFeature`. ## Version support | Ruby/Rails | 8.1 | 8.0 | 7.2 | 7.1 | 7.0 | 6.1 | 6.0 | 5.2 | 5.1 | 5.0 | |------------|---|---|---|---|---|---|---|---|---|---| | 4.0 | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | | 3.4 | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | | 3.3 | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | | 3.2 | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | | 3.1 | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ## Installation Add this to `Gemfile`: ```ruby gem "featury" ``` And execute: ```shell bundle install ``` ## Preparation As a first step, it is recommended to prepare the base class for further inheritance. This base class should contain actions within itself with the integration of the tool for features in the project. For example, it could be an ActiveRecord model, Flipper, or something else. ### ActiveRecord model The `FeatureFlag` model will be used as an example. ::: code-group ```ruby [app/features/application_feature/base.rb] module ApplicationFeature class Base < Featury::Base action :enabled? do |features:, **options| features.all? do |feature| FeatureFlag .find_or_create_by!(code: feature, actor: options[:user]) .enabled? end end action :disabled? do |features:, **options| features.any? do |feature| !FeatureFlag .find_or_create_by!(code: feature, actor: options[:user]) .enabled? end end action :enable do |features:, **options| features.all? do |feature| FeatureFlag .find_or_create_by!(code: feature, actor: options[:user]) .update!(enabled: true) end end action :disable do |features:, **options| features.all? do |feature| FeatureFlag .find_or_create_by!(code: feature, actor: options[:user]) .update!(enabled: false) end end before do |action:, features:| Slack::API::Notify.call!(action:, features:) end after :enabled?, :disabled? do |action:, features:| Slack::API::Notify.call!(action:, features:) end end end ``` ::: --- --- url: 'https://servactory.com/guide/actions/grouping.md' description: Description and examples of grouping actions (methods) in service --- # Grouping actions Group multiple methods into one execution group via the `stage` method. :::info Usage of the `position` option for `make` will sort only in `stage`. ::: ```ruby stage do make :create_user! make :create_blog_for_user! make :create_post_for_user_blog! end ``` ### Option `only_if` Checks the `only_if` condition before calling methods inside `stage`. ```ruby {2} stage do only_if ->(context:) { Settings.features.preview.enabled } make :create_user! make :create_blog_for_user! make :create_post_for_user_blog! end ``` ### Option `only_unless` The opposite of the `only_if` option. ```ruby {2} stage do only_unless ->(context:) { Settings.features.preview.disabled } make :create_user! make :create_blog_for_user! make :create_post_for_user_blog! end ``` ### Option `wrap_in` Wrap methods in `stage` with a wrapper. Example: `ActiveRecord::Base.transaction` from Rails. ```ruby {2} stage do wrap_in ->(methods:, context:) { ActiveRecord::Base.transaction { methods.call } } make :create_user! make :create_blog_for_user! make :create_post_for_user_blog! end ``` ### Option `rollback` Handle exceptions from methods in the group or from `wrap_in` via the `rollback` method. ```ruby {3,12} stage do wrap_in ->(methods:, context:) { ActiveRecord::Base.transaction { methods.call } } rollback :clear_data_and_fail! make :create_user! make :create_blog_for_user! make :create_post_for_user_blog! end # ... def clear_data_and_fail!(e) # ... fail!(message: "Failed to create data: #{e.message}") end ``` --- --- url: 'https://servactory.com/datory/guide/info.md' description: >- Description and examples of using the method to obtain information about a Datory object --- # Info Information can be obtained about each Datory object. There are two options for this. ## Method `info` ::: code-group ```ruby [Example] SerialDto.info ``` ```text [Result] # {:from=>{:name=>:id, :type=>String, :min=>nil, :max=>nil, :consists_of=>false, :format=>:uuid}, :to=>{:name=>:id, :type=>String, :min=>nil, :max=>nil, :consists_of=>false, :format=>:uuid, :required=>true, :default=>nil, :include=>nil}}, :status=> {:from=>{:name=>:status, :type=>String, :min=>nil, :max=>nil, :consists_of=>false, :format=>nil}, :to=>{:name=>:status, :type=>String, :min=>nil, :max=>nil, :consists_of=>false, :format=>nil, :required=>true, :default=>nil, :include=>nil}}, :title=> {:from=>{:name=>:title, :type=>String, :min=>nil, :max=>nil, :consists_of=>false, :format=>nil}, :to=>{:name=>:title, :type=>String, :min=>nil, :max=>nil, :consists_of=>false, :format=>nil, :required=>true, :default=>nil, :include=>nil}}, :poster=> {:from=>{:name=>:poster, :type=>[ImageDto, Hash], :min=>nil, :max=>nil, :consists_of=>false, :format=>nil}, :to=>{:name=>:poster, :type=>[ImageDto, Hash], :min=>nil, :max=>nil, :consists_of=>false, :format=>nil, :required=>true, :default=>nil, :include=>ImageDto}}, :ratings=> {:from=>{:name=>:ratings, :type=>[RatingsDto, Hash], :min=>nil, :max=>nil, :consists_of=>false, :format=>nil}, :to=>{:name=>:ratings, :type=>[RatingsDto, Hash], :min=>nil, :max=>nil, :consists_of=>false, :format=>nil, :required=>true, :default=>nil, :include=>RatingsDto}}, :countries=> {:from=>{:name=>:countries, :type=>Array, :min=>nil, :max=>nil, :consists_of=>[CountryDto, Hash], :format=>nil}, :to=>{:name=>:countries, :type=>Array, :min=>nil, :max=>nil, :consists_of=>[CountryDto, Hash], :format=>nil, :required=>true, :default=>nil, :include=>CountryDto}}, :genres=> {:from=>{:name=>:genres, :type=>Array, :min=>nil, :max=>nil, :consists_of=>[GenreDto, Hash], :format=>nil}, :to=>{:name=>:genres, :type=>Array, :min=>nil, :max=>nil, :consists_of=>[GenreDto, Hash], :format=>nil, :required=>true, :default=>nil, :include=>GenreDto}}, :seasons=> {:from=>{:name=>:seasons, :type=>Array, :min=>nil, :max=>nil, :consists_of=>[SeasonDto, Hash], :format=>nil}, :to=>{:name=>:seasons, :type=>Array, :min=>nil, :max=>nil, :consists_of=>[SeasonDto, Hash], :format=>nil, :required=>true, :default=>nil, :include=>SeasonDto}}, :premieredOn=> {:from=>{:name=>:premieredOn, :type=>String, :min=>nil, :max=>nil, :consists_of=>false, :format=>:date}, :to=>{:name=>:premiered_on, :type=>Date, :min=>nil, :max=>nil, :consists_of=>false, :format=>nil, :required=>true, :default=>nil, :include=>nil}}}> ``` ::: ## Method `describe` ::: code-group ```ruby [Example] SerialDto.describe ``` ```text [Result] ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | SerialDto | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | Attribute | From | To | As | Include | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | id | String | id | String | | | status | String | status | String | | | title | String | title | String | | | poster | [ImageDto, Hash] | poster | [ImageDto, Hash] | ImageDto | | | ratings | [RatingsDto, Hash] | ratings | [RatingsDto, Hash] | RatingsDto | | | countries | Array | countries | Array | CountryDto | | genres | Array | genres | Array | GenreDto | | seasons | Array | seasons | Array | SeasonDto | | premieredOn | String | premiered_on | Date | | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ``` ::: --- --- url: 'https://servactory.com/featury/guide/info.md' description: >- Description and examples of using the method to obtain information about a Featury object --- # Information about Featury object Information can be obtained about each Featury object. ## Method `info` ```ruby [Example] info = User::OnboardingFeature.info ``` ```ruby info.features # Feature flags of the current class. info.groups # Feature flag groups of the current class. info.tree # Tree of feature flags from the current class. ``` --- --- url: 'https://servactory.com/guide/i18n.md' description: Information about internationalization (Ruby I18n) --- # Internationalization (I18n) All texts are stored in localization files. Change or supplement them with new locales. ## Available Locales | Language | File | |----------|------| | German | [de.yml](https://github.com/servactory/servactory/blob/main/config/locales/de.yml) | | English | [en.yml](https://github.com/servactory/servactory/blob/main/config/locales/en.yml) | | Spanish | [es.yml](https://github.com/servactory/servactory/blob/main/config/locales/es.yml) | | French | [fr.yml](https://github.com/servactory/servactory/blob/main/config/locales/fr.yml) | | Italian | [it.yml](https://github.com/servactory/servactory/blob/main/config/locales/it.yml) | | Russian | [ru.yml](https://github.com/servactory/servactory/blob/main/config/locales/ru.yml) | --- --- url: 'https://servactory.com/guide/testing/rspec/migration.md' description: How to migrate from Legacy RSpec helpers to the new Fluent API --- # Migration Guide This guide helps you migrate from the Legacy RSpec helpers to the new Fluent API. ## Quick Reference | Legacy | Fluent | |--------|--------| | `allow_service_as_success!(S) { out }` | `allow_service!(S).succeeds(out)` | | `allow_service_as_success(S) { out }` | `allow_service(S).succeeds(out)` | | `allow_service_as_failure!(S) { exc }` | `allow_service!(S).fails(type:, message:)` | | `allow_service_as_failure(S) { exc }` | `allow_service(S).fails(type:, message:)` | ## Migrating Success Mocks ### Basic Success ::: code-group ```ruby [Legacy] before do allow_service_as_success!(Users::Create) do { user: user } end end ``` ```ruby [Fluent] before do allow_service!(Users::Create) .succeeds(user: user) end ``` ::: ### Success with Input Matching ::: code-group ```ruby [Legacy] before do allow_service_as_success!(PaymentService, with: { amount: 100 }) do { transaction_id: "txn_123" } end end ``` ```ruby [Fluent] before do allow_service!(PaymentService) .with(amount: 100) .succeeds(transaction_id: "txn_123") end ``` ::: ## Migrating Failure Mocks ### Basic Failure ::: code-group ```ruby [Legacy] before do allow_service_as_failure!(PaymentService) do { exception: ApplicationService::Exceptions::Failure.new( type: :payment_declined, message: "Card declined" ) } end end ``` ```ruby [Fluent] before do allow_service!(PaymentService) .fails(type: :payment_declined, message: "Card declined") end ``` ::: ### Failure with Meta ::: code-group ```ruby [Legacy] before do allow_service_as_failure!(ValidationService) do { exception: ApplicationService::Exceptions::Failure.new( type: :validation, message: "Invalid input", meta: { field: :email } ) } end end ``` ```ruby [Fluent] before do allow_service!(ValidationService) .fails( type: :validation, message: "Invalid input", meta: { field: :email } ) end ``` ::: ### Failure with Custom Exception Class ::: code-group ```ruby [Legacy] before do allow_service_as_failure!(PaymentService) do { exception: CustomPaymentException.new( type: :declined, message: "Insufficient funds" ) } end end ``` ```ruby [Fluent] before do allow_service!(PaymentService) .fails( CustomPaymentException, type: :declined, message: "Insufficient funds" ) end ``` ::: ## New Features in Fluent API ### Sequential Calls Test services that are called multiple times with different results: ```ruby before do allow_service!(RetryService) .succeeds(status: :pending) .then_succeeds(status: :processing) .then_succeeds(status: :completed) end ``` ### Failure After Success Test retry scenarios where service eventually fails: ```ruby before do allow_service!(ExternalApiService) .succeeds(response: { status: "pending" }) .then_fails(type: :timeout, message: "Request timed out") end ``` ### Custom Exception Classes ```ruby before do allow_service!(PaymentService) .fails( CustomPaymentException, type: :declined, message: "Insufficient funds" ) end ``` ## Key Differences | Aspect | Legacy | Fluent | |--------|--------|--------| | Style | Block-based | Method chaining | | Outputs | Returned from block | Passed as arguments | | Exceptions | Create manually in block | Built from parameters | | Sequential | Not supported | `then_succeeds`, `then_fails` | | Validation | Basic | Automatic against service | ## What Doesn't Need Migration The following components are **unchanged** between Legacy and Fluent APIs: ### Matchers All matchers work identically in both APIs: * `have_input` / `have_service_input` * `have_internal` / `have_service_internal` * `have_output` / `have_service_output` * `be_success_service` * `be_failure_service` ```ruby # These tests work the same way in Legacy and Fluent it { expect { perform }.to have_input(:email).type(String).required } it { expect { perform }.to have_internal(:result).type(Servactory::Result) } it { expect(perform).to have_output(:user).instance_of(User) } it { expect(perform).to be_success_service } it { expect(perform).to be_failure_service } ``` ### Installation The installation process is identical — same requires, same RSpec configuration. --- --- url: 'https://servactory.com/datory/guide/data/nesting.md' description: Description and examples of use --- # Nested data ## Single ::: code-group ```ruby [Required] one! :poster, include: ImageDto ``` ```ruby [Optional] one? :poster, include: ImageDto ``` ::: ## Multiple ::: code-group ```ruby [Required] many! :seasons, include: SeasonDto ``` ```ruby [Optional] many? :seasons, include: SeasonDto ``` ::: --- --- url: 'https://servactory.com/guide/actions/options.md' description: Description and examples of using options for actions (methods) in service --- # Options for actions ## Option `if` Checks the `if` condition before calling the method. ```ruby{2} make :something, if: ->(context:) { Settings.features.preview.enabled } def something # ... end ``` ## Option `unless` The opposite of the `if` option. ```ruby{2} make :something, unless: ->(context:) { Settings.features.preview.disabled } def something # ... end ``` ## Option `position` All methods have a position. Use `position` to call a method at a different time than it was added via `make`. Useful for service inheritance. ```ruby{3,14} class SomeApi::Base < ApplicationService::Base make :api_request!, position: 2 # ... end class SomeApi::Posts::Create < SomeApi::Base input :post_name, type: String # ... make :validate!, position: 1 private def validate! # ... end # ... end ``` --- --- url: 'https://servactory.com/guide/rails/generators.md' description: 'Generate services, specs, and extensions using Servactory generators' --- # Rails Generators Servactory provides Rails generators for common tasks. ## Installation generator Sets up the base service infrastructure. ```shell bundle exec rails g servactory:install ``` ### Generated files | File | Description | |------|-------------| | `app/services/application_service/base.rb` | Base service class | | `app/services/application_service/exceptions.rb` | Exception classes | | `app/services/application_service/result.rb` | Result class | ### Options | Option | Default | Description | |--------|---------|-------------| | `--path` | `app/services` | Output directory | | `--namespace` | `ApplicationService` | Base namespace | ### Examples ```shell # Default installation bundle exec rails g servactory:install # Custom namespace bundle exec rails g servactory:install --namespace=MyApp::Services # Custom path bundle exec rails g servactory:install --path=lib/services ``` ## Service generator Creates a new service with typed inputs. ```shell bundle exec rails g servactory:service NAME [inputs...] ``` ### Options | Option | Default | Description | |--------|---------|-------------| | `--path` | `app/services` | Output directory | ### Type shortcuts | Syntax | Result | |--------|--------| | `name` or `name:string` | `input :name, type: String` | | `age:integer` | `input :age, type: Integer` | | `active:boolean` | `input :active, type: [TrueClass, FalseClass]` | | `user:User` | `input :user, type: User` | | `items:array` | `input :items, type: Array` | | `data:hash` | `input :data, type: Hash` | ### Examples ```shell # Basic service bundle exec rails g servactory:service users/create # With typed inputs bundle exec rails g servactory:service orders/process user:User amount:integer # Nested namespace bundle exec rails g servactory:service admin/reports/generate started_on:date ended_on:date ``` ## RSpec generator Creates an RSpec test file for a service. ```shell bundle exec rails g servactory:rspec NAME [inputs...] ``` ### Options | Option | Default | Description | |--------|---------|-------------| | `--path` | `spec/services` | Output directory | ### Examples ```shell # Generate spec matching service inputs bundle exec rails g servactory:rspec users/create first_name last_name email # For existing service bundle exec rails g servactory:rspec orders/process ``` ## Extension generator Creates a new extension module. ```shell bundle exec rails g servactory:extension NAME ``` ### Options | Option | Default | Description | |--------|---------|-------------| | `--path` | `app/services/application_service/extensions` | Output directory | | `--namespace` | `ApplicationService` | Base namespace | ### Examples ```shell # Basic extension bundle exec rails g servactory:extension Auditable # Nested namespace bundle exec rails g servactory:extension Admin::AuditTrail # Custom path bundle exec rails g servactory:extension Cacheable --path=lib/extensions ``` See [Extensions](/guide/extensions) for usage details. --- --- url: 'https://servactory.com/releases/2.10.md' --- # Release 2.10 The following changes have been prepared and implemented. ## Actions ### Stage #### Methods `wrap_in` and `rollback` Added the use of the `fail!` method in case of failure inside `wrap_in` when there is no `rollback`. ## Testing ### RSpec #### Chain `with_output` Improved message when using the `with_output` chain. #### Helper `allow_service_as_failure` Fixed handling of the `on_failure` hook in the `allow_service_as_failure` helper. --- --- url: 'https://servactory.com/releases/2.11.md' --- # Release 2.11 The following changes have been prepared and implemented. ## Attributes ### Storage Changed how service input arguments are handled. Now data in all 3 attribute types is stored and processed identically. ## Ruby Support for Ruby 3.4 has been verified. --- --- url: 'https://servactory.com/releases/2.12.md' --- # Release 2.12 The following changes have been prepared and implemented. ## Attributes ### Options #### Changed `inclusion` The `inclusion` option has become a [dynamic option](../guide/options/dynamic#inclusion-option). #### Changed `schema` The `schema` option has become a [dynamic option](../guide/options/dynamic#schema-option). ### Storage The storage and interaction with data of the 3 attribute types has been redesigned. --- --- url: 'https://servactory.com/releases/2.13.md' --- # Release 2.13 The following changes have been prepared and implemented. ## Attributes ### Options #### Improved `type` Fixed the `type` option behavior with default values when using `TrueClass` and/or `FalseClass` in input. #### Improved `schema` Added support for the `prepare` option inside the `schema` option. Fixed the `schema` option behavior with default values when used in input. #### Improved `inclusion` Fixed the `inclusion` option behavior with default values when used in input. --- --- url: 'https://servactory.com/releases/2.14.md' --- # Release 2.14 The following changes have been prepared and implemented. ## Configuration ### Improved `action_shortcuts` Added support for extended mode for the `action_shortcuts` option. ```ruby configuration do action_shortcuts( { restrict: { # replacement for make prefix: :create, # method name prefix suffix: :restriction # method name suffix } } ) end ``` ```ruby class Payments::Restrictions::Create < ApplicationService::Base input :payment, type: Payment # The exclamation mark will be moved to the end of the method name restrict :payment! private def create_payment_restriction! inputs.payment.restrictions.create!( reason: "Suspicion of fraud" ) end end ``` ## Ruby Support for Ruby 3.5 Preview 1 has been verified. Support for Ruby 3.1 has been removed. --- --- url: 'https://servactory.com/releases/2.15.md' --- # Release 2.15 The following changes have been prepared and implemented. ## Attributes ### Internal Attribute * Added internal attribute validation when accessing it. * Removed support for the nested `prepare` option from the `schema` dynamic option. ### Output Attribute * Removed support for the nested `prepare` option from the `schema` dynamic option. ## Extensions * Improved internal workings for extensions. ## Testing ### RSpec * Improved behavior of `as_success` and `as_failure` methods for `allow_service_as_*` helpers. * Improved behavior of the `default` matcher for `nil` values. ## Other This release also contains other fixes and improvements. --- --- url: 'https://servactory.com/releases/2.16.md' --- # Release 2.16 The following changes have been prepared and implemented. ## Attributes ### Options Optimized options validation. ## Methods The `fail_input!`, `fail_internal!`, and `fail_output!` methods were applied in internal logic, bringing exception handling to a unified form. ## Service Information Service information available through calling the `.info` method from the service class, or information called directly from the service class. ### Added * Added information about stages and actions. * Added the `servactory?` method, designed for Servactory Web. ### Fixed * Fixed behavior with the `dynamic_options` configuration. --- --- url: 'https://servactory.com/releases/2.2.md' --- # Release 2.2 The following changes have been prepared and implemented. ## Attributes ### Internal Attribute * Added support for the `inclusion` option; * Added support for the `must` option; * Added support for helpers. ### Output Attribute * Added support for the `inclusion` option; * Added support for the `must` option; * Added support for helpers. ### Options * Added nested value validation for the `consists_of` option. ## Methods ### Method `success!` Added the `success!` method for manually completing the service early with success. ```ruby class Users::Confirmation::Send < ApplicationService::Base input :user, type: User make :skip_if_already_sent! # ... def skip_if_already_sent! return if user.need_confirmation? success! # [!code focus] end # ... end ``` ### Method `fail!` Added the `type` attribute for the `fail!` method. By default, the attribute has the value `:base`. You can specify any name and then use it when handling `Failure`. ```ruby class Users::Confirmation::Send < ApplicationService::Base input :user, type: User make :skip_if_already_sent! # ... def skip_if_already_sent! return if user.need_confirmation? fail!(:soft, message: "The confirmation has already been sent") # [!code focus] end # ... end ``` ## Service Result ### Hooks This release adds another approach to handling the service result. Support for two hooks has been added for `Result`. [More details](../guide/usage/result#hooks). #### Hook `on_success` ```ruby Users::Confirmation::Send .call(user:) .on_success do |outputs:| # [!code focus] redirect_to outputs.notification end ``` #### Hook `on_failure` ```ruby Users::Confirmation::Send .call(user:) .on_failure(:all) do |exception:| # [!code focus] flash.now[:message] = exception.message render :new end ``` ## Other This release also contains other fixes and improvements. --- --- url: 'https://servactory.com/releases/2.3.md' --- # Release 2.3 The following changes have been prepared and implemented. ## Configuration The configs and exception classes have been changed. The changes are demonstrated below. ::: code-group ```ruby [Changes] module ApplicationService module Errors # [!code --] module Exceptions # [!code ++] class InputError < Servactory::Errors::InputError; end # [!code --] class Input < Servactory::Exceptions::Input; end # [!code ++] class OutputError < Servactory::Errors::OutputError; end # [!code --] class Output < Servactory::Exceptions::Output; end # [!code ++] class InternalError < Servactory::Errors::InternalError; end # [!code --] class Internal < Servactory::Exceptions::Internal; end # [!code ++] class Failure < Servactory::Errors::Failure; end # [!code --] class Failure < Servactory::Exceptions::Failure; end # [!code ++] end end ``` ```ruby [Before] module ApplicationService module Errors class InputError < Servactory::Errors::InputError; end class OutputError < Servactory::Errors::OutputError; end class InternalError < Servactory::Errors::InternalError; end class Failure < Servactory::Errors::Failure; end end end ``` ```ruby [After] module ApplicationService module Exceptions class Input < Servactory::Exceptions::Input; end class Output < Servactory::Exceptions::Output; end class Internal < Servactory::Exceptions::Internal; end class Failure < Servactory::Exceptions::Failure; end end end ``` ::: ::: code-group ```ruby [Changes] configuration do input_error_class ApplicationService::Errors::InputError # [!code --] input_exception_class ApplicationService::Exceptions::Input # [!code ++] internal_error_class ApplicationService::Errors::InternalError # [!code --] internal_exception_class ApplicationService::Exceptions::Internal # [!code ++] output_exception_class ApplicationService::Exceptions::Output # [!code --] output_error_class ApplicationService::Errors::OutputError # [!code ++] failure_class ApplicationService::Errors::Failure # [!code --] failure_class ApplicationService::Exceptions::Failure # [!code ++] end ``` ```ruby [Before] configuration do input_error_class ApplicationService::Errors::InputError internal_error_class ApplicationService::Errors::InternalError output_error_class ApplicationService::Errors::OutputError failure_class ApplicationService::Errors::Failure end ``` ```ruby [After] configuration do input_exception_class ApplicationService::Exceptions::Input internal_exception_class ApplicationService::Exceptions::Internal output_exception_class ApplicationService::Exceptions::Output failure_class ApplicationService::Exceptions::Failure end ``` ::: --- --- url: 'https://servactory.com/releases/2.4.md' --- # Release 2.4 The following changes have been prepared and implemented. ## Attributes ### Options #### Dynamic Options [Dynamic options](../guide/options/dynamic) have been implemented. ::: code-group ```ruby [format] input :email, type: String, format: :email # [!code focus] input :password, type: String, format: :password # [!code focus] ``` ```ruby [min] input :page_number, type: Integer, min: 1 # [!code focus] ``` ```ruby [max] input :page_size, type: Integer, min: 1, max: 20 # [!code focus] ``` ```ruby [custom] input :token, type: String, token: { # [!code focus] is: :jwt, # [!code focus] message: "Invalid token" # [!code focus] } # [!code focus] ``` ::: #### Option `consists_of` Added the ability to disable the `consists_of` option using the `false` value. ```ruby input :ids, type: Array, consists_of: false # [!code focus] ``` ## Methods ### Method `fail_input!` #### Added support for the `meta` attribute ```ruby fail_input!( :invoice_number, message: "Invalid invoice number", meta: { # [!code focus] received_invoice_number: inputs.invoice_number # [!code focus] } # [!code focus] ) ``` ### Method `fail_internal!` #### Added support for the `meta` attribute ```ruby fail_internal!( :invoice_number, message: "Invalid invoice number", meta: { # [!code focus] received_invoice_number: internals.invoice_number # [!code focus] } # [!code focus] ) ``` ### Method `fail_output!` #### Added support for the `meta` attribute ```ruby fail_output!( :invoice_number, message: "Invalid invoice number", meta: { # [!code focus] received_invoice_number: outputs.invoice_number # [!code focus] } # [!code focus] ) ``` --- --- url: 'https://servactory.com/releases/2.5.md' --- # Release 2.5 The following changes have been prepared and implemented. ## Attributes ### Options #### Dynamic Options New formats have been implemented for the [dynamic option](../guide/options/dynamic) [format](../guide/options/dynamic#format-option): * `uuid`; * `duration`. ## Configuration ### Added `result_class` Added the [`result_class`](../guide/configuration#for-result) configuration for changing the `Result` class. ::: code-group ```ruby [app/services/application_service/base.rb] configuration do result_class ApplicationService::Result end ``` ```ruby [examples/application_service/result.rb] module ApplicationService class Result < Servactory::Result; end end ``` ::: ### Added `predicate_methods_enabled` Added the [`predicate_methods_enabled`](../guide/configuration#predicate-methods) configuration for disabling predicate methods. ::: code-group ```ruby [app/services/application_service/base.rb] configuration do predicate_methods_enabled false end ``` ::: ## Testing ### RSpec Helpers and matchers for RSpec have been implemented. More details can be found [here](../guide/testing/rspec/legacy). ## Ruby Support for Ruby 2.7 has been removed. ## Datory This release accompanies a new library — [Datory](../datory/getting-started). [Datory](../datory/getting-started) is based on Servactory and allows you to quickly and reliably implement objects for data serialization and deserialization. --- --- url: 'https://servactory.com/releases/2.6.md' --- # Release 2.6 The following changes have been prepared and implemented. ## Attributes ### Options #### Changed `consists_of` The `consists_of` option has become a [dynamic option](../guide/options/dynamic#consists-of-option). #### Changed `service_class_name` The `service_class_name` attribute, which is available in some options, has been replaced with a new `service` attribute. The new attribute is an object containing a prepared data set: `class_name`. As well as a method for translation: `translate`. ## Configuration ### Added `i18n_root_key` Added the `i18n_root_key` configuration for renaming the root translation key. The default value is `servactory`. ::: code-group ```ruby [app/services/application_service/base.rb] configuration do i18n_root_key :datory end ``` ::: --- --- url: 'https://servactory.com/releases/2.7.md' --- # Release 2.7 The following changes have been prepared and implemented. ## Attributes ### Options #### Option `multiple_of` Added the dynamic option `multiple_of`. More details [here](../guide/options/dynamic#multiple-of-option). ## Ruby Support for Ruby 3.0 has been removed. ## Rails Support for Rails 7.2 has been added. --- --- url: 'https://servactory.com/releases/2.8.md' --- # Release 2.8 The following changes have been prepared and implemented. ## Service Result Added the `to_h` method to `Result`. ## Configuration Deprecated configuration methods have been removed: `input_error_class`, `internal_error_class`, and `output_error_class`. --- --- url: 'https://servactory.com/releases/2.9.md' --- # Release 2.9 The following changes have been prepared and implemented. ## Datory Native support for Datory objects has been added. ## Testing ### RSpec Added support for the `with` option for `allow_*` helpers. [More details](../guide/testing/rspec/legacy#contains). The functionality implemented earlier has been improved. ## Ruby Support for Ruby 3.4 Preview 2 has been verified. ## Rails Support for Rails 8.0 has been added. For Rails 8.0, Ruby version 3.1 is not supported. --- --- url: 'https://servactory.com/releases/3.0.md' --- # Release 3.0 The following changes have been prepared and implemented. ## Architecture ### Stroma * [Stroma](https://github.com/servactory/stroma) — the library core responsible for managing modules, hooks, and extensions. * Added `before` and `after` hook system for service customization. * Reorganized structure with hierarchical module settings storage. ### Configuration * Refactoring to prevent shared state between parent and child classes. ## Attributes ### Dynamic Options * Added the `target` option for class validation; * Added Range support in the `inclusion` option; * Improved validation: nil guards, type checking in format, Float precision; * Refined `must` option: `is` lambda accepts only strict `true` value. ## Actions ### Stage * Improved stage functionality; * Renamed methods: `should_skip?`, `condition_met?`. ## Extensions * Extensions now work through the Stroma hook system with `before` and `after` support; * Added extension examples: Authorization, Transactional, Idempotent, Rollbackable, and others; * Added generator for creating extensions. See the [Extensions documentation](/guide/extensions) for details. ## Testing ### RSpec * Test Kit redesign with modular Registry DSL architecture; * Added Fluent API for service mocking; * Added cross-service exception handling support. ## Rails Generators Added and improved generators for Rails: * `servactory:install` — installation and setup; * `servactory:service` — service creation with inputs and typing support; * `servactory:rspec` — RSpec test creation; * `servactory:extension` — extension creation. Improvements include: custom path support, type normalization, extended Rails 5.1-8.1 compatibility. ## I18n Added new locales: German (de), French (fr), Spanish (es), Italian (it). ## Ruby Support for Ruby 4.0.0 has been verified. ## Rails Support for Rails 8.1 has been verified. ## Other This release also contains other fixes and improvements. --- --- url: 'https://servactory.com/guide/testing/rspec/fluent.md' description: Description and examples of service testing using RSpec --- # RSpec This page documents the recommended testing helpers with method chaining support. ## Installation ::: code-group ```ruby [spec/rails_helper.rb] require "servactory/test_kit/rspec/helpers" require "servactory/test_kit/rspec/matchers" ``` ::: ::: code-group ```ruby [spec/rails_helper.rb] RSpec.configure do |config| config.include Servactory::TestKit::Rspec::Helpers config.include Servactory::TestKit::Rspec::Matchers # ... end ``` ::: ## Helpers ### Helper `allow_service` Mocks a `.call` invocation with a specified result. Returns a builder object that supports method chaining. ```ruby before do allow_service(PaymentService) .succeeds(transaction_id: "txn_123", status: :completed) end ``` ### Helper `allow_service!` Mocks a `.call!` invocation with a specified result. When failure is configured, raises an exception instead of returning a Result with error. ```ruby before do allow_service!(PaymentService) .succeeds(transaction_id: "txn_123", status: :completed) end ``` ### Chainable Methods #### `succeeds` Configures the mock to return a successful result with specified outputs. ```ruby allow_service(PaymentService) .succeeds(transaction_id: "txn_123", status: :completed) ``` #### `fails` Configures the mock to return a failure result. ```ruby allow_service(PaymentService) .fails(type: :payment_declined, message: "Card declined") ``` With meta information: ```ruby allow_service(PaymentService) .fails(type: :validation, message: "Invalid amount", meta: { field: :amount }) ``` With custom exception class: ```ruby allow_service(PaymentService) .fails( CustomException, type: :payment_declined, message: "Card declined" ) ``` #### `with` Specifies the expected inputs for the mock to match. ```ruby allow_service(PaymentService) .with(amount: 100, currency: "USD") .succeeds(transaction_id: "txn_100") ``` The `with` method supports argument matchers (see [Argument Matchers](#argument-matchers)). #### `then_succeeds` Configures sequential return values for multiple calls. ```ruby allow_service(RetryService) .succeeds(status: :pending) .then_succeeds(status: :completed) ``` #### `then_fails` Configures sequential return with failure on subsequent call. ```ruby allow_service(RetryService) .succeeds(status: :pending) .then_fails(type: :timeout, message: "Request timed out") ``` ### Argument Matchers #### `including` Matches inputs containing at least the specified key-value pairs. ```ruby allow_service(OrderService) .with(including(quantity: 5)) .succeeds(total: 500) ``` ```ruby allow_service(OrderService) .with(including(product_id: "PROD-001", quantity: 5)) .succeeds(total: 1000) ``` #### `excluding` Matches inputs that do not contain the specified keys. ```ruby allow_service(OrderService) .with(excluding(secret_key: anything)) .succeeds(total: 750) ``` #### `any_inputs` Matches any arguments passed to the service. ```ruby allow_service(NotificationService) .with(any_inputs) .succeeds(sent: true) ``` #### `no_inputs` Matches when no arguments are passed. ```ruby allow_service(HealthCheckService) .with(no_inputs) .succeeds(healthy: true) ``` ### Automatic Validation The helpers automatically validate inputs and outputs against the service definition. #### Input Validation When using `with`, the helper validates that specified inputs exist in the service: ```ruby # Raises ValidationError: unknown_input is not defined in ServiceClass allow_service!(ServiceClass) .with(unknown_input: "value") .succeeds(result: "ok") ``` #### Output Validation The helper validates that specified outputs exist and match expected types: ```ruby # Raises ValidationError: unknown_output is not defined in ServiceClass allow_service!(ServiceClass) .succeeds(unknown_output: "value") ``` ```ruby # Raises ValidationError: order_number expects Integer, got String allow_service!(ServiceClass) .succeeds(order_number: "not_an_integer") ``` ## Example ::: code-group ```ruby [RSpec] RSpec.describe Users::Create, type: :service do describe ".call!" do subject(:perform) { described_class.call!(**attributes) } let(:attributes) do { email:, first_name:, last_name: } end let(:email) { "john@example.com" } let(:first_name) { "John" } let(:last_name) { "Kennedy" } describe "validations" do describe "inputs" do it do expect { perform }.to( have_input(:email) .type(String) .required ) end it do expect { perform }.to( have_input(:first_name) .type(String) .required ) end it do expect { perform }.to( have_input(:last_name) .type(String) .optional ) end end describe "internals" do it do expect { perform }.to( have_internal(:email_verification) .type(Servactory::Result) ) end end describe "outputs" do it do expect(perform).to( have_output(:user) .instance_of(User) ) end end end describe "and the data required for work is also valid" do before do allow_service!(EmailVerificationService) .with(email: "john@example.com") .succeeds(valid: true, normalized: "john@example.com") end it do expect(perform).to( be_success_service .with_output(:user, be_a(User)) ) end end describe "but the data required for work is invalid" do describe "because email verification fails" do before do allow_service!(EmailVerificationService) .fails(type: :invalid_email, message: "Email is not valid") end it "returns expected error", :aggregate_failures do expect { perform }.to( raise_error do |exception| expect(exception).to be_a(ApplicationService::Exceptions::Failure) expect(exception.type).to eq(:invalid_email) expect(exception.message).to eq("Email is not valid") expect(exception.meta).to be_nil end ) end end end end end ``` ```ruby [Service] class Users::Create < ApplicationService::Base input :email, type: String input :first_name, type: String input :last_name, type: String, required: false internal :email_verification, type: Servactory::Result output :user, type: User make :verify_email make :create_user private def verify_email internals.email_verification = EmailVerificationService.call!( email: inputs.email ) end def create_user outputs.user = User.create!( email: internals.email_verification.normalized, first_name: inputs.first_name, last_name: inputs.last_name ) end end ``` ::: ## Matchers ### Matcher `have_input` #### `type` Checks the input type. Intended for one meaning. ```ruby it do expect { perform }.to( have_input(:id) .type(Integer) ) end ``` #### `types` Checks input types. Intended for multiple values. ```ruby it do expect { perform }.to( have_input(:id) .types(Integer, String) ) end ``` #### `required` Checks whether the input is required. ```ruby it do expect { perform }.to( have_input(:id) .type(Integer) .required ) end ``` #### `optional` Checks whether the input is optional. ```ruby it do expect { perform }.to( have_input(:middle_name) .type(String) .optional ) end ``` #### `default` Checks the default value of the input. ```ruby it do expect { perform }.to( have_input(:middle_name) .type(String) .optional .default("") ) end ``` #### `consists_of` Checks the nested types of the input collection. You can specify multiple values. ::: code-group ```ruby [Without message] it do expect { perform }.to( have_input(:ids) .type(Array) .required .consists_of(String) ) end ``` ```ruby [With message] it do expect { perform }.to( have_input(:ids) .type(Array) .required .consists_of(String) .message("Input `ids` must be a collection of `String`") # [!code focus] ) end ``` ::: #### `inclusion` Checks the values of the `inclusion` option of the input. ::: code-group ```ruby [Without message] it do expect { perform }.to( have_input(:event_name) .type(String) .required .inclusion(%w[created rejected approved]) ) end ``` ```ruby [With message] it do expect { perform }.to( have_input(:event_name) .type(String) .required .inclusion(%w[created rejected approved]) .message(be_a(Proc)) # [!code focus] ) end ``` ::: #### `target` Checks the values of the `target` option of the input. ::: code-group ```ruby [Without message] it do expect { perform }.to( have_input(:service_class) .type(Class) .target([MyFirstService, MySecondService]) ) end ``` ```ruby [With message] it do expect { perform }.to( have_input(:service_class) .type(Class) .target([MyFirstService, MySecondService]) .message("Must be a valid service class") # [!code focus] ) end ``` ::: #### `schema` Checks the values of the `schema` option of the input. ::: code-group ```ruby [Without message] it do expect { perform }.to( have_input(:payload) .type(Hash) .required .schema( { request_id: { type: String, required: true }, user: { # ... } } ) ) end ``` ```ruby [With message] it do expect { perform }.to( have_input(:payload) .type(Hash) .required .schema( { request_id: { type: String, required: true }, user: { # ... } } ) .message("Problem with the value in the schema") # [!code focus] ) end ``` ::: #### `message` Checks `message` from the last chain. Currently only works with `consists_of`, `inclusion` and `schema` chains. ```ruby it do expect { perform }.to( have_input(:ids) .type(Array) .required .consists_of(String) # [!code focus] .message("Input `ids` must be a collection of `String`") # [!code focus] ) end ``` #### `must` Checks for the presence of the expected key in the `must` input. You can specify multiple values. ```ruby it do expect { perform }.to( have_input(:invoice_numbers) .type(Array) .consists_of(String) .required .must(:be_6_characters) ) end ``` ### Matcher `have_internal` #### `type` Checks the type of an internal attribute. Intended for one meaning. ```ruby it do expect { perform }.to( have_internal(:id) .type(Integer) ) end ``` #### `types` Checks the types of an internal attribute. Intended for multiple values. ```ruby it do expect { perform }.to( have_internal(:id) .types(Integer, String) ) end ``` #### `consists_of` Checks the nested types of an internal attribute collection. You can specify multiple values. ::: code-group ```ruby [Without message] it do expect { perform }.to( have_internal(:ids) .type(Array) .consists_of(String) ) end ``` ```ruby [With message] it do expect { perform }.to( have_internal(:ids) .type(Array) .consists_of(String) .message("Internal `ids` must be a collection of `String`") # [!code focus] ) end ``` ::: #### `inclusion` Checks the values of the `inclusion` option of an internal attribute. ::: code-group ```ruby [Without message] it do expect { perform }.to( have_internal(:event_name) .type(String) .inclusion(%w[created rejected approved]) ) end ``` ```ruby [With message] it do expect { perform }.to( have_internal(:event_name) .type(String) .inclusion(%w[created rejected approved]) .message(be_a(Proc)) # [!code focus] ) end ``` ::: #### `target` Checks the values of the `target` option of an internal attribute. ::: code-group ```ruby [Without message] it do expect { perform }.to( have_internal(:service_class) .type(Class) .target([MyFirstService, MySecondService]) ) end ``` ```ruby [With message] it do expect { perform }.to( have_internal(:service_class) .type(Class) .target([MyFirstService, MySecondService]) .message("Must be a valid service class") # [!code focus] ) end ``` ::: #### `schema` Checks the values of the `schema` option of an internal attribute. ::: code-group ```ruby [Without message] it do expect { perform }.to( have_internal(:payload) .type(Hash) .schema( { request_id: { type: String, required: true }, user: { # ... } } ) ) end ``` ```ruby [With message] it do expect { perform }.to( have_internal(:payload) .type(Hash) .schema( { request_id: { type: String, required: true }, user: { # ... } } ) .message("Problem with the value in the schema") # [!code focus] ) end ``` ::: #### `message` Checks `message` from the last chain. Currently only works with `consists_of`, `inclusion` and `schema` chains. ```ruby it do expect { perform }.to( have_internal(:ids) .type(Array) .consists_of(String) # [!code focus] .message("Internal `ids` must be a collection of `String`") # [!code focus] ) end ``` #### `must` Checks for the presence of the expected key in the `must` internal attribute. You can specify multiple values. ```ruby it do expect { perform }.to( have_internal(:invoice_numbers) .type(Array) .consists_of(String) .must(:be_6_characters) ) end ``` ### Matcher `have_output` #### `instance_of` Checks the type of the output attribute. ```ruby it do expect(perform).to( have_output(:event) .instance_of(Event) ) end ``` #### `contains` :::info In release `2.9.0` the `with` chain was renamed to `contains`. ::: Checks the value of the output attribute. ```ruby it do expect(perform).to( have_output(:full_name) .contains("John Fitzgerald Kennedy") ) end ``` #### `nested` Points to the nested value of the output attribute. ```ruby it do expect(perform).to( have_output(:event) .nested(:id) .contains("14fe213e-1b0a-4a68-bca9-ce082db0f2c6") ) end ``` ### Matcher `be_success_service` ::: code-group ```ruby [minimal] it { expect(perform).to be_success_service } ``` ::: #### `with_output` ```ruby it do expect(perform).to( be_success_service .with_output(:id, "...") ) end ``` #### `with_outputs` ```ruby it do expect(perform).to( be_success_service .with_outputs( id: "...", full_name: "...", # ... ) ) end ``` ### Matcher `be_failure_service` ::: code-group ```ruby [minimal] it { expect(perform).to be_failure_service } ``` ```ruby [full] it "returns expected failure" do expect(perform).to( be_failure_service .with(ApplicationService::Exceptions::Failure) .type(:base) .message("Some error") .meta(nil) ) end ``` ::: #### `with` ```ruby it "returns expected failure" do expect(perform).to( be_failure_service .with(ApplicationService::Exceptions::Failure) ) end ``` #### `type` ```ruby it "returns expected failure" do expect(perform).to( be_failure_service .type(:base) ) end ``` #### `message` ```ruby it "returns expected failure" do expect(perform).to( be_failure_service .message("Some error") ) end ``` #### `meta` ```ruby it "returns expected failure" do expect(perform).to( be_failure_service .meta(nil) ) end ``` --- --- url: 'https://servactory.com/guide/testing/rspec/legacy.md' description: Description and examples of service testing using RSpec --- # RSpec (Legacy) :::warning This functionality is deprecated and will be maintained for backward compatibility only. For new tests, we recommend using the [new testing API](./fluent). See the [migration guide](./migration) for step-by-step instructions. ::: ## Installation ::: code-group ```ruby [spec/rails_helper.rb] require "servactory/test_kit/rspec/helpers" require "servactory/test_kit/rspec/matchers" ``` ::: ::: code-group ```ruby [spec/rails_helper.rb] RSpec.configure do |config| config.include Servactory::TestKit::Rspec::Helpers config.include Servactory::TestKit::Rspec::Matchers # ... end ``` ::: ## Example ### Structure * `.call!` or `call`: * `subject`; * `validations`: * `inputs`; * `internals`; * `outputs`; * `when required data for work is valid`: * `be_success_service`; * `have_output`. * `when required data for work is invalid`: * `be_failure_service`. ### File ::: code-group ```ruby [RSpec] RSpec.describe Users::Create, type: :service do describe ".call!" do subject(:perform) { described_class.call!(**attributes) } let(:attributes) do { first_name:, middle_name:, last_name: } end let(:first_name) { "John" } let(:middle_name) { "Fitzgerald" } let(:last_name) { "Kennedy" } describe "validations" do describe "inputs" do it do expect { perform }.to( have_input(:first_name) .valid_with(attributes) .type(String) .required ) end it do expect { perform }.to( have_input(:middle_name) .valid_with(attributes) .type(String) .optional ) end it do expect { perform }.to( have_input(:last_name) .valid_with(attributes) .type(String) .required ) end end describe "outputs" do it do expect(perform).to( have_output(:full_name) .instance_of(String) ) end end end context "when required data for work is valid" do it { expect(perform).to be_success_service } it do expect(perform).to( have_output(:full_name) .contains("John Fitzgerald Kennedy") ) end describe "even if `middle_name` is not specified" do let(:middle_name) { nil } it do expect(perform).to( have_output(:full_name) .contains("John Kennedy") ) end end end end end ``` ```ruby [Service] class Users::Create < ApplicationService::Base input :first_name, type: String input :middle_name, type: String, required: false input :last_name, type: String output :full_name, type: String make :assign_full_name private def assign_full_name outputs.full_name = [ inputs.first_name, inputs.middle_name, inputs.last_name ].compact.join(" ") end end ``` ::: ## Helpers ### Helper `allow_service_as_success!` Mocks a `.call!` invocation with a successful result. ```ruby before do allow_service_as_success!(Users::Accept) end ``` ```ruby before do allow_service_as_success!(Users::Accept) do { user: user } end end ``` ### Helper `allow_service_as_success` Mocks a `.call` invocation with a successful result. ```ruby before do allow_service_as_success(Users::Accept) end ``` ```ruby before do allow_service_as_success(Users::Accept) do { user: user } end end ``` ### Helper `allow_service_as_failure!` Mocks a `.call!` invocation with a failed result. ```ruby before do allow_service_as_failure!(Users::Accept) do ApplicationService::Exceptions::Failure.new( message: "Some error" ) end end ``` ### Helper `allow_service_as_failure` Mocks a `.call` invocation with a failed result. ```ruby before do allow_service_as_failure(Users::Accept) do ApplicationService::Exceptions::Failure.new( message: "Some error" ) end end ``` ### Options #### Option `with` The methods `allow_service_as_success!`, `allow_service_as_success`, `allow_service_as_failure!`, and `allow_service_as_failure` support the `with` option. By default, this option does not require passing service arguments and will automatically determine this data based on the `info` method. ```ruby before do allow_service_as_success!( Users::Accept, with: { user: user } # [!code focus] ) end ``` ```ruby before do allow_service_as_success!( Users::Accept, with: { user: user } # [!code focus] ) do { user: user } end end ``` ## Matchers ### Matcher `have_input` #### `type` Checks the input type. Intended for one meaning. ```ruby it do expect { perform }.to( have_input(:id) .type(Integer) ) end ``` #### `types` Checks input types. Intended for multiple values. ```ruby it do expect { perform }.to( have_input(:ids) .types(Integer, String) ) end ``` #### `required` Checks whether the input is required. ```ruby it do expect { perform }.to( have_input(:id) .type(Integer) .required ) end ``` #### `optional` Checks whether the input is optional. ```ruby it do expect { perform }.to( have_input(:middle_name) .type(String) .optional ) end ``` #### `default` Checks the default value of the input. ```ruby it do expect { perform }.to( have_input(:middle_name) .type(String) .optional .default("") ) end ``` #### `consists_of` Checks the nested types of the input collection. You can specify multiple values. ::: code-group ```ruby [Without message] it do expect { perform }.to( have_input(:ids) .type(Array) .required .consists_of(String) ) end ``` ```ruby [With message] it do expect { perform }.to( have_input(:ids) .type(Array) .required .consists_of(String) .message("Input `ids` must be a collection of `String`") # [!code focus] ) end ``` ::: #### `inclusion` Checks the values of the `inclusion` option of the input. ::: code-group ```ruby [Without message] it do expect { perform }.to( have_input(:event_name) .type(String) .required .inclusion(%w[created rejected approved]) ) end ``` ```ruby [With message] it do expect { perform }.to( have_input(:event_name) .type(String) .required .inclusion(%w[created rejected approved]) .message(be_a(Proc)) # [!code focus] ) end ``` ::: #### `schema` Checks the values of the `schema` option of the input. ::: code-group ```ruby [Without message] it do expect { perform }.to( have_input(:payload) .type(Hash) .required .schema( { request_id: { type: String, required: true }, user: { # ... } } ) ) end ``` ```ruby [With message] it do expect { perform }.to( have_input(:payload) .type(Hash) .required .schema( { request_id: { type: String, required: true }, user: { # ... } } ) .message("Problem with the value in the schema") # [!code focus] ) end ``` ::: #### `message` Checks `message` from the last chain. Currently only works with `consists_of`, `inclusion` and `schema` chains. ```ruby it do expect { perform }.to( have_input(:ids) .type(Array) .required .consists_of(String) # [!code focus] .message("Input `ids` must be a collection of `String`") # [!code focus] ) end ``` #### `must` Checks for the presence of the expected key in the `must` input. You can specify multiple values. ```ruby it do expect { perform }.to( have_input(:invoice_numbers) .type(Array) .consists_of(String) .required .must(:be_6_characters) ) end ``` #### `valid_with` This chain will try to check the actual behavior of the input based on the data passed. ```ruby subject(:perform) { described_class.call!(**attributes) } let(:attributes) do { first_name: first_name, middle_name: middle_name, last_name: last_name } end it do expect { perform }.to( have_input(:first_name) .valid_with(attributes) .type(String) .required ) end ``` ### Matcher `have_internal` #### `type` Checks the type of an internal attribute. Intended for one meaning. ```ruby it do expect { perform }.to( have_internal(:id) .type(Integer) ) end ``` #### `types` Checks the types of an internal attribute. Intended for multiple values. ```ruby it do expect { perform }.to( have_internal(:ids) .types(Integer, String) ) end ``` #### `consists_of` Checks the nested types of an internal attribute collection. You can specify multiple values. ::: code-group ```ruby [Without message] it do expect { perform }.to( have_internal(:ids) .type(Array) .consists_of(String) ) end ``` ```ruby [With message] it do expect { perform }.to( have_internal(:ids) .type(Array) .consists_of(String) .message("Input `ids` must be a collection of `String`") # [!code focus] ) end ``` ::: #### `inclusion` Checks the values of the `inclusion` option of an internal attribute. ::: code-group ```ruby [Without message] it do expect { perform }.to( have_internal(:event_name) .type(String) .inclusion(%w[created rejected approved]) ) end ``` ```ruby [With message] it do expect { perform }.to( have_internal(:event_name) .type(String) .inclusion(%w[created rejected approved]) .message(be_a(Proc)) # [!code focus] ) end ``` ::: #### `schema` Checks the values of the `schema` option of an internal attribute. ::: code-group ```ruby [Without message] it do expect { perform }.to( have_internal(:payload) .type(Hash) .schema( { request_id: { type: String, required: true }, user: { # ... } } ) ) end ``` ```ruby [With message] it do expect { perform }.to( have_internal(:payload) .type(Hash) .schema( { request_id: { type: String, required: true }, user: { # ... } } ) .message("Problem with the value in the schema") # [!code focus] ) end ``` ::: #### `message` Checks `message` from the last chain. Currently only works with `consists_of`, `inclusion` and `schema` chains. ```ruby it do expect { perform }.to( have_internal(:ids) .type(Array) .consists_of(String) # [!code focus] .message("Input `ids` must be a collection of `String`") # [!code focus] ) end ``` #### `must` Checks for the presence of the expected key in the `must` internal attribute. You can specify multiple values. ```ruby it do expect { perform }.to( have_internal(:invoice_numbers) .type(Array) .consists_of(String) .must(:be_6_characters) ) end ``` ### Matcher `have_output` #### `instance_of` Checks the type of the output attribute. ```ruby it do expect(perform).to( have_output(:event) .instance_of(Event) ) end ``` #### `contains` :::info In release `2.9.0` the `with` chain was renamed to `contains`. ::: Checks the value of the output attribute. ```ruby it do expect(perform).to( have_output(:full_name) .contains("John Fitzgerald Kennedy") ) end ``` #### `nested` Points to the nested value of the output attribute. ```ruby it do expect(perform).to( have_output(:event) .nested(:id) .contains("14fe213e-1b0a-4a68-bca9-ce082db0f2c6") ) end ``` ### Matcher `be_success_service` ::: code-group ```ruby [minimal] it { expect(perform).to be_success_service } ``` ::: #### `with_output` ```ruby it do expect(perform).to( be_success_service .with_output(:id, "...") ) end ``` #### `with_outputs` ```ruby it do expect(perform).to( be_success_service .with_outputs( id: "...", full_name: "...", # ... ) ) end ``` ### Matcher `be_failure_service` ::: code-group ```ruby [minimal] it { expect(perform).to be_failure_service } ``` ```ruby [full] it "returns expected failure" do expect(perform).to( be_failure_service .with(ApplicationService::Exceptions::Failure) .type(:base) .message("Some error") .meta(nil) ) end ``` ::: #### `with` ```ruby it "returns expected failure" do expect(perform).to( be_failure_service .with(ApplicationService::Exceptions::Failure) ) end ``` #### `type` ```ruby it "returns expected failure" do expect(perform).to( be_failure_service .type(:base) ) end ``` #### `message` ```ruby it "returns expected failure" do expect(perform).to( be_failure_service .message("Some error") ) end ``` #### `meta` ```ruby it "returns expected failure" do expect(perform).to( be_failure_service .meta(nil) ) end ``` --- --- url: 'https://servactory.com/datory/guide/usage/serialization.md' description: Description and examples of use --- # Serialization ## Example ::: code-group ```ruby [Code] class SerialDto < Datory::Base uuid! :id string! :status string! :title one! :poster, include: ImageDto one! :ratings, include: RatingsDto many! :countries, include: CountryDto many! :genres, include: GenreDto many! :seasons, include: SeasonDto date! :premieredOn, to: :premiered_on end ``` ```text [Table] ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | SerialDto | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | Attribute | From | To | As | Include | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | id | String | id | String | | | status | String | status | String | | | title | String | title | String | | | poster | [ImageDto, Hash] | poster | [ImageDto, Hash] | ImageDto | | | ratings | [RatingsDto, Hash] | ratings | [RatingsDto, Hash] | RatingsDto | | | countries | Array | countries | Array | CountryDto | | genres | Array | genres | Array | GenreDto | | seasons | Array | seasons | Array | SeasonDto | | premieredOn | String | premiered_on | Date | | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ``` ```text [Info] # {:from=>{:name=>:id, :type=>String, :min=>nil, :max=>nil, :consists_of=>false, :format=>:uuid}, :to=>{:name=>:id, :type=>String, :min=>nil, :max=>nil, :consists_of=>false, :format=>:uuid, :required=>true, :default=>nil, :include=>nil}}, :status=> {:from=>{:name=>:status, :type=>String, :min=>nil, :max=>nil, :consists_of=>false, :format=>nil}, :to=>{:name=>:status, :type=>String, :min=>nil, :max=>nil, :consists_of=>false, :format=>nil, :required=>true, :default=>nil, :include=>nil}}, :title=> {:from=>{:name=>:title, :type=>String, :min=>nil, :max=>nil, :consists_of=>false, :format=>nil}, :to=>{:name=>:title, :type=>String, :min=>nil, :max=>nil, :consists_of=>false, :format=>nil, :required=>true, :default=>nil, :include=>nil}}, :poster=> {:from=>{:name=>:poster, :type=>[ImageDto, Hash], :min=>nil, :max=>nil, :consists_of=>false, :format=>nil}, :to=>{:name=>:poster, :type=>[ImageDto, Hash], :min=>nil, :max=>nil, :consists_of=>false, :format=>nil, :required=>true, :default=>nil, :include=>ImageDto}}, :ratings=> {:from=>{:name=>:ratings, :type=>[RatingsDto, Hash], :min=>nil, :max=>nil, :consists_of=>false, :format=>nil}, :to=>{:name=>:ratings, :type=>[RatingsDto, Hash], :min=>nil, :max=>nil, :consists_of=>false, :format=>nil, :required=>true, :default=>nil, :include=>RatingsDto}}, :countries=> {:from=>{:name=>:countries, :type=>Array, :min=>nil, :max=>nil, :consists_of=>[CountryDto, Hash], :format=>nil}, :to=>{:name=>:countries, :type=>Array, :min=>nil, :max=>nil, :consists_of=>[CountryDto, Hash], :format=>nil, :required=>true, :default=>nil, :include=>CountryDto}}, :genres=> {:from=>{:name=>:genres, :type=>Array, :min=>nil, :max=>nil, :consists_of=>[GenreDto, Hash], :format=>nil}, :to=>{:name=>:genres, :type=>Array, :min=>nil, :max=>nil, :consists_of=>[GenreDto, Hash], :format=>nil, :required=>true, :default=>nil, :include=>GenreDto}}, :seasons=> {:from=>{:name=>:seasons, :type=>Array, :min=>nil, :max=>nil, :consists_of=>[SeasonDto, Hash], :format=>nil}, :to=>{:name=>:seasons, :type=>Array, :min=>nil, :max=>nil, :consists_of=>[SeasonDto, Hash], :format=>nil, :required=>true, :default=>nil, :include=>SeasonDto}}, :premieredOn=> {:from=>{:name=>:premieredOn, :type=>String, :min=>nil, :max=>nil, :consists_of=>false, :format=>:date}, :to=>{:name=>:premiered_on, :type=>Date, :min=>nil, :max=>nil, :consists_of=>false, :format=>nil, :required=>true, :default=>nil, :include=>nil}}}> ``` ::: ```ruby class SeasonDto < Datory::Base uuid! :id uuid! :serialId, to: :serial_id integer! :number many! :episodes, include: EpisodeDto date! :premieredOn, to: :premiered_on date? :endedOn, to: :ended_on end ``` ## Usage ### Data preparation The following data type can be used for serialization. #### Method `.new` ```ruby serial = SerialDto.new(attributes) ``` #### ActiveRecord ```ruby serial = Serial.find(id) ``` ### Validation When serializing, if problems occur, an exception will be thrown. If you need to handle this behavior, you can use the `form` method. The prepared data must be passed to the `form` method: ```ruby form = SerialDto.form(serial) ``` You can validate the data using one of the following methods: ```ruby form.valid? # => true form.invalid? # => false ``` You can obtain information about an object and model using these methods: ```ruby form.target # => SerialDto form.model # => { ... } ``` If you need to update a value in the model, then these methods are intended for this: ```ruby form.update(title: "New title") form.update_by(0, title: "New title") # For collection ``` And finally, serialization is possible through calling the `serialize` method: ```ruby form.serialize # => { ... } ``` ### Call The example above demonstrates serialization from `form`. But this is also possible directly from the DTO class. In this case, if problems arise, the exception `Datory::Exceptions::SerializationError` will be thrown. ```ruby SerialDto.serialize(serial) # => { ... } ``` --- --- url: 'https://servactory.com/guide/usage/call.md' description: Description and examples of calling the service --- # Service call Services are called via `.call` or `.call!` methods. ## Method `.call!` The `.call!` method throws an exception if any problem occurs within the service. ::: code-group ```ruby [Call] Users::Accept.call!(user: User.first) ``` ```ruby [Success] # => # ``` ```ruby [Failure] # => ApplicationService::Exceptions::Input: [Users::Accept] Required input `user` is missing # => ApplicationService::Exceptions::Failure: There is some problem with the user ``` ::: ## Method `.call` The `.call` method throws an exception for input, internal, and output attribute problems. Other errors are captured and provided via the `Result` class. ::: code-group ```ruby [Call] Users::Accept.call(user: User.first) ``` ```ruby [Success] # => # ``` ```ruby [Failure] # => ApplicationService::Exceptions::Input: [Users::Accept] Required input `user` is missing # => # ``` ::: --- --- url: 'https://servactory.com/guide/exceptions/failure.md' description: Description and examples of use of service failures --- # Failure and error handling ## Description of methods and exceptions Terminate the service prematurely by calling one of these methods: * `fail_input!`; * `fail_internal!`; * `fail_output!`; * `fail!`; * `fail_result!`. These methods throw an exception. From the list above, only the following methods can be processed after being called via `call`: * `fail!`; * `fail_result!`. The remaining methods always throw an exception. Automatic checks for input, internal, and output attributes also exist. Validation problems with these attributes raise the corresponding exception. This behavior is identical to calling these methods: * `fail_input!`; * `fail_internal!`; * `fail_output!`. Service logic may throw its own exceptions (e.g., `ActiveRecord::RecordInvalid`). Handle such cases with the class-level `fail_on!` method. ## Methods ### Method `fail_input!` Throws an exception on behalf of the input attribute. The `fail_input!` method accepts error text, additional information via `meta`, and requires the input attribute name. Any service call throws an `ApplicationService::Exceptions::Input` exception. ```ruby{6} make :check! def check! return if inputs.invoice_number.start_with?("AA") fail_input!( :invoice_number, message: "Invalid invoice number", meta: { received_invoice_number: inputs.invoice_number } ) end ``` Example of information that the exception `ApplicationService::Exceptions::Input` might provide: ```ruby exception.service # => exception.detailed_message # => Invalid invoice number (ApplicationService::Exceptions::Input) exception.message # => Invalid invoice number exception.input_name # => :invoice_number exception.meta # => {:received_invoice_number=>"BB-7650AE"} ``` ### Method `fail_internal!` Throws an exception on behalf of the internal attribute. The `fail_internal!` method accepts error text, additional information via `meta`, and requires the internal attribute name. Any service call throws an `ApplicationService::Exceptions::Internal` exception. ```ruby{6} make :check! def check! return if internals.invoice_number.start_with?("AA") fail_internal!( :invoice_number, message: "Invalid invoice number", meta: { received_invoice_number: internals.invoice_number } ) end ``` Example of information that the exception `ApplicationService::Exceptions::Internal` might provide: ```ruby exception.service # => exception.detailed_message # => Invalid invoice number (ApplicationService::Exceptions::Internal) exception.message # => Invalid invoice number exception.internal_name # => :invoice_number exception.meta # => {:received_invoice_number=>"BB-7650AE"} ``` ### Method `fail_output!` Throws an exception on behalf of the output attribute. The `fail_output!` method accepts error text, additional information via `meta`, and requires the output attribute name. Any service call throws an `ApplicationService::Exceptions::Output` exception. ```ruby{6} make :check! def check! return if outputs.invoice_number.start_with?("AA") fail_output!( :invoice_number, message: "Invalid invoice number", meta: { received_invoice_number: outputs.invoice_number } ) end ``` Example of information that the exception `ApplicationService::Exceptions::Output` might provide: ```ruby exception.service # => exception.detailed_message # => Invalid invoice number (ApplicationService::Exceptions::Output) exception.message # => Invalid invoice number exception.output_name # => :invoice_number exception.meta # => {:received_invoice_number=>"BB-7650AE"} ``` ### Method `fail!` Describes custom errors. The `fail!` method accepts error text, additional information via `meta`, and optional `type`. By default, `type` is `base`. Pass any value for custom processing. Calling via `.call!` throws a `Servactory::Exceptions::Failure` exception. Calling via `.call` logs the error and makes it available in `Result`. #### Examples Minimal example with default type: ```ruby{6} make :check! def check! return if inputs.invoice_number.start_with?("AA") fail!(message: "Invalid invoice number") end ``` Extended example with default type and metadata: ```ruby{2,4-6} fail!( :base, message: "Invalid invoice number", meta: { invoice_number: inputs.invoice_number } ) ``` Example with custom `validation` type and metadata: ```ruby{7,9-12} make :check! def check! return if inputs.email.include?("@") fail!( :validation, message: "Email must contain @ symbol", meta: { field: :email, provided_value: inputs.email } ) end ``` Example of information that will be provided: ```ruby exception.detailed_message # => Invalid invoice number (ApplicationService::Exceptions::Failure) exception.message # => Invalid invoice number exception.type # => :base exception.meta # => {:invoice_number=>"BB-7650AE"} ``` For the example with `validation` type: ```ruby exception.detailed_message # => Email must contain @ symbol (ApplicationService::Exceptions::Failure) exception.message # => Email must contain @ symbol exception.type # => :validation exception.meta # => {:field=>:email, :provided_value=>"user.example.com"} ``` ### Method `fail_result!` Requires `Result` and internally calls the `fail!` method. Designed for shorthand writing of code for passing an error from one service to the current one. For example, from an API service to an application service. ```ruby fail_result!(service_result) ``` The code above is equivalent to this: ```ruby fail!( service_result.error.type, message: service_result.error.message, meta: service_result.error.meta ) ``` ### Method `fail_on!` Catches specified exceptions. The `fail_on!` method accepts exception class(es) and optionally customizes the message text. Instead of the specified exceptions, `fail!` is called. Original exception information passes to `fail!` via `meta`. #### Usage ```ruby module ApplicationService class Base < Servactory::Base fail_on! ActiveRecord::RecordNotFound, ActiveRecord::RecordInvalid # ... end end ``` Customize the message text as follows: ```ruby fail_on! ActiveRecord::RecordNotFound, with: ->(exception:) { exception.message } ``` Alternative option: ```ruby fail_on!(ActiveRecord::RecordNotFound) { |exception:| exception.message } ``` --- --- url: 'https://servactory.com/guide/usage/info.md' description: Description and examples of use of obtaining information about the service --- # Service information Services expose information about input, internal, and output attributes externally. Useful for complex service processing or testing. Example service with attributes: ```ruby class BuildFullName < ApplicationService::Base input :first_name, type: String input :middle_name, type: String, required: false input :last_name, type: String internal :prepared_full_name, type: String output :full_name, type: String # ... end ``` Access attribute information: ```ruby BuildFullName.info # => # ``` ```ruby BuildFullName.info.inputs # => [:first_name, :middle_name, :last_name] ``` ```ruby BuildFullName.info.internals # => [:prepared_full_name] ``` ```ruby BuildFullName.info.outputs # => [:full_name] ``` --- --- url: 'https://servactory.com/guide/attributes/input.md' description: Description and examples of using input attributes of service --- # Input attributes Add all expected attributes via the `input` method. Unexpected arguments (not defined as input attributes) cause an error. ## Usage Access input attributes via the `inputs` method. ```ruby{2-4,15-17} class Users::Create < ApplicationService::Base input :first_name, type: String input :middle_name, type: String input :last_name, type: String internal :full_name, type: String output :user, type: User make :assign_full_name make :create! def assign_full_name internals.full_name = [ inputs.first_name, inputs.middle_name, inputs.last_name ].join(" ") end def create! outputs.user = User.create!(full_name: internals.full_name) end end ``` ## Options See [using options](../options/usage) for details. ## Helpers Servactory provides built-in helpers and supports custom helpers. Helpers are shorthand that expand into specific options. ### Helper `optional` Equivalent to `required: false`. ```ruby{6} class Users::Create < ApplicationService::Base input :first_name, type: String input :middle_name, :optional, type: String input :last_name, type: String # ... end ``` ### Custom Add custom helpers via `input_option_helpers` in `configuration`. Helpers can be based on existing options. [Configuration example](../configuration#helpers-for-input) #### Example with `must` ```ruby{3} class Payments::Create < ApplicationService::Base input :invoice_numbers, :must_be_6_characters, type: Array, consists_of: String # ... end ``` #### Example with `prepare` ```ruby{3} class Payments::Create < ApplicationService::Base input :amount_cents, :to_money, as: :amount, type: Integer # ... end ``` ## Methods ### Method `only` Filter `inputs` with the `only` method. Returns a Hash with specified attributes. ```ruby{2} outputs.full_name = inputs.only(:first_name, :middle_name, :last_name) .values .compact .join(" ") ``` ### Method `except` Filter `inputs` with the `except` method. Returns a Hash without specified attributes. ```ruby{2} outputs.full_name = inputs.except(:gender) .values .compact .join(" ") ``` ### Predicate methods Access any input attribute as a predicate method. ```ruby{6} input :first_name, type: String # ... def something return unless inputs.user? # instead of `inputs.user.present?` # ... end ``` --- --- url: 'https://servactory.com/guide/attributes/internal.md' description: Description and examples of using internal attributes of service --- # Internal attributes Add internal private attributes via the `internal` method. ## Usage Assign and access internal attributes via `internals=`/`internals` methods. ```ruby{6,14,22} class Users::Create < ApplicationService::Base input :first_name, type: String input :middle_name, type: String input :last_name, type: String internal :full_name, type: String output :user, type: User make :assign_full_name make :create! def assign_full_name internals.full_name = [ inputs.first_name, inputs.middle_name, inputs.last_name ].join(" ") end def create! outputs.user = User.create!(full_name: internals.full_name) end end ``` ## Options See [using options](../options/usage) for details. ## Helpers Servactory supports custom helpers for project purposes. Helpers are shorthand that expand into specific options. ### Custom Add custom helpers via `internal_option_helpers` in `configuration`. Helpers can be based on existing options. [Configuration example](../configuration#helpers-for-internal) #### Example with `must` ```ruby{5} class Payments::Create < ApplicationService::Base # ... internal :invoice_numbers, :must_be_6_characters, type: Array, consists_of: String # ... end ``` ## Methods ### Method `only` Filter `internals` with the `only` method. Returns a Hash with specified attributes. ```ruby{2} outputs.full_name = internals.only(:first_name, :middle_name, :last_name) .values .compact .join(" ") ``` ### Method `except` Filter `internals` with the `except` method. Returns a Hash without specified attributes. ```ruby{2} outputs.full_name = internals.except(:gender) .values .compact .join(" ") ``` ### Predicate methods Access any internal attribute as a predicate method. ```ruby{8} # ... internal :full_name, type: String # ... def something return unless internals.full_name? # instead of `internals.full_name.present?` # ... end ``` --- --- url: 'https://servactory.com/guide/attributes/output.md' description: Description and examples of using output attributes of service --- # Output attributes Add all return attributes via the `output` method. These are available through the `Result` class. ## Usage Assign and access output attributes via `outputs=`/`outputs` methods. ```ruby{8,22} class Users::Create < ApplicationService::Base input :first_name, type: String input :middle_name, type: String input :last_name, type: String internal :full_name, type: String output :user, type: User make :assign_full_name make :create! def assign_full_name internals.full_name = [ inputs.first_name, inputs.middle_name, inputs.last_name ].join(" ") end def create! outputs.user = User.create!(full_name: internals.full_name) end end ``` ## Options See [using options](../options/usage) for details. ## Helpers Servactory supports custom helpers for project purposes. Helpers are shorthand that expand into specific options. ### Custom Add custom helpers via `output_option_helpers` in `configuration`. Helpers can be based on existing options. [Configuration example](../configuration#helpers-for-output) #### Example with `must` ```ruby{5} class Payments::Create < ApplicationService::Base # ... output :invoice_numbers, :must_be_6_characters, type: Array, consists_of: String # ... end ``` ## Methods ### Method `only` Filter `outputs` with the `only` method. Returns a Hash with specified attributes. ```ruby{2} outputs.full_name = outputs.only(:first_name, :middle_name, :last_name) .values .compact .join(" ") ``` ### Method `except` Filter `outputs` with the `except` method. Returns a Hash without specified attributes. ```ruby{2} outputs.full_name = outputs.except(:gender) .values .compact .join(" ") ``` ### Predicate methods Access any output attribute as a predicate method. ```ruby{8} # ... output :full_name, type: String # ... def something return unless outputs.full_name? # instead of `outputs.full_name.present?` # ... end ``` --- --- url: 'https://servactory.com/guide/usage/result.md' description: Description and examples of using the service result --- # Service result Each service call returns a result (unless an exception is thrown). The result is either successful or failed. ## Successful result A successful result indicates all operations completed without problems. Example: ```ruby service_result = Users::Accept.call(user: User.first) ``` Returns: ```ruby # => ``` ## Failed result A failed result indicates an expected problem occurred internally. Expected problems don't throw exceptions—they're triggered via `fail!` methods. Failed results only occur when using the `.call` method. This enables processing responses from external APIs. Example result on failure: ```ruby # => ``` ## Result content `Result` contains data regardless of outcome. On success, all output attributes are available. The `success?` and `failure?` helper methods determine the outcome. ```ruby service_result = Users::Accept.call(user: User.first) service_result.success? # => true service_result.failure? # => false ``` On failure, `Result` also contains `error` with the full error description. ```ruby service_result = Users::Accept.call(user: User.first) service_result.success? # => false service_result.failure? # => true service_result.error # => # ``` Learn more about service failures [here](../exceptions/failure). ## Result processing Process the result after calling via `call`. Two options: `success?`/`failure?` methods or `on_success`/`on_failure` hooks. ### Methods #### Method `success?` ```ruby service_result = Notifications::Slack::Error::Send.call(...) return if service_result.success? fail!( message: "The message was not sent to Slack", meta: { reason: service_result.error.message } ) ``` #### Method `failure?` Pass a type to `failure?` to check specific failure types. See [failure types](../exceptions/failure#method-fail). Default type is `all` (matches any failure type). ```ruby service_result = Notifications::Slack::Error::Send.call(...) return unless service_result.failure? fail!( message: "The message was not sent to Slack", meta: { reason: service_result.error.message } ) ``` Check for a specific failure type: ```ruby service_result = Notifications::Slack::Error::Send.call(...) return unless service_result.failure?(:validation) fail!( message: "The message was not sent to Slack", meta: { reason: service_result.error.message } ) ``` Predicate methods provide convenient type checking: ::: warning `Result` output attributes also have predicate methods. Avoid naming conflicts. ::: ```ruby service_result = Notifications::Slack::Error::Send.call(...) return unless service_result.all? fail!( message: "The message was not sent to Slack", meta: { reason: service_result.error.message } ) ``` ### Hooks Alternative approach to result processing: ```ruby Notifications::Slack::Error::Send .call(...) .on_failure do |exception:| fail!( message: "The message was not sent to Slack", meta: { reason: exception.message } ) end ``` The `on_success` method provides `outputs` argument with all output attributes. Pass a type to `on_failure`: ```ruby Notifications::Slack::Error::Send .call(...) .on_success do |outputs:| notification.update!(original_data: outputs.response) end.on_failure(:all) do |exception:| fail!( message: "The message was not sent to Slack", meta: { reason: exception.message } ) end ``` --- --- url: 'https://servactory.com/guide/actions/usage.md' description: Description and examples of using actions (methods) in the service --- # Using actions Actions in the service are sequential calls to methods. Service methods are called using the `make` method. ## Examples ### Minimal In its minimal form, calling methods via `make` is optional. The `call` method can be used instead. ```ruby class Posts::Create < ApplicationService::Base def call # something end end ``` ### Several methods ```ruby{4-6,8,12,16} class Posts::Create < ApplicationService::Base # ... make :assign_api_model make :perform_api_request make :process_result def assign_api_model internals.api_model = APIModel.new(...) end def perform_api_request internals.response = APIClient.resource.create(internals.api_model) end def process_result ARModel.create!(internals.response) end end ``` ## Options See the [options](../actions/options) section for details. ## Group of multiple actions See the [grouping](../actions/grouping) section for details. ## Aliases for `make` Add alternatives to the `make` method via `action_aliases` configuration. ```ruby {2,5} configuration do action_aliases %i[execute] end execute :something def something # ... end ``` ## Customization for `make` Add frequently used method name prefixes via `action_shortcuts` configuration. Method names stay the same length, but `make` lines become shorter and more readable. ### Simple mode In simple mode, values are passed as an array of symbols. ```ruby configuration do action_shortcuts %i[assign perform] end ``` ```ruby class CMS::API::Posts::Create < CMS::API::Base # ... assign :model perform :request private def assign_model # Build model for API request end def perform_request # Perform API request end # ... end ``` ### Advanced mode In advanced mode, values are passed as a hash. ```ruby configuration do action_shortcuts( %i[assign], { restrict: { # replacement for make prefix: :create, # method name prefix suffix: :restriction # method name suffix } } ) end ``` ```ruby class Payments::Restrictions::Create < ApplicationService::Base input :payment, type: Payment # The exclamation mark will be moved to the end of the method name restrict :payment! private def create_payment_restriction! inputs.payment.restrictions.create!( reason: "Suspicion of fraud" ) end end ``` --- --- url: 'https://servactory.com/guide/options/usage.md' description: Description and examples of using options for all service attributes --- # Using options in attributes ## Option `type` Validation option. Checks that the passed value matches the specified type (class) using `is_a?`. Required. May contain one or more classes. ::: code-group ```ruby{2,3} [input] class Notifications::Create < ApplicationService::Base input :user, type: User input :need_to_notify, type: [TrueClass, FalseClass] # ... end ``` ```ruby{4} [internal] class Notifications::Create < ApplicationService::Base # ... internal :inviter, type: User # ... end ``` ```ruby{4} [output] class Notifications::Create < ApplicationService::Base # ... output :notification, type: Notification # ... end ``` ::: ## Option `required` Validation option. Checks that the passed value is not empty using `present?`. Defaults to `true`. ::: code-group ```ruby{7} [input] class Users::Create < ApplicationService::Base input :first_name, type: String input :middle_name, type: String, required: false input :last_name, type: String # ... end ``` ::: ## Option `default` Non-validation option. Assigns a value if none was passed to the service. ::: code-group ```ruby{7} [input] class Users::Create < ApplicationService::Base # ... input :middle_name, type: String, required: false, default: "" # ... end ``` ::: ## Option `as` Non-validation option. Specifies an alias for the attribute within the service. The original name becomes unavailable inside the service. ::: code-group ```ruby{3,10} [input] class Notifications::Create < ApplicationService::Base input :user, as: :recipient, type: User # ... def create! outputs.notification = Notification.create!(recipient: inputs.recipient) end end ``` ::: ## Option `inclusion` ::: info Since version `2.12.0` this option is [dynamic](../options/dynamic#option-inclusion). ::: Dynamic validation option. Checks that the passed value is in the specified array using `include?`. ::: code-group ```ruby{4} [input] class Events::Send < ApplicationService::Base input :event_name, type: String, inclusion: %w[created rejected approved] # ... end ``` ```ruby{6} [internal] class Events::Send < ApplicationService::Base # ... internal :event_name, type: String, inclusion: %w[created rejected approved] # ... end ``` ```ruby{6} [output] class Events::Send < ApplicationService::Base # ... output :event_name, type: String, inclusion: %w[created rejected approved] # ... end ``` ::: ## Option `consists_of` ::: info Since version `2.6.0` this option is [dynamic](../options/dynamic#option-consists-of). ::: Dynamic validation option. Checks that each value in the collection matches the specified type (class), including nested values, using `is_a?`. Works only with `Array` and `Set` types. Add custom types via [`collection_mode_class_names`](../configuration#collection-mode) configuration. Optional. Defaults to `String`. ::: code-group ```ruby [input] input :ids, type: Array, consists_of: String ``` ```ruby [internal] internal :ids, type: Array, consists_of: String ``` ```ruby [output] output :ids, type: Array, consists_of: String ``` ::: ## Option `schema` ::: info Since version `2.12.0` this option is [dynamic](../options/dynamic#option-schema). ::: Dynamic validation option. Requires a hash value describing the attribute's value structure. Works only with `Hash` type. Add custom types via [`hash_mode_class_names`](../configuration#hash-mode) configuration. Optional. If unspecified, validation is skipped. No default value. ::: code-group ```ruby [input] input :payload, type: Hash, schema: { request_id: { type: String, required: true }, user: { type: Hash, required: true, first_name: { type: String, required: true }, middle_name: { type: String, required: false, default: "" }, last_name: { type: String, required: true }, pass: { type: Hash, required: true, series: { type: String, required: true }, number: { type: String, required: true } } } } ``` ```ruby [internal] internal :payload, type: Hash, schema: { request_id: { type: String, required: true }, user: { type: Hash, required: true, first_name: { type: String, required: true }, middle_name: { type: String, required: false, default: "" }, last_name: { type: String, required: true }, pass: { type: Hash, required: true, series: { type: String, required: true }, number: { type: String, required: true } } } } ``` ```ruby [output] output :payload, type: Hash, schema: { request_id: { type: String, required: true }, user: { type: Hash, required: true, first_name: { type: String, required: true }, middle_name: { type: String, required: false, default: "" }, last_name: { type: String, required: true }, pass: { type: Hash, required: true, series: { type: String, required: true }, number: { type: String, required: true } } } } ``` ::: Describe each expected hash key in this format: ```ruby { request_id: { type: String, required: true } } ``` Allowed options: required `type`, `required` and optional `default`, `prepare`. The `default` and `prepare` options are only available within `input`. If `type` specifies `Hash`, describe nesting in the same format. ## Option `must` Validation option. Create custom validations. ::: warning Since 3.0.0 The `is` lambda must return exactly `true`, not a truthy value. Values like `1`, `"string"`, or `[]` will fail validation. ::: ::: code-group ```ruby{5-9} [input] class Payments::Create < ApplicationService::Base input :invoice_numbers, type: Array, consists_of: String, must: { be_6_characters: { is: ->(value:, input:) { value.all? { |id| id.size == 6 } } } } # ... end ``` ```ruby{7-11} [internal] class Events::Send < ApplicationService::Base # ... internal :invoice_numbers, type: Array, consists_of: String, must: { be_6_characters: { is: ->(value:, internal:) { value.all? { |id| id.size == 6 } } } } # ... end ``` ```ruby{7-11} [output] class Events::Send < ApplicationService::Base # ... output :invoice_numbers, type: Array, consists_of: String, must: { be_6_characters: { is: ->(value:, output:) { value.all? { |id| id.size == 6 } } } } # ... end ``` ::: ## Option `format` Dynamic validation option (not part of main options). See [more](./dynamic#option-format). ## Option `max` Dynamic validation option (not part of main options). See [more](./dynamic#option-max). ## Option `min` Dynamic validation option (not part of main options). See [more](./dynamic#option-min). ## Option `target` Dynamic validation option for Class-typed attributes (not part of main options). See [more](./dynamic#option-target). ## Option `prepare` Non-validation option. Prepares the passed value. ::: warning Use `prepare` carefully for simple preparatory actions only. Complex logic is better applied via [`make`](../actions/usage) actions. ::: ::: code-group ```ruby{5,10} [input] class Payments::Create < ApplicationService::Base input :amount_cents, as: :amount, type: Integer, prepare: ->(value:) { Money.from_cents(value, :USD) } # ... def create! outputs.payment = Payment.create!(amount: inputs.amount) end end ``` ::: --- --- url: 'https://servactory.com/introduction.md' description: >- Introduction to Servactory - unified approach to building reliable Ruby/Rails services --- # Why Servactory ## About Servactory Servactory standardizes building reliable services of any complexity. Create simple services: ```ruby class MinimalService < ApplicationService::Base def call # ... end end ``` Then call: ```ruby MinimalService.call! # or MinimalService.call ``` Or build complex services: ```ruby class Notifications::Send < ApplicationService::Base input :comment, type: Comment input :provider, type: NotificationProvider internal :user, type: User internal :status, type: String internal :response, type: NotificatorApi::Models::Notification output :notification, type: Notification make :assign_user make :assign_status make :create_notification! make :send_notification make :update_notification! make :update_comment! make :assign_status private def assign_user internals.user = inputs.comment.user end def assign_status internals.status = StatusEnum::NOTIFIED end def create_notification! outputs.notification = Notification.create!(user:, comment: inputs.comment, provider: inputs.provider) end def send_notification service_result = Notifications::API::Send.call(notification: outputs.notification) return fail!(message: service_result.error.message) if service_result.failure? internals.response = service_result.response end def update_notification! outputs.notification.update!(original_data: internals.response) end def update_comment! inputs.comment.update!(status: internals.status) end end ``` Call like this: ```ruby # comment = Comment.first # provider = NotificationProvider.first Notifications::Send.call!(comment:, provider:) # Or # Notifications::Send.call(comment:, provider:) ``` ## Reasons to use Servactory ### Unified approach Ruby's flexibility leads to inconsistent service implementations across applications. Over time, this inconsistency complicates development and makes services harder to understand. Servactory enforces a consistent API for service implementation, ensuring uniform logic structure across all classes. ### Testing Test Servactory services like standard Ruby classes. The unified approach ensures consistent testing patterns.