Class: FService::Base Abstract

Inherits:
Object
  • Object
show all
Defined in:
lib/f_service/base.rb

Overview

This class is abstract.

Abstract base class for services. It provides the basic interface to return and handle results.

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.call(*args) ⇒ Result::Success, Result::Failure

Note:

this method shouldn’t be overridden in the subclasses

Initializes and runs a new service.

Examples:

User::UpdateName.(user: user, new_name: new_name)
# or
User::UpdateName.call(user: user, new_name: new_name)

Returns:

Raises:



23
24
25
26
27
28
# File 'lib/f_service/base.rb', line 23

def call(*args)
  result = new(*args).run
  raise(FService::Error, 'Services must return a Result') unless result.is_a? Result::Base

  result
end

.to_procProc

Allows running a service without explicit giving params. This is useful when chaining services or mapping inputs to be processed.

Examples:

# Assuming all classes here subclass FService::Base:

User::Create
  .and_then(&User::Login)
  .and_then(&SendWelcomeEmail)

# Mapping inputs:

[{ n:1 }, { n: 2 }].map(&DoubleNumber).map(&:value)
# => [2, 4]

Returns:

  • (Proc)


48
49
50
# File 'lib/f_service/base.rb', line 48

def to_proc
  proc { |args| call(**args) }
end

Instance Method Details

#Check(*types, data: nil) ⇒ Result::Success, Result::Failure

