Skip to content

Commit 7fb89e6

Browse files
abrissegkellogg
authored andcommitted
Enhance code lisibility
1 parent 6a8639b commit 7fb89e6

20 files changed

+7383
-6905
lines changed

lib/json/ld/api.rb

Lines changed: 807 additions & 771 deletions
Large diffs are not rendered by default.

lib/json/ld/compact.rb

Lines changed: 304 additions & 304 deletions
Large diffs are not rendered by default.

lib/json/ld/conneg.rb

Lines changed: 179 additions & 161 deletions
Original file line numberDiff line numberDiff line change
@@ -1,188 +1,206 @@
1-
# -*- encoding: utf-8 -*-
21
# frozen_string_literal: true
2+
3+
require 'English'
4+
35
require 'rack'
46
require 'link_header'
57

6-
module JSON::LD
7-
##
8-
# Rack middleware for JSON-LD content negotiation.
9-
#
10-
# Uses HTTP Content Negotiation to serialize `Array` and `Hash` results as JSON-LD using 'profile' accept-params to invoke appropriate JSON-LD API methods.
11-
#
12-
# Allows black-listing and white-listing of two-part profiles where the second part denotes a URL of a _context_ or _frame_. (See {JSON::LD::Writer.accept?})
13-
#
14-
# Works along with `rack-linkeddata` for serializing data which is not in the form of an `RDF::Repository`.
15-
#
16-
#
17-
# @example
18-
# use JSON::LD::Rack
19-
#
20-
# @see https://www.w3.org/TR/json-ld11/#iana-considerations
21-
# @see https://www.rubydoc.info/github/rack/rack/master/file/SPEC
22-
class ContentNegotiation
23-
VARY = {'Vary' => 'Accept'}.freeze
24-
25-
# @return [#call]
26-
attr_reader :app
27-
8+
module JSON
9+
module LD
2810
##
29-
# * Registers JSON::LD::Rack, suitable for Sinatra application
30-
# * adds helpers
11+
# Rack middleware for JSON-LD content negotiation.
3112
#
32-
# @param [Sinatra::Base] app
33-
# @return [void]
34-
def self.registered(app)
35-
options = {}
36-
app.use(JSON::LD::Rack, **options)
37-
end
13+
# Uses HTTP Content Negotiation to serialize `Array` and `Hash` results as JSON-LD using 'profile' accept-params to invoke appropriate JSON-LD API methods.
14+
#
15+
# Allows black-listing and white-listing of two-part profiles where the second part denotes a URL of a _context_ or _frame_. (See {JSON::LD::Writer.accept?})
16+
#
17+
# Works along with `rack-linkeddata` for serializing data which is not in the form of an `RDF::Repository`.
18+
#
19+
#
20+
# @example
21+
# use JSON::LD::Rack
22+
#
23+
# @see https://www.w3.org/TR/json-ld11/#iana-considerations
24+
# @see https://www.rubydoc.info/github/rack/rack/master/file/SPEC
25+
class ContentNegotiation
26+
VARY = { 'Vary' => 'Accept' }.freeze
27+
28+
# @return [#call]
29+
attr_reader :app
30+
31+
##
32+
# * Registers JSON::LD::Rack, suitable for Sinatra application
33+
# * adds helpers
34+
#
35+
# @param [Sinatra::Base] app
36+
# @return [void]
37+
def self.registered(app)
38+
options = {}
39+
app.use(JSON::LD::Rack, **options)
40+
end
3841

39-
def initialize(app)
40-
@app = app
41-
end
42+
def initialize(app)
43+
@app = app
44+
end
4245

