the-rocci-project/rOCCI-core

View on GitHub
lib/occi/core/renderer_factory.rb

Summary

Maintainability
A
0 mins
Test Coverage
module Occi
  module Core
    # A singleton factory class offering convenient access to all
    # available renderer classes. Factory can be customized using
    # the `required_methods` and `namespace` attributes.
    #
    # @attr required_methods [Array] list of required renderer methods
    # @attr namespace [Module] module containing renderer candidates
    #
    # @author Boris Parak <parak@cesnet.cz>
    class RendererFactory
      include Singleton
      include Yell::Loggable
      include Helpers::ArgumentValidator

      # Methods expected on supported renderer classes
      REQUIRED_METHODS = %i[renderer? formats render].freeze
      # Parent namespace of all supported renderer classes
      NAMESPACE = Occi::Core::Renderers

      attr_accessor :required_methods, :namespace

      # Constructs an instance of the `RendererFactory` class. Since this
      # class is a singleton, `new` (or `initialize`) are not supposed to
      # be called directly. Use attribute accessors to change settings on
      # existing factories.
      #
      # @param args [Hash] hash with factory settings
      # @option args [Array] :required_methods (`REQUIRED_METHODS`) list of required renderer methods
      # @option args [Module] :namespace (`NAMESPACE`) module containing renderer candidates
      def initialize(args = {})
        default_args! args

        logger.debug { "Initializing RendererFactory with #{args.inspect}" }
        @required_methods = args.fetch(:required_methods)
        @namespace = args.fetch(:namespace)

        reload!
      end

      #
      def reload!
        logger.debug { 'Clearing RendererFactory cache for renderer reload' }
        @ravail_cache = nil
      end

      # Lists available rendering `format`s.
      #
      # @example
      #   formats # => ['text', 'json', 'headers']
      #
      # @return [Array] list of formats, as Strings
      def formats
        renderers.keys
      end

      # Lists available renderers as a Hash mapping `format` to `Renderer` class.
      #
      # @example
      #   renderers # => { 'text' => TextRenderer }
      #
      # @return [Hash] map of available renderers, keyed by `format`
      def renderers
        return @ravail_cache if @ravail_cache
        @ravail_cache = {}

        renderer_classes.each do |rndr_klass|
          logger.debug { "RendererFactory registering #{rndr_klass} for #{rndr_klass.formats}" }
          rndr_klass.formats.each { |rndr_klass_f| @ravail_cache[rndr_klass_f] = rndr_klass }
        end

        @ravail_cache
      end

      # Returns a renderer corresponding with the given `format`.
      # If no such renderer exists, `Occi::Core::Errors::RenderingError`
      # error is raised.
      #
      # @example
      #   renderer_for 'text'   # => Occi::Core::Renderers::TextRenderer
      #   renderer_for 'tewat?' # => !Error: Occi::Core::Errors::RenderingError!
      #
      # @param format [String] over-the-wire format
      # @return [Class] factory renderer corresponding to `format`
      def renderer_for(format)
        if format.blank?
          raise Occi::Core::Errors::RenderingError,
                'Cannot return a renderer for an unspecified format'
        end
        renderers[format] || raise(Occi::Core::Errors::RenderingError, "No renderer for #{format.inspect}")
      end

      # Lists available renderers as an Array of renderer classes.
      #
      # @example
      #   renderer_classes #=> [Occi::Core::Renderers::TextRenderer]
      #
      # @return [Array] list of renderer classes
      def renderer_classes
        self.class.classes_from(namespace).select { |cand| renderer? cand }
      end

      # Checks whether the given object can act as a renderer.
      #
      # @example
      #   renderer? TextRenderer # => true
      #   renderer? NilClass     # => false
      #
      # @param candidate [Object, Class] object or class to check
      # @return [TrueClass, FalseClass] result (`true` for renderer, else `false`)
      def renderer?(candidate)
        begin
          renderer_with_methods! candidate
          renderer_with_formats! candidate
        rescue Occi::Core::Errors::RendererError => ex
          logger.debug { "Renderer validation failed with #{ex.message}" }
          return false
        end

        candidate.renderer?
      end

      # Ensures that the renderer candidate passed as an argument
      # exposes all required methods. If that is not the case,
      # an `Occi::Core::Errors::RendererError` error is raised.
      #
      # @param candidate [Object, Class] object or class to check
      def renderer_with_methods!(candidate)
        required_methods.each do |method|
          unless candidate.respond_to?(method)
            raise Occi::Core::Errors::RendererError, "#{candidate.inspect} " \
                  "does not respond to #{method.inspect}"
          end
        end
      end

      # Ensures that the renderer candidate passed as an argument
      # exposes supported formats. If that is not the case,
      # an `Occi::Core::Errors::RendererError` error is raised.
      #
      # @param candidate [Object, Class] object or class to check
      def renderer_with_formats!(candidate)
        unless candidate.respond_to?(:formats)
          raise Occi::Core::Errors::RendererError, "#{candidate.inspect} " \
                "does not respond to 'formats'"
        end

        return unless candidate.formats.blank?
        raise Occi::Core::Errors::RendererError, "#{candidate.inspect} " \
              'does not expose any supported formats'
      end

      class << self
        # Lists default methods required from any supported renderer.
        #
        # @return [Array] list of method symbols
        def required_methods
          REQUIRED_METHODS
        end

        # Returns the default renderer namespace.
        #
        # @return [Module] base namespace
        def namespace
          NAMESPACE
        end

        # Returns all constants from the given namespace. The list may contain
        # constants other than classes, from the given namespace and needs to
        # be refined further.
        #
        # @param namespace [Module] base namespace
        # @return [Array] list of constants
        def constants_from(namespace)
          unless namespace.is_a? Module
            raise Occi::Core::Errors::RendererError, "#{namespace.inspect} " \
                  'is not a Module'
          end
          logger.debug { "RendererFactory looking for renderers in #{namespace}" }
          namespace.constants.collect { |const| namespace.const_get(const) }
        end

        # Returns all classes from the given namespace.
        #
        # @param namespace [Module] base namespace
        # @return [Array] list of classes
        def classes_from(namespace)
          constants_from(namespace).select { |const| const.is_a? Class }
        end
      end

      private

      # :nodoc:
      def defaults
        {
          required_methods: self.class.required_methods,
          namespace: self.class.namespace
        }
      end

      # :nodoc:
      def sufficient_args!(args)
        %i[required_methods namespace].each do |attr|
          if args[attr].blank?
            raise Occi::Core::Errors::MandatoryArgumentError,
                  "#{attr} is a mandatory argument for #{self.class}"
          end
        end
      end
    end
  end
end