class Occi::Core::Parsers::TextParser

Implementes components necessary to parse all required instance types from `text` or `text`-like format.

@author Boris Parak <parak@cesnet.cz>

Constants

ATTRIBUTE_KEYS
CATEGORY_KEYS
HEADERS_TEXT_TYPES
HEADER_HTTP_PREFIX

Constants for header normalization

KEY_NAME_GROUPS

Constants for OCCI keys

LOCATION_KEYS
MEDIA_TYPES
OCCI_KEYS
OCCI_TEXT_TYPES
PLAIN_TEXT_TYPES
URI_LIST_TYPES

Media type constants

Public Class Methods

canonize_header_value(value) click to toggle source

Attempts to canonize value from headers by looking for hidden multi-value lines usually separated by commas.

@param value [Object] value to canonize @return [Object] original or canonized value

# File lib/occi/core/parsers/text_parser.rb, line 222
def canonize_header_value(value)
  value = value.first if value.is_a?(Array) && value.count == 1 # ary with 1 could be hidden multi-val
  value = value.split(',').map(&:strip) if value.is_a?(String)  # multi-vals can hide as ','-separated String
  value
end
canonize_headers(headers) click to toggle source

Canonizes normalized headers by eliminating naming differences in keys. Input for this method must be already normalized by `normalize_headers`.

@param headers [Hash] hash with normalized header key-value pairs @return [Hash] canonized hash with predictable keys

# File lib/occi/core/parsers/text_parser.rb, line 204
def canonize_headers(headers)
  validate_header_keys! headers.keys

  canonical = {}
  headers.each_pair do |key, value|
    pref = key_name_groups.detect { |ka| ka.include?(key) }
    canonical[pref.first] = [canonize_header_value(value)].flatten
  end

  logger.debug "Canonized headers #{canonical.inspect}" if logger_debug?
  canonical
end
key_name_groups() click to toggle source

Returns a list of available key name groups accessible as constants by name on this class.

@return [Array] list of available key name groups

# File lib/occi/core/parsers/text_parser.rb, line 258
def key_name_groups
  KEY_NAME_GROUPS.map { |kn| const_get kn }
end
locations(body, headers, media_type) click to toggle source

Extracts URI-like locations from body and headers. For details, see `Occi::Core::Parsers::Text::Location`.

@param body [String] raw `String`-like body as provided by the transport protocol @param headers [Hash] raw headers as provided by the transport protocol @param media_type [String] media type string as provided by the transport protocol @return [Array] list of extracted URIs

# File lib/occi/core/parsers/text_parser.rb, line 141
def locations(body, headers, media_type)
  if logger_debug?
    logger.debug "Parsing locations from #{media_type.inspect} in #{body.inspect} and #{headers.inspect}"
  end

  if URI_LIST_TYPES.include? media_type
    Text::Location.uri_list transform_body(body)
  elsif HEADERS_TEXT_TYPES.include? media_type
    Text::Location.plain transform_headers(headers)
  else
    Text::Location.plain transform_body(body)
  end
end
model(body, headers, media_type, model) click to toggle source

Extracts categories from body and headers. For details, see `Occi::Core::Parsers::Text::Category`.

@param body [String] raw `String`-like body as provided by the transport protocol @param headers [Hash] raw headers as provided by the transport protocol @param media_type [String] media type string as provided by the transport protocol @param model [Occi::Core::Model] `Model`-like instance to be populated (may contain existing categories) @return [Occi::Core::Model] model instance filled with parsed categories

# File lib/occi/core/parsers/text_parser.rb, line 121
def model(body, headers, media_type, model)
  if logger_debug?
    logger.debug "Parsing model from #{media_type.inspect} in #{body.inspect} and #{headers.inspect}"
  end

  if HEADERS_TEXT_TYPES.include? media_type
    Text::Category.plain transform_headers(headers), model
  elsif PLAIN_TEXT_TYPES.include? media_type
    Text::Category.plain transform_body(body), model
  else
    raise Occi::Core::Errors::ParsingError, "Model cannot be parsed from #{media_type.inspect}"
  end
end
normalize_headers(headers) click to toggle source

Normalizes raw headers into a more agreeable form. This process will remove known prefixes as well as blank lines or non-OCCI keys.

@param headers [Hash] hash with raw header key-value pairs @return [Hash] a cleaner hash with relevant headers

# File lib/occi/core/parsers/text_parser.rb, line 186
def normalize_headers(headers)
  return {} unless headers

  headers = Hash[
    headers.map { |k, v| [k.gsub(HEADER_HTTP_PREFIX, '').capitalize, v] }
  ]                                                 # remove 'HTTP_' prefix in keys
  headers.delete_if { |_k, v| v.blank? }            # remove pairs with empty values
  headers.keep_if { |k, _v| OCCI_KEYS.include?(k) } # drop non-OCCI pairs

  logger.debug "Normalized headers #{headers.inspect}" if logger_debug?
  headers
end
transform_body(body) click to toggle source

Transforms a `String`-like body into a series of independent lines. Note: Multi-line elements are not supported by this parser.