43-
##
44-
# Handles a Rack protocol request.
45-
# Parses Accept header to find appropriate mime-type and sets content_type accordingly.
46-
#
47-
# @param [Hash{String => String}] env
48-
# @return [Array(Integer, Hash, #each)] Status, Headers and Body
49-
# @see https://rubydoc.info/github/rack/rack/file/SPEC
50-
def call(env)
51-
response = app.call(env)
52-
body = response[2].respond_to?(:body) ? response[2].body : response[2]
53-
case body
46+
##
47+
# Handles a Rack protocol request.
48+
# Parses Accept header to find appropriate mime-type and sets content_type accordingly.
49+
#
50+
# @param [Hash{String => String}] env
51+
# @return [Array(Integer, Hash, #each)] Status, Headers and Body
52+
# @see https://rubydoc.info/github/rack/rack/file/SPEC
53+
def call(env)
54+
response = app.call(env)
55+
body = response[2].respond_to?(:body) ? response[2].body : response[2]
56+
case body
5457
when Array, Hash
55-
response[2] = body # Put it back in the response, it might have been a proxy
58+
response[2] = body # Put it back in the response, it might have been a proxy
5659
serialize(env, *response)
5760
else response
58-
end
59-
end
60-
61-
##
62-
# Serializes objects as JSON-LD. Defaults to expanded form, other forms
63-
# determined by presense of `profile` in accept-parms.
64-
#
65-
# @param [Hash{String => String}] env
66-
# @param [Integer] status
67-
# @param [Hash{String => Object}] headers
68-
# @param [RDF::Enumerable] body
69-
# @return [Array(Integer, Hash, #each)] Status, Headers and Body
70-
def serialize(env, status, headers, body)
71-
# This will only return json-ld content types, possibly with parameters
72-
content_types = parse_accept_header(env['HTTP_ACCEPT'] || 'application/ld+json')
73-
content_types = content_types.select do |content_type|
74-
_, *params = content_type.split(';').map(&:strip)
75-
accept_params = params.inject({}) do |memo, pv|
76-
p, v = pv.split('=').map(&:strip)
77-
memo.merge(p.downcase.to_sym => v.sub(/^["']?([^"']*)["']?$/, '\1'))
7861
end
79-
JSON::LD::Writer.accept?(accept_params)
8062
end
81-
if content_types.empty?
82-
not_acceptable("No appropriate combinaion of media-type and parameters found")
83-
else
84-
ct, *params = content_types.first.split(';').map(&:strip)
85-
accept_params = params.inject({}) do |memo, pv|
86-
p, v = pv.split('=').map(&:strip)
87-
memo.merge(p.downcase.to_sym => v.sub(/^["']?([^"']*)["']?$/, '\1'))
88-
end
89-
90-
# Determine API method from profile
91-
profile = accept_params[:profile].to_s.split(' ')
92-
93-
# Get context from Link header
94-
links = LinkHeader.parse(env['HTTP_LINK'])
95-
context = links.find_link(['rel', JSON_LD_NS+"context"]).href rescue nil
96-
frame = links.find_link(['rel', JSON_LD_NS+"frame"]).href rescue nil
9763

