Skip to content

Commit

Permalink
Finish 3.1.1
Browse files Browse the repository at this point in the history
  • Loading branch information
gkellogg committed May 30, 2020
2 parents 6c81e97 + d4882c3 commit 9b17dc2
Show file tree
Hide file tree
Showing 8 changed files with 43 additions and 269 deletions.
8 changes: 4 additions & 4 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 [email protected]:your-username/rack-linkeddata.git`
* Install bundle:
Expand All @@ -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/
1 change: 1 addition & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
52 changes: 26 additions & 26 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

* <http://github.com/datagraph/rack-linkeddata>
* <https://github.com/ruby-rdf/rack-linkeddata>

[![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
Expand Down Expand Up @@ -91,19 +91,19 @@ for N-Triples, N-Quads, Turtle, RDF/XML, RDF/JSON, JSON-LD, RDFa, TriG and TriX.

##Documentation

<http://http://rubydoc.info/github/ruby-rdf/rack-linkeddata/>
<https://rubydoc.info/github/ruby-rdf/rack-linkeddata/>

* {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
Expand All @@ -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.
Expand All @@ -136,27 +136,27 @@ This repository uses [Git Flow](https://github.com/nvie/gitflow) to mange develo

## References

* <http://www.w3.org/DesignIssues/LinkedData.html>
* <http://linkeddata.org/docs/how-to-publish>
* <http://linkeddata.org/conneg-303-redirect-code-samples>
* <http://www.w3.org/TR/cooluris/>
* <http://www.w3.org/TR/swbp-vocab-pub/>
* <http://patterns.dataincubator.org/book/publishing-patterns.html>
* <https://www.w3.org/DesignIssues/LinkedData.html>
* <https://linkeddata.org/docs/how-to-publish>
* <https://linkeddata.org/conneg-303-redirect-code-samples>
* <https://www.w3.org/TR/cooluris/>
* <https://www.w3.org/TR/swbp-vocab-pub/>
* <https://patterns.dataincubator.org/book/publishing-patterns.html>

## Authors

* [Arto Bendiken](http://github.com/bendiken) - <http://ar.to/>
* [Gregg Kellogg](http://github.com/gkellogg) - <http://greggkellogg.net/>
* [Arto Bendiken](https://github.com/artob) - <https://ar.to/>
* [Gregg Kellogg](https://github.com/gkellogg) - <https://greggkellogg.net/>

## License

This is free and unencumbered public domain software. For more information,
see <http://unlicense.org/> 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 <https://unlicense.org/> 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
2 changes: 1 addition & 1 deletion UNLICENSE
Original file line number Diff line number Diff line change
Expand Up @@ -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 <http://unlicense.org/>
For more information, please refer to <https://unlicense.org/>
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
3.1.0
3.1.1
17 changes: 3 additions & 14 deletions lib/rack/linkeddata.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
require 'rack'
require 'rack/rdf'
require 'linkeddata'

module Rack
Expand All @@ -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
Expand Down
220 changes: 2 additions & 218 deletions lib/rack/linkeddata/conneg.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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<String>]
# @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
Loading

0 comments on commit 9b17dc2

Please sign in to comment.