@param body [String] multi-line body @return [Array] an array of lines

# File lib/occi/core/parsers/text_parser.rb, line 160
def transform_body(body)
  return [] if body.blank?
  lines = body.respond_to?(:lines) ? body.lines : body.split("\n")
  lines.map(&:strip)
end
transform_headers(headers) click to toggle source

Transforms a widely varied content of headers into a unified body-like format for further processing. This method and its helpers should hide differences between `text/occi` and `text/plain` completely.

@param headers [Hash] hash with raw header key-value pairs @return [Array] an array of body-like lines

# File lib/occi/core/parsers/text_parser.rb, line 172
def transform_headers(headers)
  logger.debug "Transforming headers #{headers.inspect}" if logger_debug?
  unify_headers(
    canonize_headers(
      normalize_headers(headers)
    )
  )
end
unify_headers(headers) click to toggle source

Unifies canonized headers by transforming them into a body-like form. Output of this method should be directly parsable as `text/plain` found in body.

@param headers [Hash] hash with canonized header key-value pairs @return [Array] an array of body-like lines

# File lib/occi/core/parsers/text_parser.rb, line 234
def unify_headers(headers)
  lines = []
  headers.each_pair do |k, v|
    lines << v.map { |val| "#{k}: #{val}" }
  end

  logger.debug "Unified headers #{lines.inspect}" if logger_debug?
  lines.flatten.sort
end
validate_header_keys!(headers_keys) click to toggle source

Checks for use of multiple keys from a single key name groups. See `key_name_groups` for a list of available groups. Mixed key notations will produce an error.

@param headers_keys [Array] list of keys to check @raise [Occi::Core::Errors::ParsingError] if mixed key notations detected

# File lib/occi/core/parsers/text_parser.rb, line 249
def validate_header_keys!(headers_keys)
  return unless key_name_groups.any? { |elm| (headers_keys & elm).count > 1 }

  raise Occi::Core::Errors::ParsingError, "Headers #{headers_keys.inspect} contain mixed key notations"
end

Public Instance Methods

action_instances(body, headers) click to toggle source

Parses action instances from the given body/headers. Only actions already declared in the model are allowed.

@param body [String] raw `String`-like body as provided by the transport protocol @param headers [Hash] raw headers as provided by the transport protocol @return [Set] set of parsed instances

# File lib/occi/core/parsers/text_parser.rb, line 70
def action_instances(body, headers)
  logger.debug "Parsing Occi::Core::ActionInstance from #{body.inspect} and #{headers.inspect}" if logger_debug?
  entity_parser = Text::Entity.new(model: model)
  tformed = transform(body, headers)

  action_instance = Occi::Core::ActionInstance.new(
    action: action_instance_category(entity_parser, tformed)
  )
  entity_parser.plain_attributes! tformed, action_instance.attributes
  setify(action_instance)
end
categories(body, headers, expectation = nil) click to toggle source

Parses categories from the given body/headers and returns corresponding instances from the known model.

@param body [String] raw `String`-like body as provided by the transport protocol @param headers [Hash] raw headers as provided by the transport protocol @param expectation [Class] expected class of the returned instance(s) @return [Set] set of instances

# File lib/occi/core/parsers/text_parser.rb, line 89
def categories(body, headers, expectation = nil)
  expectation ||= Occi::Core::Category
  logger.debug "Parsing #{expectation} from #{body.inspect} and #{headers.inspect}" if logger_debug?

  cats = transform(body, headers).map do |line|
    cat = Text::Category.plain_category(line, false)
    lookup "#{cat[:scheme]}#{cat[:term]}", expectation
  end

  setify(cats)
end
entities(body, headers, expectation = nil) click to toggle source

Parses entities from the given body/headers. Only kinds, mixins, and actions already declared in the model are allowed.

@param body [String] raw `String`-like body as provided by the transport protocol @param headers [Hash] raw headers as provided by the transport protocol @param expectation [Class] expected class of the returned instance(s) @return [Set] set of instances

# File lib/occi/core/parsers/text_parser.rb, line 51
def entities(body, headers, expectation = nil)
  expectation ||= Occi::Core::Entity
  logger.debug "Parsing #{expectation} from #{body.inspect} and #{headers.inspect}" if logger_debug?

  entity_parser = Text::Entity.new(model: model)
  entity = entity_parser.plain transform(body, headers)
  unless entity.is_a?(expectation)
    raise Occi::Core::Errors::ParsingError, "Entity is not of type #{expectation}"
  end

  setify(entity)
end

Private Instance Methods

transform(body, headers) click to toggle source

Transforms `body` and `headers` into an array of lines parsable by 'text/plain' parser.

@param body [String] raw `String`-like body as provided by the transport protocol @param headers [Hash] raw headers as provided by the transport protocol @return [Array] array with transformed lines

# File lib/occi/core/parsers/text_parser.rb, line 107
def transform(body, headers)
  klass = self.class
  HEADERS_TEXT_TYPES.include?(media_type) ? klass.transform_headers(headers) : klass.transform_body(body)
end