Class: TheHelp::Service

Inherits:
Object
  • Object
show all
Includes:
ProvidesCallbacks, ServiceCaller
Defined in:
lib/the_help/service.rb

Overview

Note:

See README section “Running Callbacks”

An Abstract Service Class with Authorization and Logging

Define subclasses of Service to build out the service layer of your application.

Examples:

class CreateNewUserAccount < TheHelp::Service
  input :user
  input :send_welcome_message, default: true

  authorization_policy do
    authorized = false
    call_service(Authorize, permission: :admin_users,
                 allowed: ->() { authorized = true })
    authorized
  end

  main do
    # do something to create the user account
    if send_welcome_message
      call_service(SendWelcomeMessage, user: user,
                   success: callback(:message_sent))
    end
  end

  callback(:message_sent) do |message|
    # do something really important with `message`, I'm sure
  end
end

class Authorize < TheHelp::Service
  input :permission
  input :allowed

  authorization_policy allow_all: true

  main do
    if user_has_permission?
      allowed.call
    end
  end
end

class SendWelcomeMessage < TheHelp::Service
  input :user
  input :success, default: ->(message) { }

  main do
    message = 'Hello, world!'
    # do something with message...
    run_callback(success, message)
  end
end

CreateNewUserAccount.(context: current_user, user: new_user_object)

Calling services with a block


# Calling a service with a block when the service is not designed to
# receive one will result in an exception being raised

class DoesNotTakeBlock < TheHelp::Service
  authorization_policy allow_all: true

  main do
    # whatever
  end
end

DoesNotTakeBlock.call { |result| true } # raises TheHelp::NoResultError

# However, if the service *is* designed to receive a block (by explicitly
# assigning to the internal `#result` attribute in the main routine), the
# result will be yielded to the block if a block is present.

class CanTakeABlock < TheHelp::Service
  authorization_policy allow_all: true

  main do
    self.result = :the_service_result
  end
end

service_result = nil

CanTakeABlock.call() # works just fine
service_result
#=> nil              # but obviously the result is just discarded

CanTakeABlock.call { |result| service_result = result }
service_result
#=> :the_service_result

Constant Summary collapse

CB_NOT_AUTHORIZED =

The default :not_authorized callback

It will raise a TheHelp::NotAuthorizedError when the context is not authorized to perform the service.

->(service:, context:) {
  raise TheHelp::NotAuthorizedError,
        "Not authorized to access #{service.name} as #{context.inspect}."
}

Class Method Summary collapse

Instance Method Summary collapse

Methods included from ServiceCaller

#call_service

Methods included from ProvidesCallbacks

included

Constructor Details

#initialize(context:, logger: Logger.new($stdout), not_authorized: CB_NOT_AUTHORIZED, **inputs) ⇒ Service

Returns a new instance of Service.



188
189
190
191
192
193
194
195
# File 'lib/the_help/service.rb', line 188

def initialize(context:, logger: Logger.new($stdout),
               not_authorized: CB_NOT_AUTHORIZED, **inputs)
  self.context = context
  self.logger = logger
  self.not_authorized = not_authorized
  self.inputs = inputs
  self.stop_caller = false
end

Class Method Details

.attr_accessor(*names, make_private: false, private_reader: false, private_writer: false) ⇒ Object

Defines attr_accessors with scoping options



116
117
118
119
120
121
122
123
# File 'lib/the_help/service.rb', line 116

def attr_accessor(*names, make_private: false, private_reader: false,
                  private_writer: false)
  super(*names)
  names.each do |name|
    private name if make_private || private_reader
    private "#{name}=" if make_private || private_writer
  end
end

.authorization_policy(allow_all: false, &block) ⇒ Object

Defines the service authorization policy

If allow_all is set to true, or if the provided block (executed in the context of the service object) returns true, then the service will be run when called. Otherwise, the not_authorized callback will be invoked.

Parameters:

  • allow_all (Boolean) (defaults to: false)
  • block (Proc)

    executed in the context of the service instance (and can therefore access all inputs to the service)



164
165
166
167
168
169
170
171
172
# File 'lib/the_help/service.rb', line 164

def authorization_policy(allow_all: false, &block)
  if allow_all
    define_method(:authorized?) { true }
  else
    define_method(:authorized?, &block)
  end
  private :authorized?
  self
end

.call(*args, &block) ⇒ Object

Convenience method to instantiate the service and immediately call it

Any arguments are passed to #initialize



128
129
130
131
132
# File 'lib/the_help/service.rb', line 128

def call(*args, &block)
  result = new(*args).call(&block)
  return result unless result.is_a?(self)
  self
end

.inherited(other) ⇒ Object

:nodoc:



135
136
137
# File 'lib/the_help/service.rb', line 135

def inherited(other)
  other.instance_variable_set(:@required_inputs, required_inputs.dup)
end

.input(name, **options) ⇒ Object



174
175
176
177
178
179
180
181
182
183
184
185
# File 'lib/the_help/service.rb', line 174

def input(name, **options)
  attr_accessor name, make_private: true
  if options.key?(:default)
    required_inputs.delete(name)
    define_method(name) do
      instance_variable_get("@#{name}") || options[:default]
    end
  else
    required_inputs << name
  end
  self
end

.main(&block) ⇒ Object

Defines the primary routine of the service

The code that will be run when the service is called, assuming it is unauthorized.



149
150
151
152
153
# File 'lib/the_help/service.rb', line 149

def main(&block)
  define_method(:main, &block)
  private :main
  self
end

.required_inputsObject

:nodoc: instances need access to this, otherwise it would be made private



141
142
143
# File 'lib/the_help/service.rb', line 141

def required_inputs
  @required_inputs ||= Set.new
end

Instance Method Details

#callObject



197
198
199
200
201
202
203
204
205
206
207
208
209
# File 'lib/the_help/service.rb', line 197

def call
  validate_service_definition
  catch(:stop) do
    authorize
    log_service_call
    main
    self.block_result = yield result if block_given?
  end
  throw :stop if stop_caller
  return block_result if block_given?
  return result if result_set?
  self
end