Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature: URI drerfrencement content negotiation #72

Merged
merged 25 commits into from
Mar 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
9d69654
remove useless line preventing sending the reset password email (#65)
syphax-bouazzouni Jan 26, 2024
59bfba6
[ontoportal-bot] Gemfile.lock update
ontoportal-bot-lirmm Feb 7, 2024
fe2563e
Feature: api endpoint returns json-ld for the element with that URI
imadbourouche Feb 7, 2024
bbf3aff
implement GET, POST requests, and GET /parse to submit INRATHES ontology
imadbourouche Feb 22, 2024
0949021
Enhance tests using real data submission
imadbourouche Feb 22, 2024
471df33
Enhance bin/ontoportal to make it able to run localy with UI
imadbourouche Feb 22, 2024
c146019
Small fixes
imadbourouche Feb 22, 2024
78dc9b1
Merge branch 'development' into feature/uri-derefencing
syphax-bouazzouni Mar 8, 2024
10407f9
Merge remote-tracking branch 'origin/development' into feature/uri-de…
imadbourouche Mar 8, 2024
db9c9f4
Fix test dereference resource controller
imadbourouche Mar 11, 2024
403e98a
update gemfile: add json-ld (3.0.2)
imadbourouche Mar 11, 2024
7855123
Merge branch 'feature/uri-derefencing' of github.com:ontoportal-lirmm…
syphax-bouazzouni Mar 11, 2024
e78628b
change derefrencement namespacing and clean code
syphax-bouazzouni Mar 11, 2024
d979ac8
Fix dereference resource tests expected resultsto handle parse triples
imadbourouche Mar 11, 2024
d9e1198
Feature: add content negotiation middleware
imadbourouche Mar 11, 2024
1b1a62d
Add headers to tests instead of output_format
imadbourouche Mar 11, 2024
43af678
Apply middleware to only /ontologies/:acronym/resolve/:uri
imadbourouche Mar 12, 2024
e53ba84
Add test cases for AllegroGraph and fix xml test
imadbourouche Mar 12, 2024
2d49418
Merge remote-tracking branch 'origin/development' into feature/conten…
syphax-bouazzouni Mar 16, 2024
5c845bb
move the content_negotiation middleware into rack folder and module
syphax-bouazzouni Mar 16, 2024
14cbd56
re-implement again the usage of the output_format param if no format…
syphax-bouazzouni Mar 16, 2024
c9b5f7b
clean the tests for no more necessary checks
syphax-bouazzouni Mar 16, 2024
9080e22
clean and simplify the content negotiation middleware
syphax-bouazzouni Mar 16, 2024
b7f337b
add the accepted format in the error response of resolvability endpoint
syphax-bouazzouni Mar 16, 2024
fd3f9ea
refactor the content negotiation middleware code to be more clear
syphax-bouazzouni Mar 16, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@
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 @@

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"

Check warning on line 42 in controllers/dereference_resource_controller.rb

View check run for this annotation

Codecov / codecov/patch

controllers/dereference_resource_controller.rb#L42

Added line #L42 was not covered by tests
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

Check warning on line 41 in lib/rack/content_negotiation.rb

View check run for this annotation

Codecov / codecov/patch

lib/rack/content_negotiation.rb#L41

Added line #L41 was not covered by tests
end
else
env["format"] = options[:default]
add_content_type_header(app.call(env), env["format"])

Check warning on line 45 in lib/rack/content_negotiation.rb

View check run for this annotation

Codecov / codecov/patch

lib/rack/content_negotiation.rb#L44-L45

Added lines #L44 - L45 were not covered by tests
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

Check warning on line 81 in lib/rack/content_negotiation.rb

View check run for this annotation

Codecov / codecov/patch

lib/rack/content_negotiation.rb#L79-L81

Added lines #L79 - L81 were not covered by tests
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

Check warning on line 97 in lib/rack/content_negotiation.rb

View check run for this annotation

Codecov / codecov/patch

lib/rack/content_negotiation.rb#L97

Added line #L97 was not covered by tests
when '*/*', 'text/*'
options[:default]

Check warning on line 99 in lib/rack/content_negotiation.rb

View check run for this annotation

Codecov / codecov/patch

lib/rack/content_negotiation.rb#L99

Added line #L99 was not covered by tests
when 'application/n-triples'
'application/n-triples'

Check warning on line 101 in lib/rack/content_negotiation.rb

View check run for this annotation

Codecov / codecov/patch

lib/rack/content_negotiation.rb#L101

Added line #L101 was not covered by tests
when 'text/turtle'
'text/turtle'

Check warning on line 103 in lib/rack/content_negotiation.rb

View check run for this annotation

Codecov / codecov/patch

lib/rack/content_negotiation.rb#L103

Added line #L103 was not covered by tests
when 'application/json', 'application/ld+json', 'application/*'
'application/ld+json'

Check warning on line 105 in lib/rack/content_negotiation.rb

View check run for this annotation

Codecov / codecov/patch

lib/rack/content_negotiation.rb#L105

Added line #L105 was not covered by tests
when 'text/xml', 'text/rdf+xml', 'application/rdf+xml', 'application/xml'
'application/rdf+xml'

Check warning on line 107 in lib/rack/content_negotiation.rb

View check run for this annotation

Codecov / codecov/patch

lib/rack/content_negotiation.rb#L107

Added line #L107 was not covered by tests
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]]

Check warning on line 122 in lib/rack/content_negotiation.rb

View check run for this annotation

Codecov / codecov/patch

lib/rack/content_negotiation.rb#L119-L122

Added lines #L119 - L122 were not covered by tests
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
Loading