Base class for endpoints that have an HTTP basis to their request and responses, even if the underlying transport is not HTTP. This is basically a collection of library-like routines useful to such classes and specifically excludes the part which actually makes an HTTP call (or AMQP call, or whatever) to a resource. That's up to the subclass.

This must never be instantiated directly as an endpoint. Instead, instantiate a subclass such as Hoodoo::Client::Endpoint::HTTP or Hoodoo::Client::Endpoint::AMQP.

Namespace
Methods
G
Instance Protected methods
get_data_for_request( description_of_request )

Preprocess a high level request description, returning HTTP orientated compiled data as a DataForRequest instance.

description_of_request

DescriptionOfRequest instance.

# File lib/hoodoo/client/endpoint/endpoints/http_based.rb, line 184
def get_data_for_request( description_of_request )
  body_hash  = Hoodoo::Utilities.stringify( description_of_request.body_hash  )
  query_hash = Hoodoo::Utilities.stringify( description_of_request.query_hash )
  ident      = description_of_request.ident.to_s

  body_data  = body_hash.nil? ? '' : ::JSON.generate( body_hash )

  # Amazingly, there's no fast way to deep clone a URI. Long story
  # short - Marshal.load(Marshal.dump(uri)) takes, astonishingly,
  # twice as long to execute as URI.parse(uri.to_s). I have no idea
  # how that's possible. The Addressable gem is even slower.
  #
  #   require 'benchmark'
  #   require 'addressable/uri' # Assuming gem is present
  #
  #   s='http://user:password@pond.org.uk:9924/foo/bar.baz?thing=that'
  #   u=URI.parse(s)
  #   a=Addressable::URI.parse(s)
  #
  #   Benchmark.realtime { 1000000.times { u2=URI.parse(u.to_s) } }
  #   # => 14.110195
  #   Benchmark.realtime { 1000000.times { a2=a.dup } }
  #   # => 26.530487
  #   Benchmark.realtime { 1000000.times { u2=Marshal.load(Marshal.dump(u)) } }
  #   # => 22.048637
  #
  # ...repeatably.
  #
  # TODO: Is it possible to improve this? It's truly awful, to the
  #       extent I'm almost motivated to write a URI handler gem.
  #       The core library URI API is tragically bad.

  remote_uri = URI.parse( description_of_request.endpoint_uri.to_s )

  # Now we've a copy, we can use high level URI methods to manipulate
  # it to form the full request URI.

  remote_uri.path << "/#{ URI::escape( ident ) }" unless ident.nil?

  # Grey area over whether this encodes spaces as "%20" or "+", but
  # so long as the middleware consistently uses the URI encode/decode
  # calls, it should work out in the end anyway.

  unless query_hash.nil?
    query_hash = query_hash.dup
    query_hash[ 'search' ] = URI.encode_www_form( query_hash[ 'search' ] ) if ( query_hash[ 'search' ].is_a?( ::Hash ) )
    query_hash[ 'filter' ] = URI.encode_www_form( query_hash[ 'filter' ] ) if ( query_hash[ 'filter' ].is_a?( ::Hash ) )

    query_hash[ '_embed'     ] = query_hash[ '_embed'     ].join( ',' ) if ( query_hash[ '_embed'     ].is_a?( ::Array ) )
    query_hash[ '_reference' ] = query_hash[ '_reference' ].join( ',' ) if ( query_hash[ '_reference' ].is_a?( ::Array ) )

    query_hash.delete( 'search'     ) if query_hash[ 'search'     ].nil? || query_hash[ 'search'     ].empty?
    query_hash.delete( 'filter'     ) if query_hash[ 'filter'     ].nil? || query_hash[ 'filter'     ].empty?
    query_hash.delete( '_embed'     ) if query_hash[ '_embed'     ].nil? || query_hash[ '_embed'     ].empty?
    query_hash.delete( '_reference' ) if query_hash[ '_reference' ].nil? || query_hash[ '_reference' ].empty?
  end

  remote_uri.query = URI.encode_www_form( query_hash ) unless query_hash.nil? || query_hash.empty?

  headers = {
    'Content-Type'     => 'application/json; charset=utf-8',
    'Content-Language' => self.locale() || 'en-nz', # Locale comes from Endpoint superclass
    'Accept-Language'  => self.locale() || 'en-nz'
  }

  # Interaction comes from Endpoint superclass.
  #
  # TODO: Can anything be done about inbound X-Interaction-ID
  #       headers or interaction ID values specified by the
  #       calling client which would be stripped by an Alchemy
  #       architecture but not by conventional HTTP servers?
  #
  unless self.interaction().nil?
    headers[ 'X-Interaction-ID' ] = self.interaction().interaction_id
  end

  # Session ID comes from Endpoint superclass.
  #
  unless self.session_id().nil?
    headers[ 'X-Session-ID'] = self.session_id()
  end

  # A suite of options is defined by a constant in the Endpoint
  # superclass.
  #
  Hoodoo::Client::Headers::HEADER_TO_PROPERTY.each do | rack_header, description |
    header_name = description[ :header      ]
    header_proc = description[ :header_proc ]
    property    = description[ :property    ]

    property_value  = self.send( property )

    unless property_value.nil?
      headers[ header_name ] = header_proc.call( property_value )
    end
  end

  data             = DataForRequest.new
  data.full_uri    = remote_uri
  data.body_string = body_data
  data.header_hash = headers
  data.query_hash  = query_hash

  return data
