Dolganov Sergey @ Evil Martians
Input
Output
function (input) { ... } # =>
State
Pre-conditions
Post-conditions
Invariants
Contract
eBay Orders,
eBaymag Parcels
Lots of validations
HTTP Client
Confusing
content
module DHL
  module API
    def get_tariff(mapped_request)
      sampler = TariffSampler.new
      sampler.register_request(mapped_request)
      response = http_client.get("/GetQuote", mapped_request.to_xml)
      sampler.register_response(response)
      sampler.tag!
      response
    end
    class TariffSampler < BaseSampler
      def tag!
        tag_success!
        tag_dhl_address_ambiguity_warning!
        tag_dhl_composite_call_warning!
        tag_data_invalid_input_error!
        tag_destination_invalid_for_dhl!
        tag_unexpected_behavior! # if still no tag
      end
      # ...
    end
  end
end
"Contract"
module DHL
  module Responses
    class TariffPolicy < Tram::Policy
      param :response
      param :mapped_response
      validate :no_response_errors, stop_on_failure: true
      validate :delivery_cost_presence
      validate :delivery_currency_presence
      validate :delivery_date_presence
      # implementation of methods
  end
end        module DHL
          class Calculator < ::BaseService
            attr_reader :errors
            param :parcel
            def call
              policy = DHL::RegionalPolicy[parcel]
              return if policy.invalid?
              policy = DHL::Requests::TariffPolicy[parcel]
              return if policy.invalid?
              response = DHL::API.get_tariff(Requests::TariffMapper[parcel])
              mapped_response = Responses::TariffMapper[response]
              policy = DHL::Responses::TariffPolicy[response, mapped_response]
              return if policy.invalid?
              parcel.merge_update!(dhl: mapped_response)
            ensure
              @errors = policy&.errors 
            end
          end
        end
        if (service = DHL::Calculator.new(parcel)).call
          render json: { tariff: parcel.reload.dhl["tariff"] }
        else
          render json: { errors: { base: service.errors } }
        end
