Class: JSON::LD::ContentNegotiation
- Inherits:
-
Object
- Object
- JSON::LD::ContentNegotiation
- Defined in:
- lib/json/ld/conneg.rb
Overview
Rack middleware for JSON-LD content negotiation.
Uses HTTP Content Negotiation to serialize Array
and Hash
results as JSON-LD using ‘profile’ accept-params to invoke appropriate JSON-LD API methods.
Allows black-listing and white-listing of two-part profiles where the second part denotes a URL of a context or frame. (See Writer.accept?)
Works along with rack-linkeddata
for serializing data which is not in the form of an RDF::Repository
.
Constant Summary collapse
- VARY =
{ 'Vary' => 'Accept' }.freeze
Instance Attribute Summary collapse
- #app ⇒ #call readonly
Class Method Summary collapse
-
.registered(app)
-
Registers JSON::LD::Rack, suitable for Sinatra application * adds helpers.
-
Instance Method Summary collapse
-
#accept_entry(entry) ⇒ Object
protected
Returns an 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. -
#http_error(code, message = nil, headers = {}) ⇒ Array(Integer, Hash, #each)
protected
Outputs an HTTP
4xx
or5xx
response. -
#initialize(app) ⇒ 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 objects as JSON-LD.
Constructor Details
#initialize(app) ⇒ ContentNegotiation
Returns a new instance of ContentNegotiation.
42 43 44 |
# File 'lib/json/ld/conneg.rb', line 42 def initialize(app) @app = app end |
Instance Attribute Details
Class Method Details
Instance Method Details
#accept_entry(entry) ⇒ Object (protected)
Returns an array of quality, number of ‘*’ in content-type, and number of non-‘q’ parameters
155 156 157 158 159 160 |
# File 'lib/json/ld/conneg.rb', line 155 def accept_entry(entry) type, * = entry.split(';').map(&:strip) quality = 0 # we sort smallest first .delete_if { |e| quality = 1 - e[2..].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.
53 54 55 56 57 58 59 60 61 62 |
# File 'lib/json/ld/conneg.rb', line 53 def call(env) response = app.call(env) body = response[2].respond_to?(:body) ? response[2].body : response[2] case body when Array, Hash 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.
169 170 171 172 173 174 175 176 177 178 179 180 181 |
# File 'lib/json/ld/conneg.rb', line 169 def find_content_type_for_media_range(media_range) media_range = media_range.sub('*/*', 'application/ld+json') if media_range.to_s.start_with?('*/*') if media_range.to_s.start_with?('application/*') media_range = media_range.sub('application/*', 'application/ld+json') end if media_range.to_s.start_with?('application/json') media_range = media_range.sub('application/json', 'application/ld+json') end media_range.start_with?('application/ld+json') ? media_range : nil end |
#http_error(code, message = nil, headers = {}) ⇒ Array(Integer, Hash, #each) (protected)
Outputs an HTTP 4xx
or 5xx
response.
199 200 201 202 203 |
# File 'lib/json/ld/conneg.rb', line 199 def http_error(code, = nil, headers = {}) = [code, Rack::Utils::HTTP_STATUS_CODES[code]].join(' ') + (.nil? ? "\n" : " (#{})\n") [code, { 'Content-Type' => "text/plain" }.merge(headers), []] end |
#not_acceptable(message = nil) ⇒ Array(Integer, Hash, #each) (protected)
Outputs an HTTP 406 Not Acceptable
response.
188 189 190 |
# File 'lib/json/ld/conneg.rb', line 188 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.
145 146 147 148 149 150 151 152 |
# File 'lib/json/ld/conneg.rb', line 145 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) }.compact end |
#serialize(env, status, headers, body) ⇒ Array(Integer, Hash, #each)
Serializes objects as JSON-LD. Defaults to expanded form, other forms determined by presense of profile
in accept-parms.
73 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 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 |
# File 'lib/json/ld/conneg.rb', line 73 def serialize(env, status, headers, body) # This will only return json-ld content types, possibly with parameters content_types = parse_accept_header(env['HTTP_ACCEPT'] || 'application/ld+json') content_types = content_types.select do |content_type| _, *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 JSON::LD::Writer.accept?(accept_params) end if content_types.empty? not_acceptable("No appropriate combinaion of media-type and parameters found") else ct, *params = content_types.first.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 # Determine API method from profile profile = accept_params[:profile].to_s.split # Get context from Link header links = LinkHeader.parse(env['HTTP_LINK']) context = begin links.find_link(['rel', JSON_LD_NS + "context"]).href rescue StandardError nil end frame = begin links.find_link(['rel', JSON_LD_NS + "frame"]).href rescue StandardError nil end if profile.include?(JSON_LD_NS + "framed") && frame.nil? return not_acceptable("framed profile without a frame") end # accept? already determined that there are appropriate contexts # If profile also includes a URI which is not a namespace, use it for compaction. context ||= Writer.default_context if profile.include?(JSON_LD_NS + "compacted") result = if profile.include?(JSON_LD_NS + "flattened") API.flatten(body, context) elsif profile.include?(JSON_LD_NS + "framed") API.frame(body, frame) elsif context API.compact(body, context) elsif profile.include?(JSON_LD_NS + "expanded") API.(body) else body end headers = headers.merge(VARY).merge('Content-Type' => ct) [status, headers, [result.to_json]] end rescue StandardError http_error(500, $ERROR_INFO.) end |