lib/rack/linkeddata/conneg.rb in rack-linkeddata-0.3.0 vs lib/rack/linkeddata/conneg.rb in rack-linkeddata-1.0.0
- old
+ new
@@ -1,9 +1,23 @@
module Rack; module LinkedData
##
# Rack middleware for Linked Data content negotiation.
#
+ # Uses HTTP Content Negotiation to find an appropriate RDF
+ # format to serialize any result with a body being `RDF::Enumerable`.
+ #
+ # Override content negotiation by setting the :format option to
+ # {#initialize}.
+ #
+ # Add a :default option to set a content type to use when nothing else
+ # is found.
+ #
+ # @example
+ # use Rack::LinkedData::ContentNegotation, :format => :ttl
+ # use Rack::LinkedData::ContentNegotiation, :format => RDF::NTriples::Format
+ # use Rack::LinkedData::ContentNegotiation, :default => 'application/rdf+xml'
+ #
# @see http://www4.wiwiss.fu-berlin.de/bizer/pub/LinkedDataTutorial/
class ContentNegotiation
DEFAULT_CONTENT_TYPE = "text/plain" # N-Triples
VARY = {'Vary' => 'Accept'}.freeze
@@ -14,13 +28,15 @@
attr_reader :options
##
# @param [#call] app
# @param [Hash{Symbol => Object}] options
- # @option options [String] :default (DEFAULT_CONTENT_TYPE)
+ # Other options passed to writer.
+ # @option options [String] :default (DEFAULT_CONTENT_TYPE) Specific content type
+ # @option options [RDF::Format, #to_sym] :format Specific RDF writer format to use
def initialize(app, options = {})
- @app, @options = app, options.to_hash.dup
+ @app, @options = app, options
@options[:default] = (@options[:default] || DEFAULT_CONTENT_TYPE).to_s
end
##
# Handles a Rack protocol request.
@@ -28,36 +44,35 @@
# @param [Hash{String => String}] env
# @return [Array(Integer, Hash, #each)]
# @see http://rack.rubyforge.org/doc/SPEC.html
def call(env)
response = app.call(env)
- case env['REQUEST_METHOD'].to_sym
- when :GET, :HEAD
- case response[2] # the body
- when RDF::Enumerable
- serialize(env, *response)
- else response
- end
+ body = response[2].respond_to?(:body) ? response[2].body : response[2]
+ case body
+ when RDF::Enumerable
+ response[2] = body # Put it back in the response, it might have been a proxy
+ serialize(env, *response)
else response
end
end
##
# Serializes an `RDF::Enumerable` response into a Rack protocol
- # response using HTTP content negotiation rules.
+ # response using HTTP content negotiation rules or a specified Content-Type.
#
# @param [Hash{String => String}] env
# @param [Integer] status
# @param [Hash{String => Object}] headers
# @param [RDF::Enumerable] body
# @return [Array(Integer, Hash, #each)]
def serialize(env, status, headers, body)
begin
- writer, content_type = find_writer(env)
+ writer, content_type = find_writer(env, headers)
if writer
- headers = headers.merge(VARY).merge('Content-Type' => content_type) # FIXME: don't overwrite existing Vary headers
- [status, headers, [writer.dump(body)]]
+ # FIXME: don't overwrite existing Vary headers
+ headers = headers.merge(VARY).merge('Content-Type' => content_type)
+ [status, headers, [writer.dump(body, nil, @options)]]
else
not_acceptable
end
rescue RDF::WriterError => e
not_acceptable
@@ -65,24 +80,33 @@
end
##
# Returns an `RDF::Writer` class for the given `env`.
#
+ # If options contain a `:format` key, it identifies the specific format to use;
+ # otherwise, if the environment has an HTTP_ACCEPT header, use it to find a writer;
+ # otherwise, use the default content type
+ #
# @param [Hash{String => String}] env
+ # @param [Hash{String => Object}] headers
# @return [Array(Class, String)]
# @see http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html
- def find_writer(env)
- unless env.has_key?('HTTP_ACCEPT')
- # HTTP/1.1 §14.1: "If no Accept header field is present, then it is
- # assumed that the client accepts all media types"
- find_writer_for_content_type(options[:default])
- else
+ def find_writer(env, headers)
+ if @options[:format]
+ format = @options[:format]
+ writer = RDF::Writer.for(format.to_sym) unless format.is_a?(RDF::Format)
+ return [writer, writer.format.content_type.first] if writer
+ elsif env.has_key?('HTTP_ACCEPT')
content_types = parse_accept_header(env['HTTP_ACCEPT'])
content_types.each do |content_type|
writer, content_type = find_writer_for_content_type(content_type)
return [writer, content_type] if writer
end
return nil
+ else
+ # HTTP/1.1 §14.1: "If no Accept header field is present, then it is
+ # assumed that the client accepts all media types"
+ find_writer_for_content_type(options[:default])
end
end
##
# Returns an `RDF::Writer` class for the given `content_type`.