Order
Order
1. Wrap each mutation in a Command
(each could be done and undone, e.g. #up and #down in AR migration)
2. Chain Command
3. Execute chain
4. If one fails run `undo`
...
Failure
Failure
Validate
...
Success
Success
Validate
Parcel
Parcel*
...
Success
Success
Validate
PrintLabel
Validate#call
PrintLabel#call
...
Failure
Failure
Validate
PrintLabel
Validate#undo
PrintLabel#undo
(Sagas)
def create
  account = Account.find(params[:account_id])
  parcel = account.parcels.find(params[:id]) 
  status = 
    RussianPost::Pipeline.chain(:Validate)
                         .chain(:GetTransportationCost)
                         .chain(:Register)
                         .chain(:PayOnline)
                         .chain(:PrintLabel)
                         .new(parcel)
                         .call
  render json: status.payload
end...
Success
Success
Action 1
Action N
(Action 1 Attempt)
(Action N Attempt)
Success
Failure
(Done)
Subject
call
call
Events
(Action 1 Attempt)
(Action N Attempt)
(Done)
1
2
Success
...
Success
Success
Action 1
Action N
applied?
applied?
Events
...
Success
Success
Action 1
Action N
(Action 1 Attempt)
(Action N Attempt)
Success
Failure
(Done)
Subject
call
Events
Events
(Action 1 Attempt)
(Action N Attempt)
(Done)
1
2
Failure
...
Success
Success
Action N
Action 1
undo
(reversed)
undo
(Action 1 Attempt)
(Action N Attempt)
(Done)
1
2
Failure
...
Success
Success
Action N
Action 1
undo
Failure
(reversed)
lock Subject
with error
Events
Check out in more details
module UPS
  module Rates
    # types product, 
    # Request is both RegionalParcel and Schema
    Request = RegionalParcel.and_then(Schema)
    
    # types sum, 
    # Error is RecoverableInputError or InvalidRequest
    Error = RecoverableInputError.or(InvalidRequest) 
    # combine
    Response = 
      BaseTypes::JSONResponse.and_then(Tariff.or(Error))
  end
end
{ n :: Integer | n > 5 }module BaseTypes
  class JSONResponse < BaseType
    alias :response :value
    def match
      context[:body] = response.body
      context[:parsed] = ::JSON.parse(response.body)
      self
    rescue StandardError => error
      ContractFailure.new(error) # is BaseType, too
    end
    def unpack
      match.context[:parsed]
    end
  end
end
        class UPS::RatesContract < BaseContract # sampling by Sniffer & types
          def match(*input)
             input_match = Request.match(*input) 
             return input_match if input_match.invalid?
           
             result = yield(input_match)
             Response.match(result)
          end
        end        class UPS::API
          def rates_request(parcel)
            RatesContract.match(parcel) do |mapped_parcel|
              http_client.get('/1.0/Rates', mapped_parcel.unpack)
            end # returns a typed response
          end
        end
        case (match = UPS::API.rate_request(parcel))
        when ContractFailure
          Honeybadger.notify "Unexpected UPS behavior", context: match.context
          render json: { errors: "Unprocessable now, try later" }
        when Tariff
           render json: { tariff: match.unpack }
        when RecoverableInputError
          render json: { errors: match.unpack }
        when InvalidRequest
          Honeybadger.notify "Invalid UPS request", context: match.context
          render json: { errors: "Unprocessable now, try later" }
        else
          raise "Invalid contract processing"
        end        class UPS::RatesContract < BaseContract # sampling by types
          def match(*input)
             input_match = Request.match(*input) 
             return input_match if input_match.invalid?
           
             result = yield(input_match)
             Response.match(result)
          end
        end        class UPS::API
          def rates_request(parcel)
            RatesContract.match(parcel) do |mapped_parcel|
              http_client.get('/1.0/Rates', mapped_parcel.unpack)
            end # returns a typed response
          end
        end
        case (match = UPS::API.rate_request(parcel))
        when ContractFailure
          Honeybadger.notify "Unexpected UPS behavior", context: match.context
          render json: { errors: "Unprocessable now, try later" }
        when Tariff
           render json: { tariff: match.unpack }
        when RecoverableInputError
          render json: { errors: match.unpack }
        when InvalidRequest
          Honeybadger.notify "Invalid UPS request", context: match.context
          render json: { errors: "Unprocessable now, try later" }
        else
          raise "Invalid contract processing"
        end        class UPS::RatesContract < BaseContract # sampling by types
          def match(*input)
             input_match = Request.match(*input) 
             return input_match if input_match.invalid?
           
             result = yield(input_match)
             Response.match(result)
          end
        end        class UPS::API
          def rates_request(parcel)
            RatesContract.match(parcel) do |mapped_parcel|
              http_client.get('/1.0/Rates', mapped_parcel.unpack)
            end # returns a typed response
          end
        end
        case (match = UPS::API.rate_request(parcel))
        when ContractFailure
          Honeybadger.notify "Unexpected UPS behavior", context: match.context
          render json: { errors: "Unprocessable now, try later" }
        when Tariff
           render json: { tariff: match.unpack }
        when RecoverableInputError
          render json: { errors: match.unpack }
        when InvalidRequest
          Honeybadger.notify "Invalid UPS request", context: match.context
          render json: { errors: "Unprocessable now, try later" }
        else
          raise "Invalid contract processing"
        end        class UPS::RatesContract < BaseContract # sampling by types
          def match(*input)
             input_match = Request.match(*input) 
             return input_match if input_match.invalid?
           
             result = yield(input_match)
             Response.match(result)
          end
        end
        class UPS::API
          def rates_request(parcel)
            RatesContract.match(parcel) do |mapped_parcel|
              http_client.get('/1.0/Rates', mapped_parcel.unpack)
            end # returns a typed response
          end
        end
        case (match = UPS::API.rate_request(parcel))
        when ContractFailure
          Honeybadger.notify "Unexpected UPS behavior", context: match.context
          render json: { errors: "Unprocessable now, try later" }
        when Tariff
           render json: { tariff: match.unpack }
        when RecoverableInputError
          render json: { errors: match.unpack }
        when InvalidRequest
          Honeybadger.notify "Invalid UPS request", context: match.context
          render json: { errors: "Unprocessable now, try later" }
        else
          raise "Invalid contract processing"
        end(Refinement Types)
(valid implementation of Saga is not in gem yet)
(with Refinement Types)