Class: Rack::RDF::ContentNegotiation
- Inherits:
-
Object
- Object
- Rack::RDF::ContentNegotiation
- Defined in:
- lib/rack/rdf/conneg.rb
Overview
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.
Constant Summary collapse
- DEFAULT_CONTENT_TYPE =
N-Triples
"application/n-triples"
- VARY =
{'Vary' => 'Accept'}.freeze
Instance Attribute Summary collapse
- #app ⇒ #call readonly
- #options ⇒ Hash{Symbol => Object} readonly
Instance Method Summary collapse
-
#accept_entry(entry) ⇒ Object
protected
Returns pair of content_type (including non-‘q’ parameters) and array of quality, number of ‘*’ in content-type, and number of non-‘q’ parameters.
-
#call(env) ⇒ Array(Integer, Hash, #each)
Handles a Rack protocol request.
-
#find_content_type_for_media_range(media_range) ⇒ String?
protected
Returns a content type appropriate for the given
media_range
, returnsnil
ifmedia_range
contains a wildcard subtype that is not mapped. -
#find_writer(env, headers) {|writer, content_type, accept_params| ... } ⇒ Object
protected
Yields an
RDF::Writer
class for the givenenv
. -
#find_writer_for_content_type(content_type) {|writer, content_type| ... } ⇒ Object
protected
Yields an
RDF::Writer
class for the givencontent_type
. -
#http_error(code, message = nil, headers = {}) ⇒ Array(Integer, Hash, #each)
protected
Outputs an HTTP
4xx
or5xx
response. -
#http_status(code) ⇒ String
protected
Returns the standard HTTP status message for the given status
code
. -
#initialize(app, options) ⇒ ContentNegotiation
constructor
A new instance of ContentNegotiation.
-
#not_acceptable(message = nil) ⇒ Array(Integer, Hash, #each)
protected
Outputs an HTTP
406 Not Acceptable
response. -
#parse_accept_header(header) ⇒ Array<String>
protected
Parses an HTTP
Accept
header, returning an array of MIME content types ordered by the precedence rules defined in HTTP/1.1 §14.1. -
#serialize(env, status, headers, body) ⇒ Array(Integer, Hash, #each)
Serializes an
RDF::Enumerable
response into a Rack protocol response using HTTP content negotiation rules or a specified Content-Type.
Constructor Details
#initialize(app, options) ⇒ ContentNegotiation
Returns a new instance of ContentNegotiation.
37 38 39 40 |
# File 'lib/rack/rdf/conneg.rb', line 37 def initialize(app, ) @app, @options = app, @options[:default] = (@options[:default] || DEFAULT_CONTENT_TYPE).to_s end |
Instance Attribute Details
#options ⇒ Hash{Symbol => Object} (readonly)
29 30 31 |
# File 'lib/rack/rdf/conneg.rb', line 29 def @options end |
Instance Method Details
#accept_entry(entry) ⇒ Object (protected)
Returns pair of content_type (including non-‘q’ parameters) and array of quality, number of ‘*’ in content-type, and number of non-‘q’ parameters
176 177 178 179 180 181 |
# File 'lib/rack/rdf/conneg.rb', line 176 def accept_entry(entry) type, * = entry.split(';').map(&:strip) quality = 0 # we sort smallest first .delete_if { |e| quality = 1 - e[2..-1].to_f if e.start_with? 'q=' } [.unshift(type).join(';'), [quality, type.count('*'), 1 - .size]] end |
#call(env) ⇒ Array(Integer, Hash, #each)
Handles a Rack protocol request. Parses Accept header to find appropriate mime-type and sets content_type accordingly.
Inserts ordered content types into the environment as ORDERED_CONTENT_TYPES
if an Accept header is present
51 52 53 54 55 56 57 58 59 60 61 |
# File 'lib/rack/rdf/conneg.rb', line 51 def call(env) env['ORDERED_CONTENT_TYPES'] = parse_accept_header(env['HTTP_ACCEPT']) if env.has_key?('HTTP_ACCEPT') response = app.call(env) 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 |
#find_content_type_for_media_range(media_range) ⇒ String? (protected)
Returns a content type appropriate for the given media_range
, returns nil
if media_range
contains a wildcard subtype that is not mapped.
190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 |
# File 'lib/rack/rdf/conneg.rb', line 190 def find_content_type_for_media_range(media_range) case media_range.to_s when '*/*' [:default] when 'text/*' 'text/turtle' when 'application/*' 'application/ld+json' when 'application/json' 'application/ld+json' when 'application/xml' 'application/rdf+xml' when /^([^\/]+)\/\*$/ nil else media_range.to_s end end |
#find_writer(env, headers) {|writer, content_type, accept_params| ... } ⇒ Object (protected)
Yields 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
116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 |
# File 'lib/rack/rdf/conneg.rb', line 116 def find_writer(env, headers) if @options[:format] format = @options[:format] writer = ::RDF::Writer.for(format.to_sym) yield(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| find_writer_for_content_type(content_type) do |writer, ct, accept_params| # Yields content type with parameters yield(writer, ct, accept_params) end end 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([:default]) do |writer, ct| # Yields content type with parameters yield(writer, ct) end end end |
#find_writer_for_content_type(content_type) {|writer, content_type| ... } ⇒ Object (protected)
Yields an RDF::Writer
class for the given content_type
.
Calls Writer#accept?(content_type)
for matched content type to allow writers to further discriminate on how if to accept content-type with specified parameters.
148 149 150 151 152 153 154 155 156 157 158 159 |
# File 'lib/rack/rdf/conneg.rb', line 148 def find_writer_for_content_type(content_type) ct, *params = content_type.split(';').map(&:strip) accept_params = params.inject({}) do |memo, pv| p, v = pv.split('=').map(&:strip) memo.merge(p.downcase.to_sym => v.sub(/^["']?([^"']*)["']?$/, '\1')) end formats = ::RDF::Format.each(content_type: ct, has_writer: true).to_a.reverse formats.each do |format| yield format.writer, (ct || format.content_type.first), accept_params if format.writer.accept?(accept_params) end end |
#http_error(code, message = nil, headers = {}) ⇒ Array(Integer, Hash, #each) (protected)
Outputs an HTTP 4xx
or 5xx
response.
225 226 227 228 |
# File 'lib/rack/rdf/conneg.rb', line 225 def http_error(code, = nil, headers = {}) = http_status(code) + (.nil? ? "\n" : " (#{})\n") [code, {'Content-Type' => "text/plain"}.merge(headers), []] end |
#http_status(code) ⇒ String (protected)
Returns the standard HTTP status message for the given status code
.
235 236 237 |
# File 'lib/rack/rdf/conneg.rb', line 235 def http_status(code) [code, Rack::Utils::HTTP_STATUS_CODES[code]].join(' ') end |
#not_acceptable(message = nil) ⇒ Array(Integer, Hash, #each) (protected)
Outputs an HTTP 406 Not Acceptable
response.
214 215 216 |
# File 'lib/rack/rdf/conneg.rb', line 214 def not_acceptable( = nil) http_error(406, , VARY) end |
#parse_accept_header(header) ⇒ Array<String> (protected)
Parses an HTTP Accept
header, returning an array of MIME content types ordered by the precedence rules defined in HTTP/1.1 §14.1.
168 169 170 171 172 |
# File 'lib/rack/rdf/conneg.rb', line 168 def parse_accept_header(header) entries = header.to_s.split(',') entries = entries.map { |e| accept_entry(e) }.sort_by(&:last).map(&:first) entries.map { |e| find_content_type_for_media_range(e) }.flatten.compact end |
#serialize(env, status, headers, body) ⇒ Array(Integer, Hash, #each)
Serializes an RDF::Enumerable
response into a Rack protocol response using HTTP content negotiation rules or a specified Content-Type.
Passes parameters from Accept header, and Link header to writer.
74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 |
# File 'lib/rack/rdf/conneg.rb', line 74 def serialize(env, status, headers, body) result, content_type = nil, nil find_writer(env, headers) do |writer, ct, accept_params = {}| begin # Passes content_type as writer option to allow parameters to be extracted. = @options.merge( accept_params: accept_params, link: env['HTTP_LINK'] ) result, content_type = writer.dump(body, nil, **), ct.split(';').first break rescue ::RDF::WriterError # Continue to next writer ct rescue ct end end if result headers = headers.merge(VARY).merge('Content-Type' => content_type) [status, headers, [result]] else not_acceptable end end |