diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8e64b94..fda1fd4 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -6,7 +6,7 @@ Community contributions are essential for keeping Ruby RDF great. We want to kee This repository uses [Git Flow](https://github.com/nvie/gitflow) to manage development and release activity. All submissions _must_ be on a feature branch based on the _develop_ branch to ease staging and integration. -* create or respond to an issue on the [Github Repository](http://github.com/ruby-rdf/rack-linkeddata/issues) +* create or respond to an issue on the [Github Repository](https://github.com/ruby-rdf/rack-linkeddata/issues) * Fork and clone the repo: `git clone git@github.com:your-username/rack-linkeddata.git` * Install bundle: @@ -30,7 +30,7 @@ This repository uses [Git Flow](https://github.com/nvie/gitflow) to manage devel of thumb, additions larger than about 15 lines of code), we need an explicit [public domain dedication][PDD] on record from you. -[YARD]: http://yardoc.org/ -[YARD-GS]: http://rubydoc.info/docs/yard/file/docs/GettingStarted.md -[PDD]: http://lists.w3.org/Archives/Public/public-rdf-ruby/2010May/0013.html +[YARD]: https://yardoc.org/ +[YARD-GS]: https://rubydoc.info/docs/yard/file/docs/GettingStarted.md +[PDD]: https://lists.w3.org/Archives/Public/public-rdf-ruby/2010May/0013.html [pr]: https://github.com/ruby-rdf/rack-linkeddata/compare/ diff --git a/Gemfile b/Gemfile index 160bc52..d47b416 100644 --- a/Gemfile +++ b/Gemfile @@ -6,6 +6,7 @@ gem "linkeddata", git: "https://github.com/ruby-rdf/linkeddata", gem 'json-ld', git: "https://github.com/ruby-rdf/json-ld", branch: "develop" gem 'json-ld-preloaded', git: "https://github.com/ruby-rdf/json-ld-preloaded", branch: "develop" gem "ld-patch", git: "https://github.com/ruby-rdf/ld-patch", branch: "develop" +gem "rack-rdf", git: "https://github.com/ruby-rdf/rack-rdf", branch: "develop" gem "rdf", git: "https://github.com/ruby-rdf/rdf", branch: "develop" gem 'rdf-aggregate-repo', git: "https://github.com/ruby-rdf/rdf-aggregate-repo", branch: "develop" gem 'rdf-isomorphic', git: "https://github.com/ruby-rdf/rdf-isomorphic", branch: "develop" diff --git a/README.md b/README.md index de3f7a9..eac43fc 100755 --- a/README.md +++ b/README.md @@ -5,15 +5,15 @@ negotiation for Rack applications. You can use `Rack::LinkedData` with any Ruby web framework based on Rack, including with Ruby on Rails 3.0 and with Sinatra. -* +* -[![Gem Version](https://badge.fury.io/rb/rack-linkeddata.png)](http://badge.fury.io/rb/rack-linkeddata) -[![Build Status](https://travis-ci.org/ruby-rdf/rack-linkeddata.png?branch=master)](http://travis-ci.org/ruby-rdf/rack-linkeddata) +[![Gem Version](https://badge.fury.io/rb/rack-linkeddata.png)](https://badge.fury.io/rb/rack-linkeddata) +[![Build Status](https://travis-ci.org/ruby-rdf/rack-linkeddata.png?branch=master)](https://travis-ci.org/ruby-rdf/rack-linkeddata) ## Features * Implements [HTTP content negotiation][conneg] for RDF content types. -* Supports all [RDF.rb][]-compatible serialization formats. +* Supports all [RDF.rb][] compatible serialization formats. * Compatible with any Rack application and any Rack-based framework. ## Examples @@ -91,19 +91,19 @@ for N-Triples, N-Quads, Turtle, RDF/XML, RDF/JSON, JSON-LD, RDFa, TriG and TriX. ##Documentation - + * {Rack::LinkedData} * {Rack::LinkedData::ContentNegotiation} ## Dependencies -* [Rack](http://rubygems.org/gems/rack) (~> 2.0) -* [Linked Data](http://rubygems.org/gems/linkeddata) (~> 3.1) +* [Rack](https://rubygems.org/gems/rack) (~> 2.0) +* [Linked Data](https://rubygems.org/gems/linkeddata) (~> 3.1) ## Installation -The recommended installation method is via [RubyGems](http://rubygems.org/). +The recommended installation method is via [RubyGems](https://rubygems.org/). To install the latest official release of the gem, do: % [sudo] gem install rack-linkeddata @@ -117,7 +117,7 @@ To get a local working copy of the development repository, do: Alternatively, download the latest development version as a tarball as follows: - % wget http://github.com/ruby-rdf/rack-linkeddata/tarball/master + % wget https://github.com/ruby-rdf/rack-linkeddata/tarball/master ## Contributing This repository uses [Git Flow](https://github.com/nvie/gitflow) to mange development and release activity. All submissions _must_ be on a feature branch based on the _develop_ branch to ease staging and integration. @@ -136,27 +136,27 @@ This repository uses [Git Flow](https://github.com/nvie/gitflow) to mange develo ## References -* -* -* -* -* -* +* +* +* +* +* +* ## Authors -* [Arto Bendiken](http://github.com/bendiken) - -* [Gregg Kellogg](http://github.com/gkellogg) - +* [Arto Bendiken](https://github.com/artob) - +* [Gregg Kellogg](https://github.com/gkellogg) - ## License This is free and unencumbered public domain software. For more information, -see or the accompanying {file:UNLICENSE} file. - -[Rack]: http://rack.github.com/ -[RDF.rb]: http://ruby-rdf.github.com/rdf/ -[Linked Data]: http://linkeddata.org/ -[conneg]: http://en.wikipedia.org/wiki/Content_negotiation -[YARD]: http://yardoc.org/ -[YARD-GS]: http://rubydoc.info/docs/yard/file/docs/GettingStarted.md -[PDD]: http://unlicense.org/#unlicensing-contributions +see or the accompanying {file:UNLICENSE} file. + +[Rack]: https://rack.github.com/ +[RDF.rb]: https://ruby-rdf.github.com/rdf/ +[Linked Data]: https://linkeddata.org/ +[conneg]: https://en.wikipedia.org/wiki/Content_negotiation +[YARD]: https://yardoc.org/ +[YARD-GS]: https://rubydoc.info/docs/yard/file/docs/GettingStarted.md +[PDD]: https://unlicense.org/#unlicensing-contributions diff --git a/UNLICENSE b/UNLICENSE index 68a49da..efb9808 100644 --- a/UNLICENSE +++ b/UNLICENSE @@ -21,4 +21,4 @@ OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -For more information, please refer to +For more information, please refer to diff --git a/VERSION b/VERSION index fd2a018..94ff29c 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -3.1.0 +3.1.1 diff --git a/lib/rack/linkeddata.rb b/lib/rack/linkeddata.rb index 4efd3e5..a8f2ed2 100644 --- a/lib/rack/linkeddata.rb +++ b/lib/rack/linkeddata.rb @@ -1,4 +1,4 @@ -require 'rack' +require 'rack/rdf' require 'linkeddata' module Rack @@ -9,22 +9,11 @@ module LinkedData ## # Registers all known RDF formats with Rack's MIME types registry. # - # @param [Boolean] :overwrite (false) + # @param [Boolean] overwrite (false) # @param [Hash{Symbol => Object}] options # @return [void] def self.register_mime_types!(overwrite: false, **options) - if defined?(Rack::Mime::MIME_TYPES) - RDF::Format.each do |format| - if !Rack::Mime::MIME_TYPES.has_key?(file_ext = ".#{format.to_sym}") || overwrite - Rack::Mime::MIME_TYPES.merge!(file_ext => format.content_type.first) - end - end - RDF::Format.file_extensions.each do |file_ext, formats| - if !Rack::Mime::MIME_TYPES.has_key?(file_ext = ".#{file_ext}") || overwrite - Rack::Mime::MIME_TYPES.merge!(file_ext => formats.first.content_type.first) - end - end - end + Rack::RDF.register_mime_types!(overwrite: overwrite, **options) end end end diff --git a/lib/rack/linkeddata/conneg.rb b/lib/rack/linkeddata/conneg.rb index 2d2a289..1044f2d 100644 --- a/lib/rack/linkeddata/conneg.rb +++ b/lib/rack/linkeddata/conneg.rb @@ -6,7 +6,7 @@ module Rack; module LinkedData # format to serialize any result with a body being `RDF::Enumerable`. # # Override content negotiation by setting the :format option to - # {#initialize}. + # `#initialize`. # # Add a :default option to set a content type to use when nothing else # is found. @@ -18,222 +18,6 @@ module Rack; module LinkedData # # @see http://www4.wiwiss.fu-berlin.de/bizer/pub/LinkedDataTutorial/ # @see https://www.rubydoc.info/github/rack/rack/master/file/SPEC - class ContentNegotiation - DEFAULT_CONTENT_TYPE = "application/n-triples" # N-Triples - VARY = {'Vary' => 'Accept'}.freeze - - # @return [#call] - attr_reader :app - - # @return [Hash{Symbol => Object}] - attr_reader :options - - ## - # @param [#call] app - # @param [Hash{Symbol => Object}] options - # Other options passed to writer. - # @param [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 http://rack.rubyforge.org/doc/SPEC.html - 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 - - ## - # 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. - # - # @param [Hash{String => String}] env - # @param [Integer] status - # @param [Hash{String => Object}] headers - # @param [RDF::Enumerable] body - # @return [Array(Integer, Hash, #each)] Status, Headers and Body - 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. - writer_options = @options.merge( - accept_params: accept_params, - link: env['HTTP_LINK'] - ) - result, content_type = writer.dump(body, nil, **writer_options), 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 - - 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 - # - # @param [Hash{String => String}] env - # @param [Hash{String => Object}] headers - # @yield |writer, content_type| - # @yield_param [RDF::Writer] writer - # @yield_param [String] content_type from accept media-range without parameters - # @yield_param [Hash{Symbol => String}] accept_params from accept media-range - # @see http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html - 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(options[:default]) do |writer, ct| - # Yields content type with parameters - yield(writer, ct) - end - end - end - - ## - # 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. - # - # @param [String, #to_s] content_type - # @yield |writer, content_type| - # @yield_param [RDF::Writer] writer - # @yield_param [String] content_type (including media-type parameters) - 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 - - ## - # Parses an HTTP `Accept` header, returning an array of MIME content - # types ordered by the precedence rules defined in HTTP/1.1 §14.1. - # - # @param [String, #to_s] header - # @return [Array] - # @see http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.1 - 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 - end - - # Returns pair of content_type (including non-'q' parameters) - # and array of quality, number of '*' in content-type, and number of non-'q' parameters - def accept_entry(entry) - type, *options = entry.split(';').map(&:strip) - quality = 0 # we sort smallest first - options.delete_if { |e| quality = 1 - e[2..-1].to_f if e.start_with? 'q=' } - [options.unshift(type).join(';'), [quality, type.count('*'), 1 - options.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 '*/*' - options[: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 - - ## - # Outputs an HTTP `406 Not Acceptable` response. - # - # @param [String, #to_s] message - # @return [Array(Integer, Hash, #each)] - def not_acceptable(message = nil) - http_error(406, message, VARY) - end - - ## - # Outputs an HTTP `4xx` or `5xx` response. - # - # @param [Integer, #to_i] code - # @param [String, #to_s] message - # @param [Hash{String => String}] headers - # @return [Array(Integer, Hash, #each)] - def http_error(code, message = nil, headers = {}) - message = http_status(code) + (message.nil? ? "\n" : " (#{message})\n") - [code, {'Content-Type' => "text/plain"}.merge(headers), [message]] - end - - ## - # Returns the standard HTTP status message for the given status `code`. - # - # @param [Integer, #to_i] code - # @return [String] - def http_status(code) - [code, Rack::Utils::HTTP_STATUS_CODES[code]].join(' ') - end + class ContentNegotiation < Rack::RDF::ContentNegotiation end # class ContentNegotiation end; end # module Rack::LinkedData diff --git a/rack-linkeddata.gemspec b/rack-linkeddata.gemspec index 7f8bd01..087d9d8 100755 --- a/rack-linkeddata.gemspec +++ b/rack-linkeddata.gemspec @@ -6,7 +6,7 @@ Gem::Specification.new do |gem| gem.date = File.mtime('VERSION').strftime('%Y-%m-%d') gem.name = 'rack-linkeddata' - gem.homepage = 'http://ruby-rdf.github.com/rack-linkeddata' + gem.homepage = 'https://github.com/ruby-rdf/rack-linkeddata' gem.license = 'Unlicense' gem.summary = 'Linked Data content negotiation for Rack applications.' gem.description = 'Rack middleware for Linked Data content negotiation.' @@ -20,11 +20,11 @@ Gem::Specification.new do |gem| gem.required_ruby_version = '>= 2.4' gem.requirements = [] - gem.add_runtime_dependency 'linkeddata', '~> 3.1' - gem.add_runtime_dependency 'rdf', '~> 3.1' - gem.add_runtime_dependency 'rack', '~> 2.0' + gem.add_runtime_dependency 'linkeddata', '~> 3.1', '>= 3.1.2' + gem.add_runtime_dependency 'rack-rdf', '~> 3.1' + gem.add_runtime_dependency 'rack', '~> 2.1' - gem.add_development_dependency 'yard' , '~> 0.9.20' + gem.add_development_dependency 'yard' , '~> 0.9' gem.add_development_dependency 'rspec', '~> 3.9' gem.add_development_dependency 'rack-test', '~> 1.1' gem.post_install_message = nil