json-ld 3.2.3 → 3.2.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/VERSION +1 -1
- data/lib/json/ld/api.rb +807 -764
- data/lib/json/ld/compact.rb +304 -304
- data/lib/json/ld/conneg.rb +179 -161
- data/lib/json/ld/context.rb +2080 -1945
- data/lib/json/ld/expand.rb +745 -666
- data/lib/json/ld/extensions.rb +14 -13
- data/lib/json/ld/flatten.rb +257 -247
- data/lib/json/ld/format.rb +202 -194
- data/lib/json/ld/frame.rb +525 -502
- data/lib/json/ld/from_rdf.rb +224 -166
- data/lib/json/ld/html/nokogiri.rb +123 -121
- data/lib/json/ld/html/rexml.rb +151 -147
- data/lib/json/ld/reader.rb +107 -100
- data/lib/json/ld/resource.rb +224 -205
- data/lib/json/ld/streaming_reader.rb +574 -507
- data/lib/json/ld/streaming_writer.rb +93 -92
- data/lib/json/ld/to_rdf.rb +171 -167
- data/lib/json/ld/utils.rb +270 -264
- data/lib/json/ld/version.rb +24 -14
- data/lib/json/ld/writer.rb +334 -311
- data/lib/json/ld.rb +103 -96
- metadata +78 -209
- data/spec/api_spec.rb +0 -132
- data/spec/compact_spec.rb +0 -3482
- data/spec/conneg_spec.rb +0 -373
- data/spec/context_spec.rb +0 -2036
- data/spec/expand_spec.rb +0 -4496
- data/spec/flatten_spec.rb +0 -1203
- data/spec/format_spec.rb +0 -115
- data/spec/frame_spec.rb +0 -2498
- data/spec/from_rdf_spec.rb +0 -1005
- data/spec/matchers.rb +0 -20
- data/spec/rdfstar_spec.rb +0 -25
- data/spec/reader_spec.rb +0 -883
- data/spec/resource_spec.rb +0 -76
- data/spec/spec_helper.rb +0 -281
- data/spec/streaming_reader_spec.rb +0 -237
- data/spec/streaming_writer_spec.rb +0 -145
- data/spec/suite_compact_spec.rb +0 -22
- data/spec/suite_expand_spec.rb +0 -36
- data/spec/suite_flatten_spec.rb +0 -34
- data/spec/suite_frame_spec.rb +0 -29
- data/spec/suite_from_rdf_spec.rb +0 -22
- data/spec/suite_helper.rb +0 -411
- data/spec/suite_html_spec.rb +0 -22
- data/spec/suite_http_spec.rb +0 -35
- data/spec/suite_remote_doc_spec.rb +0 -22
- data/spec/suite_to_rdf_spec.rb +0 -30
- data/spec/support/extensions.rb +0 -44
- data/spec/test-files/test-1-compacted.jsonld +0 -10
- data/spec/test-files/test-1-context.jsonld +0 -7
- data/spec/test-files/test-1-expanded.jsonld +0 -5
- data/spec/test-files/test-1-input.jsonld +0 -10
- data/spec/test-files/test-1-rdf.ttl +0 -8
- data/spec/test-files/test-2-compacted.jsonld +0 -20
- data/spec/test-files/test-2-context.jsonld +0 -7
- data/spec/test-files/test-2-expanded.jsonld +0 -16
- data/spec/test-files/test-2-input.jsonld +0 -20
- data/spec/test-files/test-2-rdf.ttl +0 -14
- data/spec/test-files/test-3-compacted.jsonld +0 -11
- data/spec/test-files/test-3-context.jsonld +0 -8
- data/spec/test-files/test-3-expanded.jsonld +0 -10
- data/spec/test-files/test-3-input.jsonld +0 -11
- data/spec/test-files/test-3-rdf.ttl +0 -8
- data/spec/test-files/test-4-compacted.jsonld +0 -10
- data/spec/test-files/test-4-context.jsonld +0 -7
- data/spec/test-files/test-4-expanded.jsonld +0 -6
- data/spec/test-files/test-4-input.jsonld +0 -10
- data/spec/test-files/test-4-rdf.ttl +0 -5
- data/spec/test-files/test-5-compacted.jsonld +0 -13
- data/spec/test-files/test-5-context.jsonld +0 -7
- data/spec/test-files/test-5-expanded.jsonld +0 -9
- data/spec/test-files/test-5-input.jsonld +0 -13
- data/spec/test-files/test-5-rdf.ttl +0 -7
- data/spec/test-files/test-6-compacted.jsonld +0 -10
- data/spec/test-files/test-6-context.jsonld +0 -7
- data/spec/test-files/test-6-expanded.jsonld +0 -10
- data/spec/test-files/test-6-input.jsonld +0 -10
- data/spec/test-files/test-6-rdf.ttl +0 -6
- data/spec/test-files/test-7-compacted.jsonld +0 -23
- data/spec/test-files/test-7-context.jsonld +0 -4
- data/spec/test-files/test-7-expanded.jsonld +0 -20
- data/spec/test-files/test-7-input.jsonld +0 -23
- data/spec/test-files/test-7-rdf.ttl +0 -14
- data/spec/test-files/test-8-compacted.jsonld +0 -34
- data/spec/test-files/test-8-context.jsonld +0 -11
- data/spec/test-files/test-8-expanded.jsonld +0 -24
- data/spec/test-files/test-8-frame.jsonld +0 -18
- data/spec/test-files/test-8-framed.jsonld +0 -25
- data/spec/test-files/test-8-input.jsonld +0 -30
- data/spec/test-files/test-8-rdf.ttl +0 -15
- data/spec/test-files/test-9-compacted.jsonld +0 -20
- data/spec/test-files/test-9-context.jsonld +0 -13
- data/spec/test-files/test-9-expanded.jsonld +0 -14
- data/spec/test-files/test-9-input.jsonld +0 -12
- data/spec/to_rdf_spec.rb +0 -1551
- data/spec/writer_spec.rb +0 -427
data/lib/json/ld/conneg.rb
CHANGED
@@ -1,188 +1,206 @@
|
|
1
|
-
# -*- encoding: utf-8 -*-
|
2
1
|
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'English'
|
4
|
+
|
3
5
|
require 'rack'
|
4
6
|
require 'link_header'
|
5
7
|
|
6
|
-
module JSON
|
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
|
28
10
|
##
|
29
|
-
#
|
30
|
-
# * adds helpers
|
11
|
+
# Rack middleware for JSON-LD content negotiation.
|
31
12
|
#
|
32
|
-
#
|
33
|
-
#
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
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
|
38
41
|
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
+
def initialize(app)
|
43
|
+
@app = app
|
44
|
+
end
|
42
45
|
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
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
|
54
57
|
when Array, Hash
|
55
|
-
response[2] = body
|
58
|
+
response[2] = body # Put it back in the response, it might have been a proxy
|
56
59
|
serialize(env, *response)
|
57
60
|
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'))
|
78
61
|
end
|
79
|
-
JSON::LD::Writer.accept?(accept_params)
|
80
62
|
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
|
97
63
|
|
98
|
-
|
99
|
-
|
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)
|
100
83
|
end
|
101
|
-
|
102
|
-
|
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")
|
114
86
|
else
|
115
|
-
|
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]]
|
116
131
|
end
|
117
|
-
|
118
|
-
|
119
|
-
[status, headers, [result.to_json]]
|
132
|
+
rescue StandardError
|
133
|
+
http_error(500, $ERROR_INFO.message)
|
120
134
|
end
|
121
|
-
rescue
|
122
|
-
http_error(500, $!.message)
|
123
|
-
end
|
124
135
|
|
125
|
-
|
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
|
126
153
|
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
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
|
142
161
|
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
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
|
150
179
|
|
151
|
-
|
152
|
-
|
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
|
165
182
|
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
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
|
174
191
|
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
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
|
186
204
|
end
|
187
205
|
end
|
188
206
|
end
|