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`.