98-
if profile.include?(JSON_LD_NS+"framed") && frame.nil?
99-
return not_acceptable("framed profile without a frame")
64+
##
65+
# Serializes objects as JSON-LD. Defaults to expanded form, other forms
66+
# determined by presense of `profile` in accept-parms.
67+
#
68+
# @param [Hash{String => String}] env
69+
# @param [Integer] status
70+
# @param [Hash{String => Object}] headers
71+
# @param [RDF::Enumerable] body
72+
# @return [Array(Integer, Hash, #each)] Status, Headers and Body
73+
def serialize(env, status, headers, body)
74+
# This will only return json-ld content types, possibly with parameters
75+
content_types = parse_accept_header(env['HTTP_ACCEPT'] || 'application/ld+json')
76+
content_types = content_types.select do |content_type|
77+
_, *params = content_type.split(';').map(&:strip)
78+
accept_params = params.inject({}) do |memo, pv|
79+
p, v = pv.split('=').map(&:strip)
80+
memo.merge(p.downcase.to_sym => v.sub(/^["']?([^"']*)["']?$/, '\1'))
81+
end
82+
JSON::LD::Writer.accept?(accept_params)
10083
end
101-
102-
# accept? already determined that there are appropriate contexts
103-
# If profile also includes a URI which is not a namespace, use it for compaction.
104-
context ||= Writer.default_context if profile.include?(JSON_LD_NS+"compacted")
105-
106-
result = if profile.include?(JSON_LD_NS+"flattened")
107-
API.flatten(body, context)
108-
elsif profile.include?(JSON_LD_NS+"framed")
109-
API.frame(body, frame)
110-
elsif context
111-
API.compact(body, context)
112-
elsif profile.include?(JSON_LD_NS+"expanded")
113-
API.expand(body)
84+
if content_types.empty?
85+
not_acceptable("No appropriate combinaion of media-type and parameters found")
11486
else
115-
body
87+
ct, *params = content_types.first.split(';').map(&:strip)
88+
accept_params = params.inject({}) do |memo, pv|
89+
p, v = pv.split('=').map(&:strip)
90+
memo.merge(p.downcase.to_sym => v.sub(/^["']?([^"']*)["']?$/, '\1'))
91+
end
92+
93+
# Determine API method from profile
94+
profile = accept_params[:profile].to_s.split
95+
96+
# Get context from Link header
97+
links = LinkHeader.parse(env['HTTP_LINK'])
98+
context = begin
99+
links.find_link(['rel', JSON_LD_NS + "context"]).href
100+
rescue StandardError
101+
nil
102+
end
103+
frame = begin
104+
links.find_link(['rel', JSON_LD_NS + "frame"]).href
105+
rescue StandardError
106+
nil
107+
end
108+
109+
if profile.include?(JSON_LD_NS + "framed") && frame.nil?
110+
return not_acceptable("framed profile without a frame")
111+
end
112+
113+
# accept? already determined that there are appropriate contexts
114+
# If profile also includes a URI which is not a namespace, use it for compaction.
115+
context ||= Writer.default_context if profile.include?(JSON_LD_NS + "compacted")
116+
117+
result = if profile.include?(JSON_LD_NS + "flattened")
118+
API.flatten(body, context)
119+
elsif profile.include?(JSON_LD_NS + "framed")
120+
API.frame(body, frame)
121+
elsif context
122+
API.compact(body, context)
123+
elsif profile.include?(JSON_LD_NS + "expanded")
124+
API.expand(body)
125+
else
126+
body
127+
end
128+
129+
headers = headers.merge(VARY).merge('Content-Type' => ct)
130+
[status, headers, [result.to_json]]
116131
end
117-
118-
headers = headers.merge(VARY).merge('Content-Type' => ct)
119-
[status, headers, [result.to_json]]
132+
rescue StandardError
133+
http_error(500, $ERROR_INFO.message)
120134
end
121-
rescue
122-
http_error(500, $!.message)
123-
end
124135

125-
protected
136+
protected
137+
138+
##
139+
# Parses an HTTP `Accept` header, returning an array of MIME content
140+
# types ordered by the precedence rules defined in HTTP/1.1 §14.1.
141+
#
142+
# @param [String, #to_s] header
143+
# @return [Array<String>]
144+
# @see http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.1
145+
def parse_accept_header(header)
146+
entries = header.to_s.split(',')
147+
entries = entries
148+
.map { |e| accept_entry(e) }
149+
.sort_by(&:last)
150+
.map(&:first)
151+
entries.map { |e| find_content_type_for_media_range(e) }.compact
152+
end
126153

127-
##
128-
# Parses an HTTP `Accept` header, returning an array of MIME content
129-
# types ordered by the precedence rules defined in HTTP/1.1 §14.1.
130-
#
131-
# @param [String, #to_s] header
132-
# @return [Array<String>]
133-
# @see http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.1
134-
def parse_accept_header(header)
135-
entries = header.to_s.split(',')
136-
entries = entries.
137-
map { |e| accept_entry(e) }.
138-
sort_by(&:last).
139-
map(&:first)
140-
entries.map { |e| find_content_type_for_media_range(e) }.compact
141-
end
154+
# Returns an array of quality, number of '*' in content-type, and number of non-'q' parameters
155+
def accept_entry(entry)
156+
type, *options = entry.split(';').map(&:strip)
157+
quality = 0 # we sort smallest first
158+
options.delete_if { |e| quality = 1 - e[2..].to_f if e.start_with? 'q=' }
159+
[options.unshift(type).join(';'), [quality, type.count('*'), 1 - options.size]]
160+
end
142161

