json-ld 0.1.0 → 0.1.2
Sign up to get free protection for your applications and to get access to all the features.
- data/History.markdown +15 -0
- data/README.markdown +199 -3
- data/VERSION +1 -1
- data/lib/json/ld.rb +44 -4
- data/lib/json/ld/api.rb +220 -224
- data/lib/json/ld/compact.rb +126 -0
- data/lib/json/ld/evaluation_context.rb +428 -204
- data/lib/json/ld/expand.rb +185 -0
- data/lib/json/ld/extensions.rb +34 -7
- data/lib/json/ld/format.rb +2 -17
- data/lib/json/ld/frame.rb +452 -0
- data/lib/json/ld/from_rdf.rb +166 -0
- data/lib/json/ld/reader.rb +7 -231
- data/lib/json/ld/to_rdf.rb +181 -0
- data/lib/json/ld/utils.rb +97 -0
- data/lib/json/ld/writer.rb +33 -471
- metadata +51 -34
- data/lib/json/ld/normalize.rb +0 -120
@@ -0,0 +1,185 @@
|
|
1
|
+
module JSON::LD
|
2
|
+
##
|
3
|
+
# Expand module, used as part of API
|
4
|
+
module Expand
|
5
|
+
include Utils
|
6
|
+
|
7
|
+
##
|
8
|
+
# Expand an Array or Object given an active context and performing local context expansion.
|
9
|
+
#
|
10
|
+
# @param [Array, Hash] input
|
11
|
+
# @param [String] active_property
|
12
|
+
# @param [EvaluationContext] context
|
13
|
+
# @param [Hash{Symbol => Object}] options
|
14
|
+
# @return [Array, Hash]
|
15
|
+
def expand(input, active_property, context, options = {})
|
16
|
+
debug("expand") {"input: #{input.inspect}, active_property: #{active_property.inspect}, context: #{context.inspect}"}
|
17
|
+
result = case input
|
18
|
+
when Array
|
19
|
+
# If element is an array, process each item in element recursively using this algorithm,
|
20
|
+
# passing copies of the active context and active property. If the expanded entry is null, drop it.
|
21
|
+
depth do
|
22
|
+
is_list = context.container(active_property) == '@list'
|
23
|
+
value = input.map do |v|
|
24
|
+
# If active property has a @container set to @list, and item is an array,
|
25
|
+
# or the result of expanding any item is an object containing an @list property,
|
26
|
+
# throw an exception as lists of lists are not allowed.
|
27
|
+
raise ProcessingError::ListOfLists, "A list may not contain another list" if v.is_a?(Array) && is_list
|
28
|
+
|
29
|
+
expand(v, active_property, context, options)
|
30
|
+
end.flatten.compact
|
31
|
+
|
32
|
+
if is_list && value.any? {|v| v.is_a?(Hash) && v.has_key?('@list')}
|
33
|
+
raise ProcessingError::ListOfLists, "A list may not contain another list"
|
34
|
+
end
|
35
|
+
|
36
|
+
value
|
37
|
+
end
|
38
|
+
when Hash
|
39
|
+
# Otherwise, if element is an object
|
40
|
+
# If element has a @context property, update the active context according to the steps outlined
|
41
|
+
# in Context Processing and remove the @context property.
|
42
|
+
if input.has_key?('@context')
|
43
|
+
context = context.parse(input.delete('@context'))
|
44
|
+
debug("expand") {"evaluation context: #{context.inspect}"}
|
45
|
+
end
|
46
|
+
|
47
|
+
depth do
|
48
|
+
output_object = Hash.ordered
|
49
|
+
# Then, proceed and process each property and value in element as follows:
|
50
|
+
input.each do |key, value|
|
51
|
+
# Remove property from element expand property according to the steps outlined in IRI Expansion
|
52
|
+
property = context.expand_iri(key, :position => :predicate, :quiet => true)
|
53
|
+
|
54
|
+
# Set active property to the original un-expanded property if property if not a keyword
|
55
|
+
active_property = key unless key[0,1] == '@'
|
56
|
+
debug("expand property") {"#{active_property}, expanded: #{property}, value: #{value.inspect}"}
|
57
|
+
|
58
|
+
# If property does not expand to a keyword or absolute IRI, remove property from element
|
59
|
+
# and continue to the next property from element
|
60
|
+
if property.nil?
|
61
|
+
debug(" => ") {"skip nil key"}
|
62
|
+
next
|
63
|
+
end
|
64
|
+
property = property.to_s
|
65
|
+
|
66
|
+
expanded_value = case property
|
67
|
+
when '@id'
|
68
|
+
# If the property is @id the value must be a string. Expand the value according to IRI Expansion.
|
69
|
+
context.expand_iri(value, :position => :subject, :quiet => true).to_s
|
70
|
+
when '@type'
|
71
|
+
# Otherwise, if the property is @type the value must be a string, an array of strings
|
72
|
+
# or an empty JSON Object.
|
73
|
+
# Expand value or each of it's entries according to IRI Expansion
|
74
|
+
case value
|
75
|
+
when Array
|
76
|
+
depth do
|
77
|
+
[value].flatten.map do |v|
|
78
|
+
context.expand_iri(v, options.merge(:position => :property, :quiet => true)).to_s
|
79
|
+
end
|
80
|
+
end
|
81
|
+
when Hash
|
82
|
+
# Empty object used for @type wildcard
|
83
|
+
raise ProcessingError, "Object value of @type must be empty: #{value.inspect}" unless value.empty?
|
84
|
+
value
|
85
|
+
else
|
86
|
+
context.expand_iri(value, options.merge(:position => :property, :quiet => true)).to_s
|
87
|
+
end
|
88
|
+
when '@value', '@language'
|
89
|
+
# Otherwise, if the property is @value or @language the value must not be a JSON object or an array.
|
90
|
+
raise ProcessingError::Lossy, "Value of #{property} must be a string, was #{value.inspect}" if value.is_a?(Hash) || value.is_a?(Array)
|
91
|
+
value
|
92
|
+
when '@list', '@set', '@graph'
|
93
|
+
# Otherwise, if the property is @list, @set, or @graph, expand value recursively
|
94
|
+
# using this algorithm, passing copies of the active context and active property.
|
95
|
+
# If the expanded value is not an array, convert it to an array.
|
96
|
+
value = [value] unless value.is_a?(Array)
|
97
|
+
value = depth { expand(value, active_property, context, options) }
|
98
|
+
|
99
|
+
# If property is @list, and any expanded value
|
100
|
+
# is an object containing an @list property, throw an exception, as lists of lists are not supported
|
101
|
+
if property == '@list' && value.any? {|v| v.is_a?(Hash) && v.has_key?('@list')}
|
102
|
+
raise ProcessingError::ListOfLists, "A list may not contain another list"
|
103
|
+
end
|
104
|
+
|
105
|
+
value
|
106
|
+
else
|
107
|
+
# Otherwise, expand value recursively using this algorithm, passing copies of the active context and active property.
|
108
|
+
depth { expand(value, active_property, context, options) }
|
109
|
+
end
|
110
|
+
|
111
|
+
# moved from step 2.2.3
|
112
|
+
# If expanded value is null and property is not @value, continue with the next property
|
113
|
+
# from element.
|
114
|
+
if property != '@value' && expanded_value.nil?
|
115
|
+
debug(" => skip nil value")
|
116
|
+
next
|
117
|
+
end
|
118
|
+
|
119
|
+
# If the expanded value is not null and property is not a keyword
|
120
|
+
# and the active property has a @container set to @list,
|
121
|
+
# convert value to an object with an @list property whose value is set to value
|
122
|
+
# (unless value is already in that form)
|
123
|
+
if expanded_value && property[0,1] != '@' && context.container(active_property) == '@list' &&
|
124
|
+
(!expanded_value.is_a?(Hash) || !expanded_value.fetch('@list', false))
|
125
|
+
debug(" => ") { "convert #{expanded_value.inspect} to list"}
|
126
|
+
expanded_value = {'@list' => [expanded_value].flatten}
|
127
|
+
end
|
128
|
+
|
129
|
+
# Convert value to array form unless value is null or property is @id, @type, @value, or @language.
|
130
|
+
if !%(@id @language @type @value).include?(property) && !expanded_value.is_a?(Array)
|
131
|
+
debug(" => make #{expanded_value.inspect} an array")
|
132
|
+
expanded_value = [expanded_value]
|
133
|
+
end
|
134
|
+
|
135
|
+
if output_object.has_key?(property)
|
136
|
+
# If element already contains a property property, append value to the existing value.
|
137
|
+
output_object[property] += expanded_value
|
138
|
+
else
|
139
|
+
# Otherwise, create a property property with value as value.
|
140
|
+
output_object[property] = expanded_value
|
141
|
+
end
|
142
|
+
debug {" => #{expanded_value.inspect}"}
|
143
|
+
end
|
144
|
+
|
145
|
+
debug("output object") {output_object.inspect}
|
146
|
+
|
147
|
+
# If the processed element has an @value property
|
148
|
+
if output_object.has_key?('@value')
|
149
|
+
output_object.delete('@language') if output_object['@language'].to_s.empty?
|
150
|
+
output_object.delete('@type') if output_object['@type'].to_s.empty?
|
151
|
+
if output_object.keys.length > 2 || (%w(@language @type) - output_object.keys).empty?
|
152
|
+
raise ProcessingError, "element must not have more than one other property, which can either be @language or @type with a string value." unless value.is_a?(String)
|
153
|
+
end
|
154
|
+
|
155
|
+
# if @value is the only property or the value of @value equals null, replace element with the value of @value.
|
156
|
+
if output_object['@value'].nil? || output_object.keys.length == 1
|
157
|
+
return output_object['@value']
|
158
|
+
end
|
159
|
+
elsif !output_object.fetch('@type', []).is_a?(Array)
|
160
|
+
# Otherwise, if element has an @type property and it's value is not in the form of an array,
|
161
|
+
# convert it to an array.
|
162
|
+
output_object['@type'] = [output_object['@type']]
|
163
|
+
end
|
164
|
+
|
165
|
+
# If element has an @set or @list property, it must be the only property. Set element to the value of @set;
|
166
|
+
# leave @list untouched.
|
167
|
+
if !(%w(@set @list) & output_object.keys).empty?
|
168
|
+
raise ProcessingError, "element must have only @set, @list or @graph" if output_object.keys.length > 1
|
169
|
+
|
170
|
+
output_object = output_object.values.first unless output_object.has_key?('@list')
|
171
|
+
end
|
172
|
+
|
173
|
+
# If element has just a @language property, set element to null.
|
174
|
+
output_object unless output_object.is_a?(Hash) && output_object.keys == %w(@language)
|
175
|
+
end
|
176
|
+
else
|
177
|
+
# Otherwise, unless the value is a number, expand the value according to the Value Expansion rules, passing active property.
|
178
|
+
context.expand_value(active_property, input, :position => :object, :depth => @depth) unless input.nil?
|
179
|
+
end
|
180
|
+
|
181
|
+
debug {" => #{result.inspect}"}
|
182
|
+
result
|
183
|
+
end
|
184
|
+
end
|
185
|
+
end
|
data/lib/json/ld/extensions.rb
CHANGED
@@ -37,6 +37,29 @@ module RDF
|
|
37
37
|
@uri.to_s
|
38
38
|
end
|
39
39
|
end
|
40
|
+
|
41
|
+
class Literal
|
42
|
+
class Double
|
43
|
+
##
|
44
|
+
# Converts this literal into its canonical lexical representation.
|
45
|
+
# Update to use %.15E to avoid precision problems
|
46
|
+
def canonicalize!
|
47
|
+
@string = case
|
48
|
+
when @object.nan? then 'NaN'
|
49
|
+
when @object.infinite? then @object.to_s[0...-'inity'.length].upcase
|
50
|
+
when @object.zero? then '0.0E0'
|
51
|
+
else
|
52
|
+
i, f, e = ('%.15E' % @object.to_f).split(/[\.E]/)
|
53
|
+
f.sub!(/0*$/, '') # remove any trailing zeroes
|
54
|
+
f = '0' if f.empty? # ...but there must be a digit to the right of the decimal point
|
55
|
+
e.sub!(/^\+?0+(\d)$/, '\1') # remove the optional leading '+' sign and any extra leading zeroes
|
56
|
+
"#{i}.#{f}E#{e}"
|
57
|
+
end
|
58
|
+
@object = Float(@string) unless @object.nil?
|
59
|
+
self
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
40
63
|
end
|
41
64
|
|
42
65
|
if RUBY_VERSION < "1.9"
|
@@ -54,7 +77,7 @@ if RUBY_VERSION < "1.9"
|
|
54
77
|
end
|
55
78
|
|
56
79
|
def each
|
57
|
-
@ordered_keys.each {|k| yield(k,
|
80
|
+
@ordered_keys.each {|k| yield(k, self[k])}
|
58
81
|
end
|
59
82
|
alias :each_pair :each
|
60
83
|
|
@@ -70,10 +93,6 @@ if RUBY_VERSION < "1.9"
|
|
70
93
|
@ordered_keys
|
71
94
|
end
|
72
95
|
|
73
|
-
def values
|
74
|
-
@ordered_keys.map {|k| super[k]}
|
75
|
-
end
|
76
|
-
|
77
96
|
def clear
|
78
97
|
@ordered_keys.clear
|
79
98
|
super
|
@@ -97,7 +116,9 @@ if RUBY_VERSION < "1.9"
|
|
97
116
|
end
|
98
117
|
|
99
118
|
def merge!(other)
|
100
|
-
|
119
|
+
new_keys = other.instance_variable_get(:@ordered_keys) || other.keys
|
120
|
+
new_keys -= @ordered_keys
|
121
|
+
@ordered_keys += new_keys
|
101
122
|
super
|
102
123
|
self
|
103
124
|
end
|
@@ -108,8 +129,14 @@ if RUBY_VERSION < "1.9"
|
|
108
129
|
end
|
109
130
|
|
110
131
|
class Hash
|
111
|
-
def
|
132
|
+
def self.ordered(obj = nil, &block)
|
112
133
|
InsertOrderPreservingHash.new(obj, &block)
|
113
134
|
end
|
114
135
|
end
|
136
|
+
else
|
137
|
+
class Hash
|
138
|
+
def self.ordered(obj = nil, &block)
|
139
|
+
Hash.new(obj, &block)
|
140
|
+
end
|
141
|
+
end
|
115
142
|
end
|
data/lib/json/ld/format.rb
CHANGED
@@ -22,7 +22,7 @@ module JSON::LD
|
|
22
22
|
# @see http://www.w3.org/TR/rdf-testcases/#ntriples
|
23
23
|
class Format < RDF::Format
|
24
24
|
content_type 'application/ld+json',
|
25
|
-
:
|
25
|
+
:extension => :jsonld,
|
26
26
|
:alias => 'application/x-ld+json'
|
27
27
|
content_encoding 'utf-8'
|
28
28
|
|
@@ -39,7 +39,7 @@ module JSON::LD
|
|
39
39
|
# @param [String] sample Beginning several bytes (~ 1K) of input.
|
40
40
|
# @return [Boolean]
|
41
41
|
def self.detect(sample)
|
42
|
-
!!sample.match(/\{\s*"@(
|
42
|
+
!!sample.match(/\{\s*"@(id|context|type)"/m)
|
43
43
|
end
|
44
44
|
|
45
45
|
##
|
@@ -48,19 +48,4 @@ module JSON::LD
|
|
48
48
|
:jsonld
|
49
49
|
end
|
50
50
|
end
|
51
|
-
|
52
|
-
# Alias for JSON-LD format
|
53
|
-
#
|
54
|
-
# This allows the following:
|
55
|
-
#
|
56
|
-
# @example Obtaining an Notation3 format class
|
57
|
-
# RDF::Format.for(:jsonld) #=> JSON::LD::JSONLD
|
58
|
-
# RDF::Format.for(:jsonld).reader #=> JSON::LD::Reader
|
59
|
-
# RDF::Format.for(:jsonld).writer #=> JSON::LD::Writer
|
60
|
-
class JSONLD < RDF::Format
|
61
|
-
content_encoding 'utf-8'
|
62
|
-
|
63
|
-
reader { JSON::LD::Reader }
|
64
|
-
writer { JSON::LD::Writer }
|
65
|
-
end
|
66
51
|
end
|
@@ -0,0 +1,452 @@
|
|
1
|
+
module JSON::LD
|
2
|
+
module Frame
|
3
|
+
include Utils
|
4
|
+
|
5
|
+
##
|
6
|
+
# Frame input. Input is expected in expanded form, but frame is in compacted form.
|
7
|
+
#
|
8
|
+
# @param [Hash{Symbol => Object}] state
|
9
|
+
# Current framing state
|
10
|
+
# @param [Hash{String => Hash}] subjects
|
11
|
+
# Map of flattened subjects
|
12
|
+
# @param [Hash{String => Object}] frame
|
13
|
+
# @param [Hash{String => Object}] parent
|
14
|
+
# Parent subject or top-level array
|
15
|
+
# @param [String] property
|
16
|
+
# Property referencing this frame, or null for array.
|
17
|
+
# @raise [JSON::LD::InvalidFrame]
|
18
|
+
def frame(state, subjects, frame, parent, property)
|
19
|
+
raise ProcessingError, "why isn't @subjects a hash?: #{@subjects.inspect}" unless @subjects.is_a?(Hash)
|
20
|
+
depth do
|
21
|
+
debug("frame") {"state: #{state.inspect}"}
|
22
|
+
debug("frame") {"subjects: #{subjects.keys.inspect}"}
|
23
|
+
debug("frame") {"frame: #{frame.to_json(JSON_STATE)}"}
|
24
|
+
debug("frame") {"parent: #{parent.to_json(JSON_STATE)}"}
|
25
|
+
debug("frame") {"property: #{property.inspect}"}
|
26
|
+
# Validate the frame
|
27
|
+
validate_frame(state, frame)
|
28
|
+
|
29
|
+
# Create a set of matched subjects by filtering subjects by checking the map of flattened subjects against frame
|
30
|
+
# This gives us a hash of objects indexed by @id
|
31
|
+
matches = filter_subjects(state, subjects, frame)
|
32
|
+
debug("frame") {"matches: #{matches.keys.inspect}"}
|
33
|
+
|
34
|
+
# Get values for embedOn and explicitOn
|
35
|
+
embed = get_frame_flag(state, frame, 'embed');
|
36
|
+
explicit = get_frame_flag(state, frame, 'explicit');
|
37
|
+
debug("frame") {"embed: #{embed.inspect}, explicit: #{explicit.inspect}"}
|
38
|
+
|
39
|
+
# For each id and subject from the set of matched subjects ordered by id
|
40
|
+
matches.keys.sort.each do |id|
|
41
|
+
element = matches[id]
|
42
|
+
# If the active property is null, set the map of embeds in state to an empty map
|
43
|
+
state = state.merge(:embeds => {}) if property.nil?
|
44
|
+
|
45
|
+
output = {'@id' => id}
|
46
|
+
|
47
|
+
# prepare embed meta info
|
48
|
+
embedded_subject = {:parent => parent, :property => property}
|
49
|
+
|
50
|
+
# If embedOn is true, and id is in map of embeds from state
|
51
|
+
if embed && (existing = state[:embeds].fetch(id, nil))
|
52
|
+
# only overwrite an existing embed if it has already been added to its
|
53
|
+
# parent -- otherwise its parent is somewhere up the tree from this
|
54
|
+
# embed and the embed would occur twice once the tree is added
|
55
|
+
embed = false
|
56
|
+
|
57
|
+
embed = if existing[:parent].is_a?(Array)
|
58
|
+
# If existing has a parent which is an array containing a JSON object with @id equal to id, element has already been embedded and can be overwritten, so set embedOn to true
|
59
|
+
existing[:parent].detect {|p| p['@id'] == id}
|
60
|
+
else
|
61
|
+
# Otherwise, existing has a parent which is a subject definition. Set embedOn to true if any of the items in parent property is a subject definition or subject reference for id because the embed can be overwritten
|
62
|
+
existing[:parent].fetch(existing[:property], []).any? do |v|
|
63
|
+
v.is_a?(Hash) && v.fetch('@id', nil) == id
|
64
|
+
end
|
65
|
+
end
|
66
|
+
debug("frame") {"embed now: #{embed.inspect}"}
|
67
|
+
|
68
|
+
# If embedOn is true, existing is already embedded but can be overwritten
|
69
|
+
remove_embed(state, id) if embed
|
70
|
+
end
|
71
|
+
|
72
|
+
unless embed
|
73
|
+
# not embedding, add output without any other properties
|
74
|
+
add_frame_output(state, parent, property, output)
|
75
|
+
else
|
76
|
+
# Add embed to map of embeds for id
|
77
|
+
state[:embeds][id] = embedded_subject
|
78
|
+
debug("frame") {"add embedded_subject: #{embedded_subject.inspect}"}
|
79
|
+
|
80
|
+
# Process each property and value in the matched subject as follows
|
81
|
+
element.keys.sort.each do |prop|
|
82
|
+
value = element[prop]
|
83
|
+
if prop[0,1] == '@'
|
84
|
+
# If property is a keyword, add property and a copy of value to output and continue with the next property from subject
|
85
|
+
output[prop] = value.dup
|
86
|
+
next
|
87
|
+
end
|
88
|
+
|
89
|
+
# If property is not in frame:
|
90
|
+
unless frame.has_key?(prop)
|
91
|
+
debug("frame") {"non-framed property #{prop}"}
|
92
|
+
# If explicitOn is false, Embed values from subject in output using subject as element and property as active property
|
93
|
+
embed_values(state, element, prop, output) unless explicit
|
94
|
+
|
95
|
+
# Continue to next property
|
96
|
+
next
|
97
|
+
end
|
98
|
+
|
99
|
+
# Process each item from value as follows
|
100
|
+
value.each do |item|
|
101
|
+
debug("frame") {"value property #{prop.inspect} == #{item.inspect}"}
|
102
|
+
|
103
|
+
# FIXME: If item is a JSON object with the key @list
|
104
|
+
if list?(item)
|
105
|
+
# create a JSON object named list with the key @list and the value of an empty array
|
106
|
+
list = {'@list' => []}
|
107
|
+
|
108
|
+
# Append list to property in output
|
109
|
+
add_frame_output(state, output, prop, list)
|
110
|
+
|
111
|
+
# Process each listitem in the @list array as follows
|
112
|
+
item['@list'].each do |listitem|
|
113
|
+
if subject_reference?(listitem)
|
114
|
+
itemid = listitem['@id']
|
115
|
+
debug("frame") {"list item of #{prop} recurse for #{itemid.inspect}"}
|
116
|
+
|
117
|
+
# If listitem is a subject reference process listitem recursively using this algorithm passing a new map of subjects that contains the @id of listitem as the key and the subject reference as the value. Pass the first value from frame for property as frame, list as parent, and @list as active property.
|
118
|
+
frame(state, {itemid => @subjects[itemid]}, frame[prop].first, list, '@list')
|
119
|
+
else
|
120
|
+
# Otherwise, append a copy of listitem to @list in list.
|
121
|
+
debug("frame") {"list item of #{prop} non-subject ref #{listitem.inspect}"}
|
122
|
+
add_frame_output(state, list, '@list', listitem)
|
123
|
+
end
|
124
|
+
end
|
125
|
+
elsif subject_reference?(item)
|
126
|
+
# If item is a subject reference process item recursively
|
127
|
+
# Recurse into sub-objects
|
128
|
+
itemid = item['@id']
|
129
|
+
debug("frame") {"value property #{prop} recurse for #{itemid.inspect}"}
|
130
|
+
|
131
|
+
# passing a new map as subjects that contains the @id of item as the key and the subject reference as the value. Pass the first value from frame for property as frame, output as parent, and property as active property
|
132
|
+
frame(state, {itemid => @subjects[itemid]}, frame[prop].first, output, prop)
|
133
|
+
else
|
134
|
+
# Otherwise, append a copy of item to active property in output.
|
135
|
+
debug("frame") {"value property #{prop} non-subject ref #{item.inspect}"}
|
136
|
+
add_frame_output(state, output, prop, item)
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
# Process each property and value in frame in lexographical order, where property is not a keyword, as follows:
|
142
|
+
frame.keys.sort.each do |prop|
|
143
|
+
next if prop[0,1] == '@' || output.has_key?(prop)
|
144
|
+
property_frame = frame[prop]
|
145
|
+
debug("frame") {"frame prop: #{prop.inspect}. property_frame: #{property_frame.inspect}"}
|
146
|
+
|
147
|
+
# Set property frame to the first item in value or a newly created JSON object if value is empty.
|
148
|
+
property_frame = property_frame.first || {}
|
149
|
+
|
150
|
+
# Skip to the next property in frame if property is in output or if property frame contains @omitDefault which is true or if it does not contain @omitDefault but the value of omit default flag true.
|
151
|
+
next if output.has_key?(prop) || get_frame_flag(state, property_frame, 'omitDefault')
|
152
|
+
|
153
|
+
# Set the value of property in output to a new JSON object with a property @preserve and a value that is a copy of the value of @default in frame if it exists, or the string @null otherwise
|
154
|
+
default = property_frame.fetch('@default', '@null').dup
|
155
|
+
default = [default] unless default.is_a?(Array)
|
156
|
+
output[prop] = [{"@preserve" => default.compact}]
|
157
|
+
debug("=>") {"add default #{output[prop].inspect}"}
|
158
|
+
end
|
159
|
+
|
160
|
+
# Add output to parent
|
161
|
+
add_frame_output(state, parent, property, output)
|
162
|
+
end
|
163
|
+
end
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
##
|
168
|
+
# Build hash of subjects used for framing. Also returns flattened representation
|
169
|
+
# of input.
|
170
|
+
#
|
171
|
+
# @param [Hash{String => Hash}] subjects
|
172
|
+
# destination for mapped subjects and their Object representations
|
173
|
+
# @param [Array, Hash] input
|
174
|
+
# JSON-LD in expanded form
|
175
|
+
# @param [BlankNodeNamer] namer
|
176
|
+
# @return
|
177
|
+
# input with subject definitions changed to references
|
178
|
+
def get_framing_subjects(subjects, input, namer)
|
179
|
+
depth do
|
180
|
+
debug("framing subjects") {"input: #{input.inspect}"}
|
181
|
+
case input
|
182
|
+
when Array
|
183
|
+
input.map {|o| get_framing_subjects(subjects, o, namer)}
|
184
|
+
when Hash
|
185
|
+
case
|
186
|
+
when subject?(input) || subject_reference?(input)
|
187
|
+
# Get name for subject, mapping old blank node identifiers to new
|
188
|
+
name = blank_node?(input) ? namer.get_name(input.fetch('@id', nil)) : input['@id']
|
189
|
+
debug("framing subjects") {"new subject: #{name.inspect}"} unless subjects.has_key?(name)
|
190
|
+
subject = subjects[name] ||= {'@id' => name}
|
191
|
+
|
192
|
+
# In property order
|
193
|
+
input.keys.sort.each do |prop|
|
194
|
+
value = input[prop]
|
195
|
+
case prop
|
196
|
+
when '@id'
|
197
|
+
# Skip @id, already assigned
|
198
|
+
when /^@/
|
199
|
+
# Copy other keywords
|
200
|
+
subject[prop] = value
|
201
|
+
else
|
202
|
+
case value
|
203
|
+
when Hash
|
204
|
+
# Special case @list, which is not in expanded form
|
205
|
+
raise InvalidFrame::Syntax, "Unexpected hash value: #{value.inspect}" unless value.has_key?('@list')
|
206
|
+
|
207
|
+
# Map entries replacing subjects with subject references
|
208
|
+
subject[prop] = {"@list" =>
|
209
|
+
value['@list'].map {|o| get_framing_subjects(subjects, o, namer)}
|
210
|
+
}
|
211
|
+
when Array
|
212
|
+
# Map array entries
|
213
|
+
subject[prop] = get_framing_subjects(subjects, value, namer)
|
214
|
+
else
|
215
|
+
raise InvalidFrame::Syntax, "unexpected value: #{value.inspect}"
|
216
|
+
end
|
217
|
+
end
|
218
|
+
end
|
219
|
+
|
220
|
+
# Return as subject reference
|
221
|
+
{"@id" => name}
|
222
|
+
else
|
223
|
+
# At this point, it's not a subject or a reference, just return input
|
224
|
+
input
|
225
|
+
end
|
226
|
+
else
|
227
|
+
# Returns equivalent representation
|
228
|
+
input
|
229
|
+
end
|
230
|
+
end
|
231
|
+
end
|
232
|
+
|
233
|
+
##
|
234
|
+
# Flatten input, used in framing.
|
235
|
+
#
|
236
|
+
# This algorithm works by transforming input to statements, and then back to JSON-LD
|
237
|
+
#
|
238
|
+
# @return [Array{Hash}]
|
239
|
+
def flatten
|
240
|
+
debug("flatten")
|
241
|
+
expanded = depth {self.expand(self.value, nil, context)}
|
242
|
+
statements = []
|
243
|
+
depth {self.statements("", expanded, nil, nil, nil ) {|s| statements << s}}
|
244
|
+
debug("flatten") {"statements: #{statements.map(&:to_nquads).join("\n")}"}
|
245
|
+
|
246
|
+
# Transform back to JSON-LD, not flattened
|
247
|
+
depth {self.from_statements(statements, BlankNodeNamer.new("t"))}
|
248
|
+
end
|
249
|
+
|
250
|
+
##
|
251
|
+
# Replace @preserve keys with the values, also replace @null with null
|
252
|
+
#
|
253
|
+
# @param [Array, Hash] input
|
254
|
+
# @return [Array, Hash]
|
255
|
+
def cleanup_preserve(input)
|
256
|
+
depth do
|
257
|
+
#debug("cleanup preserve") {input.inspect}
|
258
|
+
result = case input
|
259
|
+
when Array
|
260
|
+
# If, after replacement, an array contains only the value null remove the value, leaving an empty array.
|
261
|
+
input.map {|o| cleanup_preserve(o)}.compact
|
262
|
+
when Hash
|
263
|
+
output = Hash.ordered
|
264
|
+
input.each do |key, value|
|
265
|
+
if key == '@preserve'
|
266
|
+
# replace all key-value pairs where the key is @preserve with the value from the key-pair
|
267
|
+
output = cleanup_preserve(value)
|
268
|
+
else
|
269
|
+
v = cleanup_preserve(value)
|
270
|
+
|
271
|
+
# Because we may have added a null value to an array, we need to clean that up, if we possible
|
272
|
+
v = v.first if v.is_a?(Array) && v.length == 1 &&
|
273
|
+
context.expand_iri(key) != "@graph" && context.container(key).nil?
|
274
|
+
output[key] = v
|
275
|
+
end
|
276
|
+
end
|
277
|
+
output
|
278
|
+
when '@null'
|
279
|
+
# If the value from the key-pair is @null, replace the value with nul
|
280
|
+
nil
|
281
|
+
else
|
282
|
+
input
|
283
|
+
end
|
284
|
+
#debug(" => ") {result.inspect}
|
285
|
+
result
|
286
|
+
end
|
287
|
+
end
|
288
|
+
|
289
|
+
private
|
290
|
+
|
291
|
+
##
|
292
|
+
# Returns a map of all of the subjects that match a parsed frame.
|
293
|
+
#
|
294
|
+
# @param state the current framing state.
|
295
|
+
# @param subjects the set of subjects to filter.
|
296
|
+
# @param frame the parsed frame.
|
297
|
+
#
|
298
|
+
# @return all of the matched subjects.
|
299
|
+
def filter_subjects(state, subjects, frame)
|
300
|
+
subjects.dup.keep_if {|id, element| filter_subject(state, element, frame)}
|
301
|
+
end
|
302
|
+
|
303
|
+
##
|
304
|
+
# Returns true if the given subject matches the given frame.
|
305
|
+
#
|
306
|
+
# Matches either based on explicit type inclusion where the subject
|
307
|
+
# has any type listed in the frame. If the frame has empty types defined
|
308
|
+
# matches subjects not having a @type. If the frame has a type of {} defined
|
309
|
+
# matches subjects having any type defined.
|
310
|
+
#
|
311
|
+
# Otherwise, does duck typing, where the subject must have all of the properties
|
312
|
+
# defined in the frame.
|
313
|
+
#
|
314
|
+
# @param [Hash{Symbol => Object}] state the current frame state.
|
315
|
+
# @param [Hash{String => Object}] subject the subject to check.
|
316
|
+
# @param [Hash{String => Object}] frame the frame to check.
|
317
|
+
#
|
318
|
+
# @return true if the subject matches, false if not.
|
319
|
+
def filter_subject(state, subject, frame)
|
320
|
+
if types = frame.fetch('@type', nil)
|
321
|
+
subject_types = subject.fetch('@type', [])
|
322
|
+
raise InvalidFrame::Syntax, "frame @type must be an array: #{types.inspect}" unless types.is_a?(Array)
|
323
|
+
raise InvalidFrame::Syntax, "subject @type must be an array: #{subject_types.inspect}" unless subject_types.is_a?(Array)
|
324
|
+
# If frame has an @type, use it for selecting appropriate subjects.
|
325
|
+
debug("frame") {"filter subject: #{subject_types.inspect} has any of #{types.inspect}"}
|
326
|
+
|
327
|
+
# Check for type wild-card, or intersection
|
328
|
+
types == [{}] ? !subject_types.empty? : subject_types.any? {|t| types.include?(t)}
|
329
|
+
else
|
330
|
+
# Duck typing, for subjects not having a type, but having @id
|
331
|
+
|
332
|
+
# Subject matches if it has all the properties in the frame
|
333
|
+
frame_keys = frame.keys.reject {|k| k[0,1] == '@'}
|
334
|
+
subject_keys = subject.keys.reject {|k| k[0,1] == '@'}
|
335
|
+
(frame_keys & subject_keys) == frame_keys
|
336
|
+
end
|
337
|
+
end
|
338
|
+
|
339
|
+
def validate_frame(state, frame)
|
340
|
+
raise InvalidFrame::Syntax,
|
341
|
+
"Invalid JSON-LD syntax; a JSON-LD frame must be an object" unless frame.is_a?(Hash)
|
342
|
+
end
|
343
|
+
|
344
|
+
# Return value of @name in frame, or default from state if it doesn't exist
|
345
|
+
def get_frame_flag(state, frame, name)
|
346
|
+
!!(frame.fetch("@#{name}", [state[name.to_sym]]).first)
|
347
|
+
end
|
348
|
+
|
349
|
+
##
|
350
|
+
# Removes an existing embed.
|
351
|
+
#
|
352
|
+
# @param state the current framing state.
|
353
|
+
# @param id the @id of the embed to remove.
|
354
|
+
def remove_embed(state, id)
|
355
|
+
debug("frame") {"remove embed #{id.inspect}"}
|
356
|
+
# get existing embed
|
357
|
+
embeds = state[:embeds];
|
358
|
+
embed = embeds[id];
|
359
|
+
parent = embed[:parent];
|
360
|
+
property = embed[:property];
|
361
|
+
|
362
|
+
# create reference to replace embed
|
363
|
+
subject = {}
|
364
|
+
subject['@id'] = id
|
365
|
+
ref = {'@id' => id}
|
366
|
+
|
367
|
+
# remove existing embed
|
368
|
+
if subject?(parent)
|
369
|
+
# replace subject with reference
|
370
|
+
parent[property].map! do |v|
|
371
|
+
v.is_a?(Hash) && v.fetch('@id', nil) == id ? ref : v
|
372
|
+
end
|
373
|
+
end
|
374
|
+
|
375
|
+
# recursively remove dependent dangling embeds
|
376
|
+
def remove_dependents(id, embeds)
|
377
|
+
debug("frame") {"remove dependents for #{id}"}
|
378
|
+
|
379
|
+
depth do
|
380
|
+
# get embed keys as a separate array to enable deleting keys in map
|
381
|
+
embeds.each do |id_dep, e|
|
382
|
+
p = e.fetch(:parent, {}) if e.is_a?(Hash)
|
383
|
+
next unless p.is_a?(Hash)
|
384
|
+
pid = p.fetch('@id', nil)
|
385
|
+
if pid == id
|
386
|
+
debug("frame") {"remove #{id_dep} from embeds"}
|
387
|
+
embeds.delete(id_dep)
|
388
|
+
remove_dependents(id_dep, embeds)
|
389
|
+
end
|
390
|
+
end
|
391
|
+
end
|
392
|
+
end
|
393
|
+
|
394
|
+
remove_dependents(id, embeds)
|
395
|
+
end
|
396
|
+
|
397
|
+
##
|
398
|
+
# Adds framing output to the given parent.
|
399
|
+
#
|
400
|
+
# @param state the current framing state.
|
401
|
+
# @param parent the parent to add to.
|
402
|
+
# @param property the parent property, null for an array parent.
|
403
|
+
# @param output the output to add.
|
404
|
+
def add_frame_output(state, parent, property, output)
|
405
|
+
if parent.is_a?(Hash)
|
406
|
+
debug("frame") { "add for property #{property.inspect}: #{output.inspect}"}
|
407
|
+
parent[property] ||= []
|
408
|
+
parent[property] << output
|
409
|
+
else
|
410
|
+
debug("frame") { "add top-level: #{output.inspect}"}
|
411
|
+
parent << output
|
412
|
+
end
|
413
|
+
end
|
414
|
+
|
415
|
+
##
|
416
|
+
# Embeds values for the given element and property into output.
|
417
|
+
def embed_values(state, element, property, output)
|
418
|
+
element[property].each do |o|
|
419
|
+
# Get element @id, if this is an object
|
420
|
+
sid = o['@id'] if subject_reference?(o)
|
421
|
+
if sid
|
422
|
+
unless state[:embeds].has_key?(sid)
|
423
|
+
debug("frame") {"embed element #{sid.inspect}"}
|
424
|
+
# Embed full element, if it isn't already embedded
|
425
|
+
embed = {:parent => output, :property => property}
|
426
|
+
state[:embeds][sid] = embed
|
427
|
+
|
428
|
+
# Recurse into element
|
429
|
+
s = @subjects.fetch(sid, {'@id' => sid})
|
430
|
+
o = {}
|
431
|
+
s.each do |prop, value|
|
432
|
+
if prop[0,1] == '@'
|
433
|
+
# Copy keywords
|
434
|
+
o[prop] = s[prop].dup
|
435
|
+
else
|
436
|
+
depth do
|
437
|
+
debug("frame") {"embed property #{prop.inspect} value #{value.inspect}"}
|
438
|
+
embed_values(state, s, prop, o)
|
439
|
+
end
|
440
|
+
end
|
441
|
+
end
|
442
|
+
else
|
443
|
+
debug("frame") {"don't embed element #{sid.inspect}"}
|
444
|
+
end
|
445
|
+
else
|
446
|
+
debug("frame") {"embed property #{property.inspect}, value #{o.inspect}"}
|
447
|
+
end
|
448
|
+
add_frame_output(state, output, property, o.dup)
|
449
|
+
end
|
450
|
+
end
|
451
|
+
end
|
452
|
+
end
|