EGI-FCTF/rOCCI-core

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

Summary

Maintainability
A
25 mins
Test Coverage
module Occi
  module Core
    # Contains an attribute definition information.
    # This definition does not carry the name of the
    # attribute or its value. These should be associated
    # with the definition by other means, e.g. in a hash
    # `{ name: definition }` or by using instances of
    # the `Occi::Core::Attribute` class.
    #
    # @attr type [Class] Ruby class of the desired value
    # @attr required [TrueClass, FalseClass] required flag
    # @attr mutable [TrueClass, FalseClass] mutable flag
    # @attr default [Object] default value, optional
    # @attr description [String, NilClass] attribute description, optional
    # @attr pattern [Regexp, NilClass] value pattern, only for type `String`
    #
    # @author Boris Parak <parak@cesnet.cz>
    class AttributeDefinition
      include Yell::Loggable
      include Helpers::ArgumentValidator
      extend Helpers::YamlSummoner

      attr_accessor :type, :required, :mutable,
                    :default, :description, :pattern

      # Constructs an instance with sensible defaults. The default
      # definition is for a String-based optional attribute without
      # a pattern.
      #
      # @example
      #   AttributeDefinition.new type: Integer, required: true
      #   AttributeDefinition.new
      #
      # @param args [Hash] arguments with definition information
      # @option args [Class] :type (String) class of the desired value
      # @option args [TrueClass, FalseClass] :required (false) required flag
      # @option args [TrueClass, FalseClass] :mutable (true) mutable flag
      # @option args [Object] :default (nil) default value
      # @option args [String, NilClass] :description (nil) attribute description
      # @option args [Regexp, NilClass] :pattern (nil) value pattern
      def initialize(args = {})
        default_args! args

        @type = args.fetch(:type)
        @required = args.fetch(:required)
        @mutable = args.fetch(:mutable)
        @default = args.fetch(:default)
        @description = args.fetch(:description)
        @pattern = args.fetch(:pattern)
      end

      # Alias with a question mark
      alias required? required
      alias mutable? mutable

      # Changes the value of `required` to `true`, in case
      # `required` is `nil` or `false`.
      #
      # @example
      #   attr_def.required? # => false
      #   attr_def.required!
      #   attr_def.required? # => true
      def required!
        self.required = true
      end

      # Shorthand for getting the negated value of `required`.
      #
      # @example
      #   attr_def.required? # => false
      #   attr_def.optional? # => true
      #
      # @return [TrueClass, FalseClass] negated value of `required`
      def optional?
        !required?
      end

      # Changes the value of `required` to `false`, in case
      # `required` is `nil` or `true`.
      #
      # @example
      #   attr_def.required? # => true
      #   attr_def.optional!
      #   attr_def.required? # => false
      def optional!
        self.required = false
      end

      # Changes the value of `mutable` to `true`, in case
      # `mutable` is `nil` or `false`.
      #
      # @example
      #   attr_def.mutable? # => false
      #   attr_def.mutable!
      #   attr_def.mutable? # => true
      def mutable!
        self.mutable = true
      end

      # Shorthand for getting the negated value of `mutable`.
      #
      # @example
      #   attr_def.mutable?   # => true
      #   attr_def.immutable? # => false
      #
      # @return [TrueClass, FalseClass] negated value of `mutable`
      def immutable?
        !mutable?
      end

      # Changes the value of `mutable` to `false`, in case
      # `mutable` is `nil` or `true`.
      #
      # @example
      #   attr_def.mutable?   # => true
      #   attr_def.immutable!
      #   attr_def.mutable?   # => false
      def immutable!
        self.mutable = false
      end

      # Indicates the presence of a default value.
      #
      # @example
      #   attr_def.default  # => nil
      #   attr_def.default? # => false
      #
      # @return [TrueClass, FalseClass] default value indicator
      def default?
        !default.nil?
      end

      # Indicates the presence of a pattern for value.
      #
      # @example
      #   attr_def.pattern  # => /.*/
      #   attr_def.pattern? # => true
      #
      # @return [TrueClass, FalseClass] pattern indicator
      def pattern?
        !pattern.nil?
      end

      # Indicates whether the given value is an acceptable
      # value for an attribute with this definition.
      #
      # @example
      #   attr_def.type       # => String
      #   attr_def.value? 5.0 # => false
      #
      # @param value [Object] candidate value
      # @return [TrueClass, FalseClass] validation result
      def valid?(value)
        begin
          valid! value
        rescue Occi::Core::Errors::AttributeValidationError => ex
          logger.debug "AttributeValidation: #{ex.message}"
          return false
        end

        true
      end

      # Indicates whether the given value is an acceptable
      # value for an attribute with this definition. This
      # method will raise an error if the given value is
      # not acceptable.
      #
      # @example
      #   attr_def.type       # => String
      #   attr_def.value! 0.5 # => Occi::Core::Errors::AttributeValidationError
      #
      # @param value [Object] candidate value
      def valid!(value)
        valid_type!
        valid_value! value
      end

      private

      # :nodoc:
      def valid_type!
        return if type
        raise Occi::Core::Errors::AttributeValidationError,
              'No type has been defined'
      end

      # :nodoc:
      def valid_value!(value)
        if value.nil? && required?
          raise Occi::Core::Errors::AttributeValidationError,
                'Value is required but not provided'
        end
        return if value.nil? && optional?

        unless value_ancestors(value).include?(type)
          raise Occi::Core::Errors::AttributeValidationError,
                "Type #{value.class} is incompatible with " \
                "defined type #{type}"
        end

        match_pattern! value
      end

      # :nodoc:
      def match_pattern!(value)
        return unless type_ancestors.include?(String)
        return unless pattern?
        return if pattern.match(value)

        raise Occi::Core::Errors::AttributeValidationError,
              "#{value.inspect} does not match pattern #{pattern.inspect}"
      end

      # :nodoc:
      def type_ancestors
        type.ancestors
      end

      # :nodoc:
      def value_ancestors(value)
        value.class.ancestors
      end

      # :nodoc:
      def sufficient_args!(args)
        sufficient_type! args[:type]

        %i[required mutable].each do |attr|
          next unless args[attr].nil?
          raise Occi::Core::Errors::MandatoryArgumentError,
                "#{attr} is a mandatory argument"
        end
      end

      # :nodoc:
      def sufficient_type!(type)
        if type.nil?
          raise Occi::Core::Errors::MandatoryArgumentError,
                'type is a mandatory argument'
        end

        return if type.is_a?(Class) || type.is_a?(Module)
        raise Occi::Core::Errors::MandatoryArgumentError,
              'type must be a class or module'
      end

      # :nodoc:
      def defaults
        {
          type: String,
          required: false,
          mutable: true,
          default: nil,
          description: nil,
          pattern: nil
        }
      end

      class << self
        # :nodoc:
        def allowed_yaml_classes
          [String, Regexp, URI, IPAddr, Integer, Float, Boolean, Array, Hash].freeze
        end
        private :allowed_yaml_classes
      end
    end
  end
end