end
get_data_for_response( description_of_response )

Process a raw HTTP response description, returning an instance of Hoodoo::Client::AugmentedArray or Hoodoo::Client::AugmentedHash with either processed body data inside, or error data associated.

description_of_response

DescriptionOfResponse instance.

# File lib/hoodoo/client/endpoint/endpoints/http_based.rb, line 296
def get_data_for_response( description_of_response )
  code = description_of_response.http_status_code
  body = description_of_response.raw_body_data

  begin
    parsed = ::JSON.parse(
      body,
      :object_class => Hoodoo::Client::AugmentedHash,
      :array_class  => Hoodoo::Client::AugmentedArray
    )

  rescue => e
    data = response_class_for( description_of_response.action ).new
    data.response_options = Hoodoo::Client::Headers.x_header_to_options(
      description_of_response.http_headers
    )

    case code
      when 404
        return generate_404_response_for( description_of_response.action )
      when 408
        data.platform_errors.add_error( 'platform.timeout' )
      when 200
        data.platform_errors.add_error(
          'platform.fault',
          :message   => 'Could not parse retrieved body data despite receiving HTTP status code 200',
          :reference => { :exception => RuntimeError.new( "#{ body }" ) }
        )
      when 204
        if data.response_options[ 'deja_vu' ] != 'confirmed'
          data.platform_errors.add_error(
            'platform.fault',
            :message   => "Unexpected raw HTTP status code 204 with 'X-Deja-Vu: confirmed' not present",
            :reference => { :exception => RuntimeError.new( '204' ) }
          )
        end # Else do nothing; keep the empty 'data'
      else
        data.platform_errors.add_error(
          'platform.fault',
          :message   => "Unexpected raw HTTP status code #{ code } with non-JSON response",
          :reference => { :exception => RuntimeError.new( "#{ body }" ) }
        )
    end

    return data
  end

  # Just in case someone changes JSON parsers under us and the
  # replacement doesn't support the options used above...

  unless parsed.is_a?( Hoodoo::Client::AugmentedHash )
    raise "Hoodoo::Services::Middleware: Incompatible JSON implementation in use which doesn't understand 'object_class' or 'array_class' options"
  end

  # If the parsed data wrapped an array, extract just the array
  # part, else the hash part.

  if ( parsed[ '_data' ].is_a?( ::Array ) )
    size           = parsed[ '_dataset_size'           ]
    estimated_size = parsed[ '_estimated_dataset_size' ]

    parsed                        = parsed[ '_data' ]
    parsed.dataset_size           = size
    parsed.estimated_dataset_size = estimated_size

  elsif ( ( code < 200 || code > 299 ) && parsed[ 'kind' ] == 'Errors' )

    # This isn't an array, it's an AugmentedHash describing errors.
    # Turn this into a formal errors collection.

    errors_from_resource = Hoodoo::Errors.new()

    parsed[ 'errors' ].each do | error |
      errors_from_resource.add_precompiled_error(
        error[ 'code'      ],
        error[ 'message'   ],
        error[ 'reference' ],
        code
      )
    end

    # Use a 'clean' copy of the response class rather than keeping
    # the originating data. People will not make assumptions about
    # error payloads and trip over with the early return 404 stuff
    # etc. this way.

    parsed = response_class_for( description_of_response.action ).new
    parsed.set_platform_errors( errors_from_resource )

  end

  parsed.response_options = Hoodoo::Client::Headers.x_header_to_options(
    description_of_response.http_headers
  )

  return parsed
end