Skip to content

Commit

Permalink
Feature: URI drerfrencement content negotiation (#72)
Browse files Browse the repository at this point in the history
* remove useless line preventing sending the reset password email (#65)

* [ontoportal-bot] Gemfile.lock update

* Feature: api endpoint returns json-ld  for the element with that URI

* implement GET, POST requests, and GET /parse to submit INRATHES ontology

* Enhance tests using real data submission

* Enhance bin/ontoportal to make it able to run localy with UI

* Small fixes

- change controller name and test controller name
- remove /parse endpoint
- rackup to shotgun in bin/ontoportal

* Fix test dereference resource controller

- in json test, before we test the result we sort the hashes with the function (sort_nested_hash)
- in xml, ntriples and turtle, we split the result and the expected result, sort them and compare them

* update gemfile: add json-ld (3.0.2)

* change derefrencement namespacing and clean code

* Fix dereference resource tests expected resultsto handle parse triples

* Feature: add content negotiation middleware

* Add headers to tests instead of output_format

* Apply middleware to only /ontologies/:acronym/resolve/:uri

* Add test cases for AllegroGraph and fix xml test

* move the content_negotiation middleware into rack folder and  module

* re-implement again the usage of  the output_format param if no format is given in the request header

* clean the tests for no more necessary checks

* clean and simplify the content negotiation middleware

* add the accepted format in the error response of resolvability endpoint

* refactor the content negotiation middleware code to be more clear

---------

Co-authored-by: Syphax bouazzouni <gs_bouazzouni@esi.dz>
Co-authored-by: OntoPortal Bot <ontoportal.bot.lirmm@gmail.com>
  • Loading branch information
3 people authored Mar 18, 2024
1 parent e4eef92 commit ccf4f77
Show file tree
Hide file tree
Showing 3 changed files with 155 additions and 24 deletions.
30 changes: 13 additions & 17 deletions controllers/dereference_resource_controller.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
require_relative '../test/test_case'

use Rack::ContentNegotiation

class DereferenceResourceController < ApplicationController
namespace "/ontologies" do
Expand All @@ -11,7 +12,8 @@ class DereferenceResourceController < ApplicationController
error 500, "Usage: ontologies/:acronym/resolve/:uri?output_format= OR POST: acronym, uri, output_format parameters"
end

output_format = params[:output_format].presence || 'jsonld'
output_format = env["format"].presence || params[:output_format].presence || 'application/n-triples'

process_request(acronym, uri, output_format)
end

Expand All @@ -28,25 +30,19 @@ def process_request(acronym_param, uri_param, output_format)

r = Resource.new(sub.id, uri)
case output_format
when 'jsonld'
content_type 'application/json'
reply JSON.parse(r.to_json)
when 'json'
content_type 'application/json'
reply JSON.parse(r.to_json)
when 'xml'
content_type 'application/xml'
reply r.to_xml
when 'turtle'
content_type 'text/turtle'
reply r.to_turtle
when 'ntriples'
content_type 'application/n-triples'
reply r.to_ntriples
when 'application/ld+json', 'application/json'
r.to_json
when 'application/rdf+xml', 'application/xml'
r.to_xml
when 'text/turtle'
r.to_turtle
when 'application/n-triples'
r.to_ntriples
else
error 500, "Invalid output format"
error 500, "Invalid output format, valid format are: application/json, application/ld+json, application/xml, application/rdf+xml, text/turtle and application/n-triples"
end


end

def valid_url?(url)
Expand Down
131 changes: 131 additions & 0 deletions lib/rack/content_negotiation.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
module Rack
class ContentNegotiation
DEFAULT_CONTENT_TYPE = "application/n-triples" # N-Triples
VARY = { 'Vary' => 'Accept' }.freeze
ENDPOINTS_FILTER = %r{^/ontologies/[^/]+/resolve/[^/]+$} # Accepted API endpoints to apply content negotiation

# @return [#call]
attr_reader :app

# @return [Hash{Symbol => String}]
attr_reader :options

##
# @param [#call] app
# @param [Hash{Symbol => Object}] options
# 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
@options[:default] = (@options[:default] || DEFAULT_CONTENT_TYPE).to_s
end

##
# 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
#
# @param [Hash{String => String}] env
# @return [Array(Integer, Hash, #each)] Status, Headers and Body
# @see https://rubydoc.info/github/rack/rack/file/SPEC
def call(env)
if env['PATH_INFO'].match?(ENDPOINTS_FILTER)
if env.has_key?('HTTP_ACCEPT')
accepted_types = parse_accept_header(env['HTTP_ACCEPT'])
if !accepted_types.empty?
env["format"] = accepted_types.first
add_content_type_header(app.call(env), env["format"])
else
not_acceptable
end
else
env["format"] = options[:default]
add_content_type_header(app.call(env), env["format"])
end
else
app.call(env)
end
end

protected

# Parses an HTTP `Accept` header, returning an array of MIME content types ordered by precedence rules.
#
# @param [String, #to_s] header
# @return [Array<String>] Array of content types sorted by precedence
# @see https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.1
def parse_accept_header(header)
entries = header.to_s.split(',')
parsed_entries = entries.map { |entry| parse_accept_entry(entry) }
sorted_entries = parsed_entries.sort_by { |entry| entry.quality }.reverse
content_types = sorted_entries.map { |entry| entry.content_type }
content_types.flatten.compact
end



# Parses an individual entry from the Accept header.
#
# @param [String] entry An entry from the Accept header
# @return [Entry] An object representing the parsed entry
def parse_accept_entry(entry)
# Represents an entry parsed from the Accept header
entry_struct = Struct.new(:content_type, :quality, :wildcard_count, :param_count)
content_type, *params = entry.split(';').map(&:strip)
quality = 1.0 # Default quality
params.reject! do |param|
if param.start_with?('q=')
quality = param[2..-1].to_f
true
end
end
wildcard_count = content_type.count('*')
entry_struct.new(content_type, quality, wildcard_count, params.size)
end


##
# Returns a content type appropriate for the given `media_range`,
# returns `nil` if `media_range` contains a wildcard subtype
# that is not mapped.
#
# @param [String, #to_s] media_range
# @return [String, nil]
def find_content_type_for_media_range(media_range)
case media_range.to_s
when '*/*', 'text/*'
options[:default]
when 'application/n-triples'
'application/n-triples'
when 'text/turtle'
'text/turtle'
when 'application/json', 'application/ld+json', 'application/*'
'application/ld+json'
when 'text/xml', 'text/rdf+xml', 'application/rdf+xml', 'application/xml'
'application/rdf+xml'
else
nil
end
end

##
# Outputs an HTTP `406 Not Acceptable` response.
#
# @param [String, #to_s] message
# @return [Array(Integer, Hash, #each)]
def not_acceptable(message = nil)
code = 406
http_status = [code, Rack::Utils::HTTP_STATUS_CODES[code]].join(' ')
message = http_status + (message.nil? ? "\n" : " (#{message})\n")
[code, { 'Content-Type' => "text/plain" }.merge(VARY), [message]]
end

def add_content_type_header(response, type)
response[1] = response[1].merge(VARY).merge('Content-Type' => type)
response
end

end
end
18 changes: 11 additions & 7 deletions test/controllers/test_dereference_resource_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ def self.before_suite
end

def test_dereference_resource_controller_json
get "/ontologies/#{@@graph}/resolve/#{@@uri}?output_format=json"
header 'Accept', 'application/json'
get "/ontologies/#{@@graph}/resolve/#{@@uri}"
assert last_response.ok?

result = last_response.body
Expand Down Expand Up @@ -73,7 +74,8 @@ def test_dereference_resource_controller_json
end

def test_dereference_resource_controller_xml
get "/ontologies/#{@@graph}/resolve/#{@@uri}?output_format=xml"
header 'Accept', 'application/xml'
get "/ontologies/#{@@graph}/resolve/#{@@uri}"
assert last_response.ok?

result = last_response.body
Expand Down Expand Up @@ -124,7 +126,8 @@ def test_dereference_resource_controller_xml
end

def test_dereference_resource_controller_ntriples
get "/ontologies/#{@@graph}/resolve/#{@@uri}?output_format=ntriples"
header 'Accept', 'application/n-triples'
get "/ontologies/#{@@graph}/resolve/#{@@uri}"
assert last_response.ok?

result = last_response.body
Expand All @@ -135,16 +138,17 @@ def test_dereference_resource_controller_ntriples
<http://opendata.inrae.fr/thesaurusINRAE/c_6496> <http://www.w3.org/2004/02/skos/core#topConceptOf> <http://opendata.inrae.fr/thesaurusINRAE/mt_65> .
<http://opendata.inrae.fr/thesaurusINRAE/c_6496> <http://www.w3.org/2004/02/skos/core#inScheme> <http://opendata.inrae.fr/thesaurusINRAE/thesaurusINRAE> .
<http://opendata.inrae.fr/thesaurusINRAE/c_6496> <http://www.w3.org/2004/02/skos/core#inScheme> <http://opendata.inrae.fr/thesaurusINRAE/mt_65> .
<http://opendata.inrae.fr/thesaurusINRAE/c_6496> <http://www.w3.org/2004/02/skos/core#prefLabel> "alt\\\\u00E9rationdel'ADN"@fr .
<http://opendata.inrae.fr/thesaurusINRAE/c_6496> <http://www.w3.org/2004/02/skos/core#prefLabel> "alt\\u00E9rationdel'ADN"@fr .
<http://opendata.inrae.fr/thesaurusINRAE/mt_65> <http://www.w3.org/2004/02/skos/core#hasTopConcept> <http://opendata.inrae.fr/thesaurusINRAE/c_6496> .
NTRIPLES
a = result.gsub('\\"', '"').gsub(' ', '')[1..-2].split("\\n").reject(&:empty?)
a = result.gsub(' ', '').split("\n").reject(&:empty?)
b = expected_result.gsub(' ', '').split("\n").reject(&:empty?)
assert_equal b.sort, a.sort
end

def test_dereference_resource_controller_turtle
get "/ontologies/#{@@graph}/resolve/#{@@uri}?output_format=turtle"
header 'Accept', 'text/turtle'
get "/ontologies/#{@@graph}/resolve/#{@@uri}"
assert last_response.ok?

result = last_response.body
Expand All @@ -164,7 +168,7 @@ def test_dereference_resource_controller_turtle
ns0:mt_65
skos:hasTopConcept ns0:c_6496 .
TURTLE
a = result.gsub('\\"', '"').gsub(' ', '')[1..-2].split("\\n").reject(&:empty?)
a = result.gsub(' ', '').split("\n").reject(&:empty?)
b = expected_result.gsub(' ', '').split("\n").reject(&:empty?)

assert_equal b.sort, a.sort
Expand Down

0 comments on commit ccf4f77

Please sign in to comment.