Converts a boolean to a Result. Truthy values map to Success, and falsey values map to Failures. You can optionally provide a types for the result. The result value defaults as the evaluated value of the given block. If you want another value you can pass it through the ‘data:` argument.

Examples:

class CheckMathWorks < FService::Base
  def run
    Check(:math_works) { 1 < 2 }
    # => #<Success @value=true, @types=[:math_works]>

    Check(:math_works) { 1 > 2 }
    # => #<Failure @error=false, @types=[:math_works]>

    Check(:math_works, data: 1 + 2) { 1 > 2 }
    # => #<Failure @types=:math_works, @error=3>
  end

    Check(:math_works, data: 1 + 2) { 1 < 2 }
    # => #<Success @types=[:math_works], @value=3>
  end
end

Parameters:

  • types

    the Result types

Returns:



191
192
193
194
195
196
197
# File 'lib/f_service/base.rb', line 191

def Check(*types, data: nil)
  res = yield

  final_data = data || res

  res ? Success(*types, data: final_data) : Failure(*types, data: final_data)
end

#Failure(*types, data: nil) ⇒ Result::Failure

Returns a failed result. You can optionally specify types and a value for your result. You’ll probably want to return this inside #run.

Examples:

def run
  Failure()
  # => #<Failure @error=nil, @types=[]>

  Failure(:not_a_number)
  # => #<Failure @error=nil, @types=[:not_a_number]>

  Failure(data: "10")
  # => #<Failure @error="10", @types=[]>

  Failure(:not_a_number, data: "10")
  # => #<Failure @error="10", @types=[:not_a_number]>
end

Parameters:

  • types

    the Result types

  • data (defaults to: nil)

    the result value

Returns:



161
162
163
# File 'lib/f_service/base.rb', line 161

def Failure(*types, data: nil)
  Result::Failure.new(data, types)
end

#failure(data = nil) ⇒ Result::Failure

Deprecated.

Use #Failure instead.

Returns a failed operation. You’ll probably want to return this inside #run.

Examples:

class User::ValidateAge < FService::Base
  def initialize(age:)
    @age = age
  end

  def run
    return failure(status: 'No age given!', data: @age) if age.blank?
    return failure(status: 'Too young!', data: @age) if age < 18

    success(status: 'Valid age.', data: @age)
  end
end

Returns:



251
252
253
254
255
256
257
258
259
# File 'lib/f_service/base.rb', line 251

def failure(data = nil)
  FService.deprecate!(
    name: "#{self.class}##{__method__}",
    alternative: '#Failure',
    from: caller[0]
  )

  Result::Failure.new(data)
end

#result(condition, data = nil) ⇒ Result::Success, Result::Failure

Deprecated.

Use #Check instead.

Return either Success or Failure given the condition.

Examples:

class YearIsLeap < FService::Base
  def initialize(year:)
    @year = year
  end

  def run
    return failure(status: 'No year given!', data: @year) if @year.nil?

    result(leap?, @year)
  end

  private

  def leap?
    ((@year % 4).zero? && @year % 100 != 0) || (@year % 400).zero?
  end
end

Returns:



285
286
287
288
289
290
291
292
293
# File 'lib/f_service/base.rb', line 285

def result(condition, data = nil)
  FService.deprecate!(
    name: "#{self.class}##{__method__}",
    alternative: '#Check',
    from: caller[0]
  )

  condition ? success(data) : failure(data)
end

#runResult::Success, Result::Failure

Note:

this method SHOULD be overridden in the subclasses

This method is where the main work of your service must be. It is called after initilizing the service and should return an FService::Result.

Examples:

class User::UpdateName < FService::Base
  def initialize(user:, new_name:)
    @user = user
    @new_name = new_name
  end

  def run
    return Failure(:missing_user) if user.nil?

    if @user.update(name: @new_name)
      Success(:created, data: user)
    else
      Failure(:creation_failed, data: user.errors)
    end
  end
end

Returns:

Raises:

  • (NotImplementedError)


77
78
79
# File 'lib/f_service/base.rb', line 77

def run
  raise NotImplementedError, 'Services must implement #run'
end

#success(data = nil) ⇒ Result::Success

Deprecated.

Use #Success instead.

Returns a successful operation. You’ll probably want to return this inside #run.

Examples:

class User::ValidateAge < FService::Base
  def initialize(age:)
    @age = age
  end

  def run
    return failure(status: 'No age given!', data: @age) if age.blank?
    return failure(status: 'Too young!', data: @age) if age < 18

    success(status: 'Valid age.', data: @age)
  end
end

Returns:



101
102
103
104
105
106
107
108
109
# File 'lib/f_service/base.rb', line 101

def success(data = nil)
  FService.deprecate!(
    name: "#{self.class}##{__method__}",
    alternative: '#Success',
    from: caller[0]
  )

  Result::Success.new(data)
end

#Success(*types, data: nil) ⇒ Result::Success

Returns a successful result. You can optionally specify a list of types and a value for your result. You’ll probably want to return this inside #run.

Examples:

def run
  Success()
  # => #<Success @value=nil, @types=[]>

  Success(:ok)
  # => #<Success @value=nil, @types=[:ok]>

  Success(data: 10)
  # => #<Success @value=10, @types=[]>

  Success(:ok, data: 10)
  # => #<Success @value=10, @types=[:ok]>
end

Parameters:

  • types

    the Result types

  • data (defaults to: nil)

    the result value

Returns:



134
135
136
# File 'lib/f_service/base.rb', line 134

def Success(*types, data: nil)
  Result::Success.new(data, types)
end

#Try(*types, catch: StandardError) ⇒ Result::Success, Result::Failure

If the given block raises an exception, it wraps it in a Failure. Otherwise, maps the block value in a Success object. You can specify which exceptions to watch for. It’s possible to provide a types for the result too.

Examples:

class IHateEvenNumbers < FService::Base
  def run
    Try(:rand_int) do
      n = rand(1..10)
      raise "Yuck! It's a #{n}" if n.even?

      n
    end
  end
end

IHateEvenNumbers.call
# => #<Success @value=9, @types=[:rand_int]>

IHateEvenNumbers.call
# => #<Failure @error=#<RuntimeError: Yuck! It's a 4>, @types=[:rand_int]>

Parameters:

  • types

    the Result types

  • catch (defaults to: StandardError)

    the exception list to catch

Returns:



225
226
227
228
229
230
231
# File 'lib/f_service/base.rb', line 225

def Try(*types, catch: StandardError)
  res = yield

  Success(*types, data: res)
rescue *catch => e
  Failure(*types, data: e)
end