json-ld 1.1.8 → 1.1.9
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +4 -0
- data/VERSION +1 -1
- data/lib/json/ld/api.rb +92 -63
- data/lib/json/ld/context.rb +2 -2
- data/lib/json/ld/expand.rb +1 -1
- data/lib/json/ld/extensions.rb +0 -33
- data/lib/json/ld/flatten.rb +99 -150
- data/lib/json/ld/format.rb +3 -1
- data/lib/json/ld/frame.rb +235 -222
- data/lib/json/ld/reader.rb +8 -10
- data/lib/json/ld/utils.rb +104 -4
- data/lib/json/ld/writer.rb +0 -16
- data/spec/api_spec.rb +14 -9
- data/spec/format_spec.rb +7 -9
- data/spec/reader_spec.rb +12 -9
- data/spec/spec_helper.rb +12 -5
- data/spec/streaming_writer_spec.rb +2 -4
- data/spec/suite_from_rdf_spec.rb +1 -0
- data/spec/suite_helper.rb +18 -18
- data/spec/writer_spec.rb +3 -4
- metadata +38 -24
data/lib/json/ld/flatten.rb
CHANGED
@@ -5,169 +5,118 @@ module JSON::LD
|
|
5
5
|
##
|
6
6
|
# This algorithm creates a JSON object node map holding an indexed representation of the graphs and nodes represented in the passed expanded document. All nodes that are not uniquely identified by an IRI get assigned a (new) blank node identifier. The resulting node map will have a member for every graph in the document whose value is another object with a member for every node represented in the document. The default graph is stored under the @default member, all other graphs are stored under their graph name.
|
7
7
|
#
|
8
|
-
# @param [Array, Hash]
|
9
|
-
# Expanded
|
10
|
-
# @param [Hash
|
11
|
-
#
|
12
|
-
# @param [String] active_graph
|
8
|
+
# @param [Array, Hash] input
|
9
|
+
# Expanded JSON-LD input
|
10
|
+
# @param [Hash] graphs A map of graph name to subjects
|
11
|
+
# @param [String] graph
|
13
12
|
# The name of the currently active graph that the processor should use when processing.
|
14
|
-
# @param [String]
|
15
|
-
# The
|
16
|
-
# @param [String] active_property
|
17
|
-
# The currently active property or keyword that the processor should use when processing.
|
13
|
+
# @param [String] name
|
14
|
+
# The name assigned to the current input if it is a bnode
|
18
15
|
# @param [Array] list
|
19
|
-
# List
|
20
|
-
def
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
list = nil)
|
16
|
+
# List to append to, nil for none
|
17
|
+
def create_node_map(input,
|
18
|
+
graphs,
|
19
|
+
graph = '@default',
|
20
|
+
name = nil,
|
21
|
+
list = nil)
|
26
22
|
depth do
|
27
|
-
debug("node_map") {"
|
28
|
-
|
29
|
-
|
30
|
-
# If
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
}
|
39
|
-
else
|
40
|
-
# Otherwise element is a JSON object. Reference the JSON object which is the value of the active graph member of node map using the variable graph. If the active subject is null, set node to null otherwise reference the active subject member of graph using the variable node.
|
41
|
-
# Spec FIXME: initializing it to an empty JSON object, if necessary
|
42
|
-
raise "Expected element to be a hash, was #{element.class}" unless element.is_a?(Hash)
|
43
|
-
graph = node_map[active_graph] ||= {}
|
44
|
-
node = graph[active_subject] if active_subject
|
45
|
-
|
46
|
-
# If element has an @type member, perform for each item the following steps:
|
47
|
-
if element.has_key?('@type')
|
48
|
-
types = Array(element['@type']).map do |item|
|
49
|
-
# If item is a blank node identifier, replace it with a newly generated blank node identifier passing item for identifier.
|
50
|
-
blank_node?(item) ? namer.get_name(item) : item
|
51
|
-
end
|
52
|
-
|
53
|
-
element['@type'] = element['@type'].is_a?(Array) ? types : types.first
|
54
|
-
end
|
55
|
-
|
56
|
-
# If element has an @value member, perform the following steps:
|
57
|
-
if value?(element)
|
58
|
-
unless list
|
59
|
-
# If no list has been passed, merge element into the active property member of the active subject in graph.
|
60
|
-
merge_value(node, active_property, element)
|
61
|
-
else
|
62
|
-
# Otherwise, append element to the @list member of list.
|
63
|
-
merge_value(list, '@list', element)
|
64
|
-
end
|
65
|
-
elsif list?(element)
|
66
|
-
# Otherwise, if element has an @list member, perform the following steps:
|
67
|
-
# Initialize a new JSON object result having a single member @list whose value is initialized to an empty array.
|
68
|
-
result = {'@list' => []}
|
69
|
-
|
70
|
-
# Recursively call this algorithm passing the value of element's @list member as new element and result as list.
|
71
|
-
generate_node_map(element['@list'],
|
72
|
-
node_map,
|
73
|
-
active_graph,
|
74
|
-
active_subject,
|
75
|
-
active_property,
|
76
|
-
result)
|
77
|
-
|
78
|
-
# Append result to the the value of the active property member of node.
|
79
|
-
debug("node_map") {"@list: #{result.inspect}"}
|
80
|
-
merge_value(node, active_property, result)
|
23
|
+
debug("node_map") {"graph: #{graph}, input: #{input.inspect}, name: #{name}"}
|
24
|
+
case input
|
25
|
+
when Array
|
26
|
+
# If input is an array, process each entry in input recursively by passing item for input, node map, active graph, active subject, active property, and list.
|
27
|
+
input.map {|o| create_node_map(o, graphs, graph, nil, list)}
|
28
|
+
when Hash
|
29
|
+
type = input['@type']
|
30
|
+
if value?(input)
|
31
|
+
# Rename blanknode @type
|
32
|
+
input['@type'] = namer.get_name(type) if type && blank_node?(type)
|
33
|
+
list << input if list
|
81
34
|
else
|
82
|
-
#
|
83
|
-
|
84
|
-
# If element has an @id member, set id to its value and remove the member from element. If id is a blank node identifier, replace it with a newly generated blank node identifier passing id for identifier.
|
85
|
-
# Otherwise, set id to the result of the Generate Blank Node Identifier algorithm passing null for identifier.
|
86
|
-
id = element.delete('@id')
|
87
|
-
id = namer.get_name(id) if blank_node?(id)
|
88
|
-
debug("node_map") {"id: #{id.inspect}"}
|
35
|
+
# Input is a node definition
|
89
36
|
|
90
|
-
#
|
91
|
-
|
37
|
+
# spec requires @type to be named first, so assign names early
|
38
|
+
Array(type).each {|t| namer.get_name(t) if blank_node?(t)}
|
92
39
|
|
93
|
-
#
|
94
|
-
if
|
95
|
-
|
96
|
-
|
97
|
-
elsif active_property
|
98
|
-
# Create a new JSON object reference consisting of a single member @id whose value is id.
|
99
|
-
reference = {'@id' => id}
|
100
|
-
|
101
|
-
# If list is null:
|
102
|
-
unless list
|
103
|
-
merge_value(node, active_property, reference)
|
104
|
-
else
|
105
|
-
merge_value(list, '@list', reference)
|
106
|
-
end
|
40
|
+
# get name for subject
|
41
|
+
if name.nil?
|
42
|
+
name ||= input['@id']
|
43
|
+
name = namer.get_name(name) if blank_node?(name)
|
107
44
|
end
|
108
45
|
|
109
|
-
#
|
110
|
-
|
111
|
-
|
112
|
-
#
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
46
|
+
# add subject reference to list
|
47
|
+
list << {'@id' => name} if list
|
48
|
+
|
49
|
+
# create new subject or merge into existing one
|
50
|
+
subject = (graphs[graph] ||= {})[name] ||= {'@id' => name}
|
51
|
+
|
52
|
+
input.keys.kw_sort.each do |property|
|
53
|
+
objects = input[property]
|
54
|
+
case property
|
55
|
+
when '@id'
|
56
|
+
# Skip
|
57
|
+
when '@reverse'
|
58
|
+
# handle reverse properties
|
59
|
+
referenced_node, reverse_map = {'@id' => name}, objects
|
60
|
+
reverse_map.each do |reverse_property, items|
|
61
|
+
items.each do |item|
|
62
|
+
item_name = item['@id']
|
63
|
+
item_name = namer.get_name(item_name) if blank_node?(item_name)
|
64
|
+
create_node_map(item, graphs, graph, item_name)
|
65
|
+
add_value(graphs[graph][item_name],
|
66
|
+
reverse_property,
|
67
|
+
referenced_node,
|
68
|
+
property_is_array: true,
|
69
|
+
allow_duplicate: false)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
when '@graph'
|
73
|
+
graphs[name] ||= {}
|
74
|
+
g = graph == '@merged' ? graph : name
|
75
|
+
create_node_map(objects, graphs, g)
|
76
|
+
when /^@(?!type)/
|
77
|
+
# copy non-@type keywords
|
78
|
+
if property == '@index' && subject['@index']
|
79
|
+
raise JsonLdError::ConflictingIndexes,
|
80
|
+
"Element already has index #{subject['@index']} dfferent from #{input['@index']}" if
|
81
|
+
subject['@index'] != input['@index']
|
82
|
+
subject['@index'] = input.delete('@index')
|
83
|
+
end
|
84
|
+
subject[property] = objects
|
85
|
+
else
|
86
|
+
# if property is a bnode, assign it a new id
|
87
|
+
property = namer.get_name(property) if blank_node?(property)
|
88
|
+
|
89
|
+
add_value(subject, property, [], property_is_array: true) if objects.empty?
|
90
|
+
|
91
|
+
objects.each do |o|
|
92
|
+
o = namer.get_name(o) if property == '@type' && blank_node?(o)
|
93
|
+
|
94
|
+
case
|
95
|
+
when node?(o) || node_reference?(o)
|
96
|
+
id = o['@id']
|
97
|
+
id = namer.get_name(id) if blank_node?(id)
|
98
|
+
|
99
|
+
# add reference and recurse
|
100
|
+
add_value(subject, property, {'@id' => id}, property_is_array: true, allow_duplicate: false)
|
101
|
+
create_node_map(o, graphs, graph, id)
|
102
|
+
when list?(o)
|
103
|
+
olist = []
|
104
|
+
create_node_map(o['@list'], graphs, graph, name, olist)
|
105
|
+
o = {'@list' => olist}
|
106
|
+
add_value(subject, property, o, property_is_array: true, allow_duplicate: true)
|
107
|
+
else
|
108
|
+
# handle @value
|
109
|
+
create_node_map(o, graphs, graph, name)
|
110
|
+
add_value(subject, property, o, property_is_array: true, allow_duplicate: false)
|
111
|
+
end
|
138
112
|
end
|
139
113
|
end
|
140
114
|
end
|
141
|
-
|
142
|
-
# If element has an @graph member, recursively invoke this algorithm passing the value of the @graph member for element, node map, and id for active graph before removing the @graph member from element.
|
143
|
-
if element.has_key?('@graph')
|
144
|
-
generate_node_map(element.delete('@graph'),
|
145
|
-
node_map,
|
146
|
-
id)
|
147
|
-
end
|
148
|
-
|
149
|
-
# Finally, for each key-value pair property-value in element ordered by property perform the following steps:
|
150
|
-
# Note: Not ordering doesn't seem to affect results and is more performant
|
151
|
-
element.keys.each do |property|
|
152
|
-
value = element[property]
|
153
|
-
|
154
|
-
# If property is a blank node identifier, replace it with a newly generated blank node identifier passing property for identifier.
|
155
|
-
property = namer.get_name(property) if blank_node?(property)
|
156
|
-
|
157
|
-
# If node does not have a property member, create one and initialize its value to an empty array.
|
158
|
-
node[property] ||= []
|
159
|
-
|
160
|
-
# Recursively invoke this algorithm passing value as new element, id as new active subject, and property as new active property.
|
161
|
-
generate_node_map(value,
|
162
|
-
node_map,
|
163
|
-
active_graph,
|
164
|
-
id,
|
165
|
-
property)
|
166
|
-
end
|
167
115
|
end
|
116
|
+
else
|
117
|
+
# add non-object to list
|
118
|
+
list << input if list
|
168
119
|
end
|
169
|
-
|
170
|
-
debug("node_map") {node_map.to_json(JSON_STATE) rescue 'malformed json'}
|
171
120
|
end
|
172
121
|
end
|
173
122
|
end
|
data/lib/json/ld/format.rb
CHANGED
@@ -39,7 +39,9 @@ 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*"@(id|context|type)"/m)
|
42
|
+
!!sample.match(/\{\s*"@(id|context|type)"/m) &&
|
43
|
+
# Exclude CSVW metadata
|
44
|
+
!sample.include?("http://www.w3.org/ns/csvw")
|
43
45
|
end
|
44
46
|
|
45
47
|
##
|
data/lib/json/ld/frame.rb
CHANGED
@@ -7,158 +7,140 @@ module JSON::LD
|
|
7
7
|
#
|
8
8
|
# @param [Hash{Symbol => Object}] state
|
9
9
|
# Current framing state
|
10
|
-
# @param [
|
11
|
-
#
|
10
|
+
# @param [Array<String>] subjects
|
11
|
+
# The subjects to filter
|
12
12
|
# @param [Hash{String => Object}] frame
|
13
13
|
# @param [Hash{Symbol => Object}] options ({})
|
14
|
-
# @option options [Hash{String => Object}] :parent
|
15
|
-
# Parent
|
16
|
-
# @option options [String] :property
|
17
|
-
#
|
14
|
+
# @option options [Hash{String => Object}] :parent (nil)
|
15
|
+
# Parent subject or top-level array
|
16
|
+
# @option options [String] :property (nil)
|
17
|
+
# The parent property.
|
18
18
|
# @raise [JSON::LD::InvalidFrame]
|
19
|
-
def frame(state,
|
19
|
+
def frame(state, subjects, frame, options = {})
|
20
20
|
depth do
|
21
21
|
parent, property = options[:parent], options[:property]
|
22
|
-
debug("frame") {"state: #{state.inspect}"}
|
23
|
-
debug("frame") {"nodes: #{nodes.keys.inspect}"}
|
24
|
-
debug("frame") {"frame: #{frame.to_json(JSON_STATE) rescue 'malformed json'}"}
|
25
|
-
debug("frame") {"parent: #{parent.to_json(JSON_STATE) rescue 'malformed json'}"}
|
26
|
-
debug("frame") {"property: #{property.inspect}"}
|
27
22
|
# Validate the frame
|
28
23
|
validate_frame(state, frame)
|
24
|
+
frame = frame.first if frame.is_a?(Array)
|
29
25
|
|
30
|
-
#
|
26
|
+
# Get values for embedOn and explicitOn
|
27
|
+
flags = {
|
28
|
+
embed: get_frame_flag(frame, options, :embed),
|
29
|
+
explicit: get_frame_flag(frame, options, :explicit),
|
30
|
+
requireAll: get_frame_flag(frame, options, :requireAll),
|
31
|
+
}
|
32
|
+
|
33
|
+
# Create a set of matched subjects by filtering subjects by checking the map of flattened subjects against frame
|
31
34
|
# This gives us a hash of objects indexed by @id
|
32
|
-
matches =
|
33
|
-
debug("frame") {"matches: #{matches.keys.inspect}"}
|
35
|
+
matches = filter_subjects(state, subjects, frame, flags)
|
34
36
|
|
35
|
-
#
|
36
|
-
embed = get_frame_flag(state, frame, 'embed');
|
37
|
-
explicit = get_frame_flag(state, frame, 'explicit');
|
38
|
-
debug("frame") {"embed: #{embed.inspect}, explicit: #{explicit.inspect}"}
|
39
|
-
|
40
|
-
# For each id and node from the set of matched nodes ordered by id
|
37
|
+
# For each id and node from the set of matched subjects ordered by id
|
41
38
|
matches.keys.kw_sort.each do |id|
|
42
|
-
|
43
|
-
|
44
|
-
|
39
|
+
subject = matches[id]
|
40
|
+
|
41
|
+
if flags[:embed] == '@link' && state[:link].has_key?(id)
|
42
|
+
# TODO: may want to also match an existing linked subject
|
43
|
+
# against the current frame ... so different frames could
|
44
|
+
# produce different subjects that are only shared in-memory
|
45
|
+
# when the frames are the same
|
46
|
+
|
47
|
+
# add existing linked subject
|
48
|
+
add_frame_output(parent, property, state[:link][id])
|
49
|
+
next
|
50
|
+
end
|
51
|
+
|
52
|
+
# Note: In order to treat each top-level match as a
|
53
|
+
# compartmentalized result, clear the unique embedded subjects map
|
54
|
+
# when the property is None, which only occurs at the top-level.
|
55
|
+
state = state.merge(uniqueEmbeds: {}) if property.nil?
|
45
56
|
|
46
57
|
output = {'@id' => id}
|
47
|
-
|
48
|
-
# prepare embed meta info
|
49
|
-
embedded_node = {parent: parent, property: property}
|
50
|
-
|
51
|
-
# If embedOn is true, and id is in map of embeds from state
|
52
|
-
if embed && (existing = state[:embeds].fetch(id, nil))
|
53
|
-
# only overwrite an existing embed if it has already been added to its parent -- otherwise its parent is somewhere up the tree from this embed and the embed would occur twice once the tree is added
|
54
|
-
embed = false
|
55
|
-
|
56
|
-
embed = if existing[:parent].is_a?(Array)
|
57
|
-
# 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
|
58
|
-
existing[:parent].detect {|p| p['@id'] == id}
|
59
|
-
else
|
60
|
-
# Otherwise, existing has a parent which is a node definition. Set embedOn to true if any of the items in parent property is a node definition or node reference for id because the embed can be overwritten
|
61
|
-
existing[:parent].fetch(existing[:property], []).any? do |v|
|
62
|
-
v.is_a?(Hash) && v.fetch('@id', nil) == id
|
63
|
-
end
|
64
|
-
end
|
65
|
-
debug("frame") {"embed now: #{embed.inspect}"}
|
58
|
+
state[:link][id] = output
|
66
59
|
|
67
|
-
|
68
|
-
|
60
|
+
# if embed is @never or if a circular reference would be created
|
61
|
+
# by an embed, the subject cannot be embedded, just add the
|
62
|
+
# reference; note that a circular reference won't occur when the
|
63
|
+
# embed flag is `@link` as the above check will short-circuit
|
64
|
+
# before reaching this point
|
65
|
+
if flags[:embed] == '@never' || creates_circular_reference(subject, state[:subjectStack])
|
66
|
+
add_frame_output(parent, property, output)
|
67
|
+
next
|
69
68
|
end
|
70
69
|
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
element.keys.kw_sort.each do |prop|
|
81
|
-
value = element[prop]
|
82
|
-
if prop[0,1] == '@'
|
83
|
-
# If property is a keyword, add property and a copy of value to output and continue with the next property from node
|
84
|
-
output[prop] = value.dup
|
85
|
-
next
|
86
|
-
end
|
70
|
+
# if only the last match should be embedded
|
71
|
+
if flags[:embed] == '@last'
|
72
|
+
# remove any existing embed
|
73
|
+
remove_embed(state, id) if state[:uniqueEmbeds].include?(id)
|
74
|
+
state[:uniqueEmbeds][id] = {
|
75
|
+
parent: parent,
|
76
|
+
property: property
|
77
|
+
}
|
78
|
+
end
|
87
79
|
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
value.each do |item|
|
100
|
-
debug("frame") {"value property #{prop.inspect} == #{item.inspect}"}
|
101
|
-
|
102
|
-
# FIXME: If item is a JSON object with the key @list
|
103
|
-
if list?(item)
|
104
|
-
# create a JSON object named list with the key @list and the value of an empty array
|
105
|
-
list = {'@list' => []}
|
106
|
-
|
107
|
-
# Append list to property in output
|
108
|
-
add_frame_output(state, output, prop, list)
|
109
|
-
|
110
|
-
# Process each listitem in the @list array as follows
|
111
|
-
item['@list'].each do |listitem|
|
112
|
-
if node_reference?(listitem)
|
113
|
-
itemid = listitem['@id']
|
114
|
-
debug("frame") {"list item of #{prop} recurse for #{itemid.inspect}"}
|
115
|
-
|
116
|
-
# If listitem is a node reference process listitem recursively using this algorithm passing a new map of nodes that contains the @id of listitem as the key and the node reference as the value. Pass the first value from frame for property as frame, list as parent, and @list as active property.
|
117
|
-
frame(state, {itemid => @node_map[itemid]}, frame[prop].first, parent: list, property: '@list')
|
118
|
-
else
|
119
|
-
# Otherwise, append a copy of listitem to @list in list.
|
120
|
-
debug("frame") {"list item of #{prop} non-node ref #{listitem.inspect}"}
|
121
|
-
add_frame_output(state, list, '@list', listitem)
|
122
|
-
end
|
123
|
-
end
|
124
|
-
elsif node_reference?(item)
|
125
|
-
# If item is a node reference process item recursively
|
126
|
-
# Recurse into sub-objects
|
127
|
-
itemid = item['@id']
|
128
|
-
debug("frame") {"value property #{prop} recurse for #{itemid.inspect}"}
|
129
|
-
|
130
|
-
# passing a new map as nodes that contains the @id of item as the key and the node reference as the value. Pass the first value from frame for property as frame, output as parent, and property as active property
|
131
|
-
frame(state, {itemid => @node_map[itemid]}, frame[prop].first, parent: output, property: prop)
|
132
|
-
else
|
133
|
-
# Otherwise, append a copy of item to active property in output.
|
134
|
-
debug("frame") {"value property #{prop} non-node ref #{item.inspect}"}
|
135
|
-
add_frame_output(state, output, prop, item)
|
136
|
-
end
|
137
|
-
end
|
80
|
+
# push matching subject onto stack to enable circular embed checks
|
81
|
+
state[:subjectStack] << subject
|
82
|
+
|
83
|
+
# iterate over subject properties in order
|
84
|
+
subject.keys.kw_sort.each do |prop|
|
85
|
+
objects = subject[prop]
|
86
|
+
|
87
|
+
# copy keywords to output
|
88
|
+
if prop.start_with?('@')
|
89
|
+
output[prop] = objects.dup
|
90
|
+
next
|
138
91
|
end
|
139
92
|
|
140
|
-
#
|
141
|
-
frame.
|
142
|
-
next if prop[0,1] == '@' || output.has_key?(prop)
|
143
|
-
property_frame = frame[prop]
|
144
|
-
debug("frame") {"frame prop: #{prop.inspect}. property_frame: #{property_frame.inspect}"}
|
93
|
+
# explicit is on and property isn't in frame, skip processing
|
94
|
+
next if flags[:explicit] && !frame.has_key?(prop)
|
145
95
|
|
146
|
-
|
147
|
-
|
96
|
+
# add objects
|
97
|
+
objects.each do |o|
|
98
|
+
case
|
99
|
+
when list?(o)
|
100
|
+
# add empty list
|
101
|
+
list = {'@list' => []}
|
102
|
+
add_frame_output(output, prop, list)
|
148
103
|
|
149
|
-
|
150
|
-
|
104
|
+
src = o['@list']
|
105
|
+
src.each do |oo|
|
106
|
+
if node_reference?(oo)
|
107
|
+
subframe = frame[prop].first['@list'] if frame[prop].is_a?(Array) && frame[prop].first.is_a?(Hash)
|
108
|
+
subframe ||= create_implicit_frame(flags)
|
109
|
+
frame(state, [oo['@id']], subframe, options.merge(parent: list, property: '@list'))
|
110
|
+
else
|
111
|
+
add_frame_output(list, '@list', oo.dup)
|
112
|
+
end
|
113
|
+
end
|
114
|
+
when node_reference?(o)
|
115
|
+
# recurse into subject reference
|
116
|
+
subframe = frame[prop] || create_implicit_frame(flags)
|
117
|
+
frame(state, [o['@id']], subframe, options.merge(parent: output, property: prop))
|
118
|
+
else
|
119
|
+
# include other values automatically
|
120
|
+
add_frame_output(output, prop, o.dup)
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
151
124
|
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
125
|
+
# handle defaults in order
|
126
|
+
frame.keys.kw_sort.reject {|p| p.start_with?('@')}.each do |prop|
|
127
|
+
# if omit default is off, then include default values for
|
128
|
+
# properties that appear in the next frame but are not in
|
129
|
+
# the matching subject
|
130
|
+
n = frame[prop].first || {}
|
131
|
+
omit_default_on = get_frame_flag(n, options, :omitDefault)
|
132
|
+
if !omit_default_on && !output[prop]
|
133
|
+
preserve = n.fetch('@default', '@null').dup
|
134
|
+
preserve = [preserve] unless preserve.is_a?(Array)
|
135
|
+
output[prop] = [{'@preserve' => preserve}]
|
157
136
|
end
|
158
|
-
|
159
|
-
# Add output to parent
|
160
|
-
add_frame_output(state, parent, property, output)
|
161
137
|
end
|
138
|
+
|
139
|
+
# add output to parent
|
140
|
+
add_frame_output(parent, property, output)
|
141
|
+
|
142
|
+
# pop matching subject from circular ref-checking stack
|
143
|
+
state[:subjectStack].pop()
|
162
144
|
end
|
163
145
|
end
|
164
146
|
end
|
@@ -170,10 +152,9 @@ module JSON::LD
|
|
170
152
|
# @return [Array, Hash]
|
171
153
|
def cleanup_preserve(input)
|
172
154
|
depth do
|
173
|
-
#debug("cleanup preserve") {input.inspect}
|
174
155
|
result = case input
|
175
156
|
when Array
|
176
|
-
# If, after replacement, an array contains only the value null remove the value, leaving an empty array.
|
157
|
+
# If, after replacement, an array contains only the value null remove the value, leaving an empty array.
|
177
158
|
input.map {|o| cleanup_preserve(o)}.compact
|
178
159
|
when Hash
|
179
160
|
output = Hash.new(input.size)
|
@@ -183,7 +164,7 @@ module JSON::LD
|
|
183
164
|
output = cleanup_preserve(value)
|
184
165
|
else
|
185
166
|
v = cleanup_preserve(value)
|
186
|
-
|
167
|
+
|
187
168
|
# Because we may have added a null value to an array, we need to clean that up, if we possible
|
188
169
|
v = v.first if v.is_a?(Array) && v.length == 1 &&
|
189
170
|
context.expand_iri(key) != "@graph" && context.container(key).nil?
|
@@ -197,23 +178,29 @@ module JSON::LD
|
|
197
178
|
else
|
198
179
|
input
|
199
180
|
end
|
200
|
-
#debug(" => ") {result.inspect}
|
201
181
|
result
|
202
182
|
end
|
203
183
|
end
|
204
184
|
|
205
185
|
private
|
206
|
-
|
186
|
+
|
207
187
|
##
|
208
|
-
# Returns a map of all of the
|
209
|
-
#
|
210
|
-
# @param
|
211
|
-
#
|
212
|
-
# @param
|
213
|
-
#
|
214
|
-
# @
|
215
|
-
|
216
|
-
|
188
|
+
# Returns a map of all of the subjects that match a parsed frame.
|
189
|
+
#
|
190
|
+
# @param [Hash{Symbol => Object}] state
|
191
|
+
# Current framing state
|
192
|
+
# @param [Hash{String => Hash}] subjects
|
193
|
+
# The subjects to filter
|
194
|
+
# @param [Hash{String => Object}] frame
|
195
|
+
# @param [Hash{Symbol => String}] flags the frame flags.
|
196
|
+
#
|
197
|
+
# @return all of the matched subjects.
|
198
|
+
def filter_subjects(state, subjects, frame, flags)
|
199
|
+
subjects.inject({}) do |memo, id|
|
200
|
+
subject = state[:subjects][id]
|
201
|
+
memo[id] = subject if filter_subject(subject, frame, flags)
|
202
|
+
memo
|
203
|
+
end
|
217
204
|
end
|
218
205
|
|
219
206
|
##
|
@@ -226,42 +213,95 @@ module JSON::LD
|
|
226
213
|
#
|
227
214
|
# Otherwise, does duck typing, where the node must have all of the properties
|
228
215
|
# defined in the frame.
|
229
|
-
#
|
230
|
-
# @param [Hash{
|
231
|
-
# @param [Hash{String => Object}] node the node to check.
|
216
|
+
#
|
217
|
+
# @param [Hash{String => Object}] subject the subject to check.
|
232
218
|
# @param [Hash{String => Object}] frame the frame to check.
|
233
|
-
#
|
234
|
-
#
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
# If frame has an @type, use it for selecting appropriate nodes.
|
242
|
-
debug("frame") {"filter node: #{node_types.inspect} has any of #{types.inspect}"}
|
219
|
+
# @param [Hash{Symbol => Object}] flags the frame flags.
|
220
|
+
#
|
221
|
+
# @return [Boolean] true if the node matches, false if not.
|
222
|
+
def filter_subject(subject, frame, flags)
|
223
|
+
types = frame.fetch('@type', [])
|
224
|
+
raise InvalidFrame::Syntax, "frame @type must be an array: #{types.inspect}" unless types.is_a?(Array)
|
225
|
+
subject_types = subject.fetch('@type', [])
|
226
|
+
raise InvalidFrame::Syntax, "node @type must be an array: #{node_types.inspect}" unless subject_types.is_a?(Array)
|
243
227
|
|
244
|
-
|
245
|
-
|
228
|
+
# check @type (object value means 'any' type, fall through to ducktyping)
|
229
|
+
if !types.empty? &&
|
230
|
+
!(types.length == 1 && types.first.is_a?(Hash))
|
231
|
+
# If frame has an @type, use it for selecting appropriate nodes.
|
232
|
+
return types.any? {|t| subject_types.include?(t)}
|
246
233
|
else
|
247
234
|
# Duck typing, for nodes not having a type, but having @id
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
235
|
+
wildcard, matches_some = true, false
|
236
|
+
|
237
|
+
frame.each do |k, v|
|
238
|
+
case k
|
239
|
+
when '@id'
|
240
|
+
return false if v.is_a?(String) && subject['@id'] != v
|
241
|
+
wildcard, matches_some = true, true
|
242
|
+
when '@type'
|
243
|
+
wildcard, matches_some = true, true
|
244
|
+
when /^@/
|
245
|
+
else
|
246
|
+
wildcard = false
|
247
|
+
|
248
|
+
# v == [] means do not match if property is present
|
249
|
+
if subject.has_key?(k)
|
250
|
+
return false if v == []
|
251
|
+
matches_some = true
|
252
|
+
next
|
253
|
+
end
|
254
|
+
|
255
|
+
# all properties must match to be a duck unless a @default is
|
256
|
+
# specified
|
257
|
+
has_default = v.is_a?(Array) && v.length == 1 && v.first.is_a?(Hash) && v.first.has_key?('@default')
|
258
|
+
return false if flags[:requireAll] && !has_default
|
259
|
+
end
|
260
|
+
end
|
261
|
+
|
262
|
+
# return true if wildcard or subject matches some properties
|
263
|
+
wildcard || matches_some
|
253
264
|
end
|
254
265
|
end
|
255
266
|
|
256
267
|
def validate_frame(state, frame)
|
257
268
|
raise InvalidFrame::Syntax,
|
258
|
-
"Invalid JSON-LD syntax; a JSON-LD frame must be an object: #{frame.inspect}" unless
|
269
|
+
"Invalid JSON-LD syntax; a JSON-LD frame must be an object: #{frame.inspect}" unless
|
270
|
+
frame.is_a?(Hash) || (frame.is_a?(Array) && frame.first.is_a?(Hash) && frame.length == 1)
|
259
271
|
end
|
260
|
-
|
261
|
-
#
|
262
|
-
|
263
|
-
|
264
|
-
|
272
|
+
|
273
|
+
# Checks the current subject stack to see if embedding the given subject
|
274
|
+
# would cause a circular reference.
|
275
|
+
#
|
276
|
+
# @param subject_to_embed the subject to embed.
|
277
|
+
# @param subject_stack the current stack of subjects.
|
278
|
+
#
|
279
|
+
# @return true if a circular reference would be created, false if not.
|
280
|
+
def creates_circular_reference(subject_to_embed, subject_stack)
|
281
|
+
subject_stack[0..-2].any? do |subject|
|
282
|
+
subject['@id'] == subject_to_embed['@id']
|
283
|
+
end
|
284
|
+
end
|
285
|
+
|
286
|
+
# Gets the frame flag value for the given flag name.
|
287
|
+
#
|
288
|
+
# @param frame the frame.
|
289
|
+
# @param options the framing options.
|
290
|
+
# @param name the flag name.
|
291
|
+
#
|
292
|
+
# @return the flag value.
|
293
|
+
def get_frame_flag(frame, options, name)
|
294
|
+
rval = frame.fetch("@#{name}", [options[name]]).first
|
295
|
+
rval = rval.values.first if value?(rval)
|
296
|
+
if name == :embed
|
297
|
+
rval = case rval
|
298
|
+
when true then '@last'
|
299
|
+
when false then '@never'
|
300
|
+
when '@always', '@never', '@link' then rval
|
301
|
+
else '@last'
|
302
|
+
end
|
303
|
+
end
|
304
|
+
rval
|
265
305
|
end
|
266
306
|
|
267
307
|
##
|
@@ -270,29 +310,32 @@ module JSON::LD
|
|
270
310
|
# @param state the current framing state.
|
271
311
|
# @param id the @id of the embed to remove.
|
272
312
|
def remove_embed(state, id)
|
273
|
-
debug("frame") {"remove embed #{id.inspect}"}
|
274
313
|
# get existing embed
|
275
|
-
embeds = state[:
|
314
|
+
embeds = state[:uniqueEmbeds];
|
276
315
|
embed = embeds[id];
|
277
|
-
parent = embed[:parent];
|
278
316
|
property = embed[:property];
|
279
317
|
|
280
318
|
# create reference to replace embed
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
319
|
+
subject = {'@id' => id}
|
320
|
+
|
321
|
+
if embed[:parent].is_a?(Array)
|
322
|
+
# replace subject with reference
|
323
|
+
embed[:parent].map! do |parent|
|
324
|
+
compare_values(parent, subject) ? subject : parent
|
325
|
+
end
|
326
|
+
else
|
327
|
+
parent = embed[:parent]
|
287
328
|
# replace node with reference
|
288
|
-
parent[property].
|
289
|
-
|
329
|
+
if parent[property].is_a?(Array)
|
330
|
+
parent[property].reject! {|v| compare_values(v, subject)}
|
331
|
+
parent[property] << subject
|
332
|
+
elsif compare_values(parent[property], subject)
|
333
|
+
parent[property] = subject
|
290
334
|
end
|
291
335
|
end
|
292
336
|
|
293
337
|
# recursively remove dependent dangling embeds
|
294
338
|
def remove_dependents(id, embeds)
|
295
|
-
debug("frame") {"remove dependents for #{id}"}
|
296
339
|
|
297
340
|
depth do
|
298
341
|
# get embed keys as a separate array to enable deleting keys in map
|
@@ -301,70 +344,40 @@ module JSON::LD
|
|
301
344
|
next unless p.is_a?(Hash)
|
302
345
|
pid = p.fetch('@id', nil)
|
303
346
|
if pid == id
|
304
|
-
debug("frame") {"remove #{id_dep} from embeds"}
|
305
347
|
embeds.delete(id_dep)
|
306
348
|
remove_dependents(id_dep, embeds)
|
307
349
|
end
|
308
350
|
end
|
309
351
|
end
|
310
352
|
end
|
311
|
-
|
353
|
+
|
312
354
|
remove_dependents(id, embeds)
|
313
355
|
end
|
314
356
|
|
315
357
|
##
|
316
358
|
# Adds framing output to the given parent.
|
317
359
|
#
|
318
|
-
# @param state the current framing state.
|
319
360
|
# @param parent the parent to add to.
|
320
361
|
# @param property the parent property, null for an array parent.
|
321
362
|
# @param output the output to add.
|
322
|
-
def add_frame_output(
|
363
|
+
def add_frame_output(parent, property, output)
|
323
364
|
if parent.is_a?(Hash)
|
324
|
-
debug("frame") { "add for property #{property.inspect}: #{output.inspect}"}
|
325
365
|
parent[property] ||= []
|
326
366
|
parent[property] << output
|
327
367
|
else
|
328
|
-
debug("frame") { "add top-level: #{output.inspect}"}
|
329
368
|
parent << output
|
330
369
|
end
|
331
370
|
end
|
332
|
-
|
333
|
-
|
334
|
-
#
|
335
|
-
|
336
|
-
|
337
|
-
|
338
|
-
|
339
|
-
|
340
|
-
|
341
|
-
|
342
|
-
# Embed full element, if it isn't already embedded
|
343
|
-
embed = {parent: output, property: property}
|
344
|
-
state[:embeds][sid] = embed
|
345
|
-
|
346
|
-
# Recurse into element
|
347
|
-
s = @node_map.fetch(sid, {'@id' => sid})
|
348
|
-
o = {}
|
349
|
-
s.each do |prop, value|
|
350
|
-
if prop[0,1] == '@'
|
351
|
-
# Copy keywords
|
352
|
-
o[prop] = s[prop].dup
|
353
|
-
else
|
354
|
-
depth do
|
355
|
-
debug("frame") {"embed property #{prop.inspect} value #{value.inspect}"}
|
356
|
-
embed_values(state, s, prop, o)
|
357
|
-
end
|
358
|
-
end
|
359
|
-
end
|
360
|
-
else
|
361
|
-
debug("frame") {"don't embed element #{sid.inspect}"}
|
362
|
-
end
|
363
|
-
else
|
364
|
-
debug("frame") {"embed property #{property.inspect}, value #{o.inspect}"}
|
365
|
-
end
|
366
|
-
add_frame_output(state, output, property, o.dup)
|
367
|
-
end
|
371
|
+
|
372
|
+
# Creates an implicit frame when recursing through subject matches. If
|
373
|
+
# a frame doesn't have an explicit frame for a particular property, then
|
374
|
+
# a wildcard child frame will be created that uses the same flags that
|
375
|
+
# the parent frame used.
|
376
|
+
#
|
377
|
+
# @param [Hash] flags the current framing flags.
|
378
|
+
# @return [Array<Hash>] the implicit frame.
|
379
|
+
def create_implicit_frame(flags)
|
380
|
+
[flags.keys.inject({}) {|memo, key| memo["@#{key}"] = [flags[key]]; memo}]
|
368
381
|
end
|
369
382
|
end
|
370
383
|
end
|