Class: Rack::SPARQL::ContentNegotiation

Inherits:
Object
  • Object
show all
Defined in:
vendor/bundler/ruby/3.3.0/bundler/gems/sparql-36baa432eb7f/lib/rack/sparql/conneg.rb

Overview

Rack middleware for SPARQL 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.

This endpoint also serves the fuction of Rack::LinkedData, as it will serialize SPARQL results, which may be RDF Graphs

Constant Summary collapse

VARY =
{'Vary' => 'Accept'}.freeze

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(app, options = {}) ⇒ ContentNegotiation

Returns a new instance of ContentNegotiation.

Parameters:

  • app (#call)
  • options (Hash{Symbol => Object}) (defaults to: {})

    Other options passed to writer.

Options Hash (options):

  • :format (RDF::Format, #to_sym)

    Specific RDF writer format to use



32
33
34
# File 'vendor/bundler/ruby/3.3.0/bundler/gems/sparql-36baa432eb7f/lib/rack/sparql/conneg.rb', line 32

def initialize(app, options = {})
  @app, @options = app, options
end

Instance Attribute Details

#app#call (readonly)

Returns:



21
22
23
# File 'vendor/bundler/ruby/3.3.0/bundler/gems/sparql-36baa432eb7f/lib/rack/sparql/conneg.rb', line 21

def app
  @app
end

#optionsHash{Symbol => Object} (readonly)

Returns:



25
26
27
# File 'vendor/bundler/ruby/3.3.0/bundler/gems/sparql-36baa432eb7f/lib/rack/sparql/conneg.rb', line 25

def options
  @options
end

Instance Method Details

#accept_entry(entry) ⇒ Object (protected)



119
120
121
122
123
124
# File 'vendor/bundler/ruby/3.3.0/bundler/gems/sparql-36baa432eb7f/lib/rack/sparql/conneg.rb', line 119

def accept_entry(entry)
  type, *options = entry.delete(' ').split(';')
  quality = 0 # we sort smallest first
  options.delete_if { |e| quality = 1 - e[2..-1].to_f if e.start_with? 'q=' }
  [type, [quality, type.count('*'), 1 - options.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.

If result is RDF::Literal::Boolean, RDF::Query::Results, or RDF::Enumerable The result is serialized using SPARQL::Results

Inserts ordered content types into the environment as ORDERED_CONTENT_TYPES if an Accept header is present.

Normalizes application/x-www-form-urlencoded to either application/sparql-query or application/sparql-update forms.



49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
# File 'vendor/bundler/ruby/3.3.0/bundler/gems/sparql-36baa432eb7f/lib/rack/sparql/conneg.rb', line 49

def call(env)
  env['ORDERED_CONTENT_TYPES'] = parse_accept_header(env['HTTP_ACCEPT']) if env.has_key?('HTTP_ACCEPT')
  # Normalize application/x-www-form-urlencoded to application/sparql-query or application/sparql-update
  if env['REQUEST_METHOD'] == 'POST' && env.fetch('CONTENT_TYPE', 'application/x-www-form-urlencoded').to_s.start_with?('application/x-www-form-urlencoded')
    content = env['rack.input'].read
    params = Rack::Utils.parse_query(content)
    if query = params.delete('query')
      return [406, {"Content-Type" => "text/plain"}, ["Multiple query parameters"]] unless query.is_a?(String)
      env['rack.input'] = StringIO.new(query)
      env['CONTENT_TYPE'] = 'application/sparql-query'
      env['QUERY_STRING'] = Rack::Utils.build_query(params)
    elsif update = params.delete('update')
      return [406, {"Content-Type" => "text/plain"}, ["Multiple update parameters"]] unless update.is_a?(String)
      env['rack.input'] = StringIO.new(update)
      env['CONTENT_TYPE'] = 'application/sparql-update'
      env['QUERY_STRING'] = Rack::Utils.build_query(params)
    else
      env['rack.input'].rewind # never mind
    end
  end
  response = app.call(env)
  body = response[2].respond_to?(:body) ? response[2].body : response[2]
  body = body.first if body.is_a?(Array) && body.length == 1 && body.first.is_a?(RDF::Literal::Boolean)
  case body
  when RDF::Enumerable, RDF::Query::Solutions, RDF::Literal::Boolean
    response[2] = body  # Put it back in the response, it might have been a proxy
    serialize(env, *response)
  else response
  end
end

#http_error(code, message = nil, headers = {}) ⇒ Array(Integer, Hash, #each) (protected)

Outputs an HTTP 4xx or 5xx response.

Parameters:

Returns:



142
143
144
145
# File 'vendor/bundler/ruby/3.3.0/bundler/gems/sparql-36baa432eb7f/lib/rack/sparql/conneg.rb', line 142

def http_error(code, message = nil, headers = {})
  message = http_status(code) + (message.nil? ? "\n" : " (#{message})\n")
  [code, {'Content-Type' => 'text/plain; charset=utf-8'}.merge(headers), [message]]
end

#http_status(code) ⇒ String (protected)

Returns the standard HTTP status message for the given status code.

Parameters:

Returns:



152
153
154
# File 'vendor/bundler/ruby/3.3.0/bundler/gems/sparql-36baa432eb7f/lib/rack/sparql/conneg.rb', line 152

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.

Parameters:

  • message (String, #to_s) (defaults to: nil)

Returns:



131
132
133
# File 'vendor/bundler/ruby/3.3.0/bundler/gems/sparql-36baa432eb7f/lib/rack/sparql/conneg.rb', line 131

def not_acceptable(message = nil)
  http_error(406, message, 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 Section 14.1.

Parameters:

Returns:

See Also:



114
115
116
117
# File 'vendor/bundler/ruby/3.3.0/bundler/gems/sparql-36baa432eb7f/lib/rack/sparql/conneg.rb', line 114

def parse_accept_header(header)
  entries = header.to_s.split(',')
  entries.map { |e| accept_entry(e) }.sort_by(&:last).map(&:first)
end

#serialize(env, status, headers, body) ⇒ Array(Integer, Hash, #each)

Serializes a SPARQL query result into a Rack protocol response using HTTP content negotiation rules or a specified Content-Type.

Parameters:

Returns:

Raises:



90
91
92
93
94
95
96
97
98
99
100
101
102
103
# File 'vendor/bundler/ruby/3.3.0/bundler/gems/sparql-36baa432eb7f/lib/rack/sparql/conneg.rb', line 90

def serialize(env, status, headers, body)
  begin
    serialize_options = {}
    serialize_options[:content_types] = env['ORDERED_CONTENT_TYPES'] if env['ORDERED_CONTENT_TYPES']
    serialize_options.merge!(@options)
    results = ::SPARQL.serialize_results(body, **serialize_options)
    raise RDF::WriterError, "can't serialize results" unless results
    headers = headers.merge(VARY).merge('Content-Type' => results.content_type) # FIXME: don't overwrite existing Vary headers
    [status, headers, [results]]
  rescue RDF::WriterError => e
    # Use this instead of not_acceptable so that headers are not lost.
    http_error(406, e.message, headers.merge(VARY))
  end
end