module Ratonvirus::Support::Backend

The backend implementation allows us to set different backends on the main Ratonvirus configuration, e.g. scanner and storage backends. This makes the library agnostic of the actual implementation of these both and allows the developer to configure

The solution is a bit hacky monkey patch type of solution as it adds code to the underlying implementation through class_eval. The reason for this is to define arbitrary getter, setter and destroye methods that are nicer to use for the user. Wrapping this functionality to its own module makes the resulting code less prone to errors as all of the backends are defined exactly the same way.

Modifying this may be tough, so be sure to test properly in case you make any modifications.

Public Instance Methods

backend_class(backend_cls, backend_type) click to toggle source

First argument “backend_cls”:

The subclass that refers to the backend's namespace, e.g.
`"Scanner"`.

Second argument “backend_type”:

The backend type in the given namespace, e.g. `:eicar`

The returned result will be e.g.

Ratonvirus::Scanner::Eicar
Ratonvirus::Storage::ActiveStorage
# File lib/ratonvirus/support/backend.rb, line 30
def backend_class(backend_cls, backend_type)
  return backend_type if backend_type.is_a?(Class)

  subclass = ActiveSupport::Inflector.camelize(backend_type.to_s)
  ActiveSupport::Inflector.constantize(
    "#{name}::#{backend_cls}::#{subclass}"
  )
end

Private Instance Methods

define_backend(backend_type, backend_subclass) click to toggle source

Defines the “backend” methods.

For example, this:

define_backend :foo, 'Foo'

Would define the following methods:

# Getter for foo
def self.foo
  @foo ||= create_foo
end

# Setter for foo
def self.foo=(foo_type)
  set_backend(
    :foo,
    'Foo',
    foo_type
  )
end

# Destroys the currently active foo.
# The foo is re-initialized when the getter is called.
def self.destroy_foo
  @foo = nil
end

private
  def self.create_foo
    if @foo_defs.nil?
      raise NotDefinedError.new("Foo not defined!")
    end

    @foo_defs[:klass].new(@foo_defs[:config])
  end

Usage (getter):

Ratonvirus.foo

Usage (setter):

Ratonvirus.foo = :bar
Ratonvirus.foo = :bar, {option: 'value'}
Ratonvirus.foo = Ratonvirus::Foo::Bar.new
Ratonvirus.foo = Ratonvirus::Foo::Bar.new({option: 'value'})

Usage (destroyer):

Ratonvirus.destroy_foo
# File lib/ratonvirus/support/backend.rb, line 88
      def define_backend(backend_type, backend_subclass)
        class_eval <<-CODE, __FILE__, __LINE__ + 1
          # Getter for #{backend_type}
          def self.#{backend_type}                      # def self.foo
            @#{backend_type} ||= create_#{backend_type} #   @foo ||= create_foo
          end                                           # end

          # Setter for #{backend_type}
          def self.#{backend_type}=(#{backend_type}_value) # def self.foo=(foo_value)
            set_backend(                                   #   set_backend(
              :#{backend_type},                            #     :foo
              "#{backend_subclass}",                       #     "Foo"
              #{backend_type}_value                        #     foo_value
            )                                              #   )
          end                                              # end

          # Destroys the currently active #{backend_type}.
          # The #{backend_type} is re-initialized when the getter is called.
          def self.destroy_#{backend_type} # def self.destroy_foo
            @#{backend_type} = nil         #   @foo = nil
          end                              # end

          # Creates a new backend instance
          # private
          def self.create_#{backend_type}                                   # def self.create_foo
            if @#{backend_type}_defs.nil?                                   #   if @foo_defs.nil?
              raise NotDefinedError.new("#{backend_subclass} not defined!") #     raise NotDefinedError.new("Foo not defined")
            end                                                             #   end
                                                                            #
            @#{backend_type}_defs[:klass].new(                              #   @foo_defs[:klass].new(
              @#{backend_type}_defs[:config]                                #     @foo_defs[:config]
            )                                                               #   )
          end                                                               # end
          private_class_method :create_#{backend_type}                      # private_class_method :create_foo
        CODE
      end
set_backend(backend_type, backend_cls, backend_value) click to toggle source

Sets the backend to local variables for the backend initialization. The goal of this method is to get the following configuration set to local `@x_defs` variable, where 'x' is the type of backend.

For example, for a backend with type “scanner”, this would be @scanner_defs.

The first argument, “backend_type” is the type of backend we are configuring, e.g. `:scanner`.

The second argument “backend_cls” is the backend subclass that is used in the module's namespace, e.g. “Scanner”. This would refer to subclasses `Ratonvirus::Scanner::…`.

The third argument “backend_value” is the actual value the user provided for the setter method, e.g. `:eicar` or `Ratonvirus::Scanner::Eicar.new`. The user may also provide a second argument to the setter method e.g. like `Ratonvirus.scanner = :eicar, {conf: 'option'}`, in which case these both arguments are provided in this argument as an array.

# File lib/ratonvirus/support/backend.rb, line 145
def set_backend(backend_type, backend_cls, backend_value)
  base_class = backend_class(backend_cls, "Base")

  if backend_value.is_a?(base_class)
    # Set the instance
    instance_variable_set(:"@#{backend_type}", backend_value)

    # Store the class (type) and config for storing them below to local
    # variable in case it needs to be re-initialized at some point.
    subtype = backend_value.class
    config = backend_value.config
  else
    case backend_value
    when Array
      subtype = backend_value.shift
      config = backend_value.shift || {}

      raise InvalidError, "Invalid #{backend_type} type: #{subtype}" unless subtype.is_a?(Symbol)
    when Symbol
      subtype = backend_value
      config = {}
    else
      raise InvalidError, "Invalid #{backend_type} provided!"
    end

    # Destroy the current one
    send(:"destroy_#{backend_type}")
  end

  instance_variable_set(
    :"@#{backend_type}_defs",
    klass: backend_class(backend_cls, subtype),
    config: config
  )
end