143-
# Returns an array of quality, number of '*' in content-type, and number of non-'q' parameters
144-
def accept_entry(entry)
145-
type, *options = entry.split(';').map(&:strip)
146-
quality = 0 # we sort smallest first
147-
options.delete_if { |e| quality = 1 - e[2..-1].to_f if e.start_with? 'q=' }
148-
[options.unshift(type).join(';'), [quality, type.count('*'), 1 - options.size]]
149-
end
162+
##
163+
# Returns a content type appropriate for the given `media_range`,
164+
# returns `nil` if `media_range` contains a wildcard subtype
165+
# that is not mapped.
166+
#
167+
# @param [String, #to_s] media_range
168+
# @return [String, nil]
169+
def find_content_type_for_media_range(media_range)
170+
media_range = media_range.sub('*/*', 'application/ld+json') if media_range.to_s.start_with?('*/*')
171+
if media_range.to_s.start_with?('application/*')
172+
media_range = media_range.sub('application/*',
173+
'application/ld+json')
174+
end
175+
if media_range.to_s.start_with?('application/json')
176+
media_range = media_range.sub('application/json',
177+
'application/ld+json')
178+
end
150179

151-
##
152-
# Returns a content type appropriate for the given `media_range`,
153-
# returns `nil` if `media_range` contains a wildcard subtype
154-
# that is not mapped.
155-
#
156-
# @param [String, #to_s] media_range
157-
# @return [String, nil]
158-
def find_content_type_for_media_range(media_range)
159-
media_range = media_range.sub('*/*', 'application/ld+json') if media_range.to_s.start_with?('*/*')
160-
media_range = media_range.sub('application/*', 'application/ld+json') if media_range.to_s.start_with?('application/*')
161-
media_range = media_range.sub('application/json', 'application/ld+json') if media_range.to_s.start_with?('application/json')
162-
163-
media_range.start_with?('application/ld+json') ? media_range : nil
164-
end
180+
media_range.start_with?('application/ld+json') ? media_range : nil
181+
end
165182

166-
##
167-
# Outputs an HTTP `406 Not Acceptable` response.
168-
#
169-
# @param [String, #to_s] message
170-
# @return [Array(Integer, Hash, #each)]
171-
def not_acceptable(message = nil)
172-
http_error(406, message, VARY)
173-
end
183+
##
184+
# Outputs an HTTP `406 Not Acceptable` response.
185+
#
186+
# @param [String, #to_s] message
187+
# @return [Array(Integer, Hash, #each)]
188+
def not_acceptable(message = nil)
189+
http_error(406, message, VARY)
190+
end
174191

175-
##
176-
# Outputs an HTTP `4xx` or `5xx` response.
177-
#
178-
# @param [Integer, #to_i] code
179-
# @param [String, #to_s] message
180-
# @param [Hash{String => String}] headers
181-
# @return [Array(Integer, Hash, #each)]
182-
def http_error(code, message = nil, headers = {})
183-
message = [code, Rack::Utils::HTTP_STATUS_CODES[code]].join(' ') +
184-
(message.nil? ? "\n" : " (#{message})\n")
185-
[code, {'Content-Type' => "text/plain"}.merge(headers), [message]]
192+
##
193+
# Outputs an HTTP `4xx` or `5xx` response.
194+
#
195+
# @param [Integer, #to_i] code
196+
# @param [String, #to_s] message
197+
# @param [Hash{String => String}] headers
198+
# @return [Array(Integer, Hash, #each)]
199+
def http_error(code, message = nil, headers = {})
200+
message = [code, Rack::Utils::HTTP_STATUS_CODES[code]].join(' ') +
201+
(message.nil? ? "\n" : " (#{message})\n")
202+
[code, { 'Content-Type' => "text/plain" }.merge(headers), [message]]
203+
end
186204
end
187205
end
188206
end

0 commit comments

Comments
 (0)