Class: TheHelp::Service

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

Overview

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
    # do something really important, 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: ->() { }

  main do
    # whatever
    success.call
  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.



186
187
188
189
190
191
192
# File 'lib/the_help/service.rb', line 186

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
end

Class Method Details

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

Defines attr_accessors with scoping options



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

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)



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

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



126
127
128
129
130
# File 'lib/the_help/service.rb', line 126

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

.inherited(other) ⇒ Object

:nodoc:



133
134
135
# File 'lib/the_help/service.rb', line 133

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

.input(name, **options) ⇒ Object



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

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.



147
148
149
150
151
# File 'lib/the_help/service.rb', line 147

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



139
140
141
# File 'lib/the_help/service.rb', line 139

def required_inputs
  @required_inputs ||= Set.new
end

Instance Method Details

#callObject



194
195
196
197
198
199
200
201
202
203
204
205
# File 'lib/the_help/service.rb', line 194

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